Artifact 254203be6f470418d2244dc2e78a5adf2893e48d61c853d60f72ab2e4b9170a3:
- File
src/smoke.zig
— part of check-in
[7c656e4718]
at
2026-08-16 17:44:08
on branch trunk
— smoke S9: require the carried shun row, not a phantom (#73)
Carrying the /shun config group means a fossil clone records the artifact as suppressed instead of holding a phantom for it. That is the better end state: a phantom is something fossil goes looking for, so the next sync with any third party would undo the takedown.
S9 asked for the phantom specifically and so went red. It now asks fossil's own shun table and REQUIRES the row — tolerating the phantom would leave the scenario unable to notice the carriage regressing. The phantom probe stays as a diagnostic that tells a lost row apart from a served artifact. (user: dmestas, size: 83141) [annotate] [blame] [check-ins using]
//! Fossil interop smoke check — the one sanctioned use of the real `fossil` //! binary. `fossil` is invoked nowhere else in this tree. //! //! Policy, enforced by this file's shape, not by convention: //! - Every byte handed to fossil is zit's OWN output (`artifact.create` / //! `delta.create` over an RFC fixture) -- never the raw RFC transcription, //! never anything fossil produced. //! - fossil may only CONFIRM an RFC-cited zit expectation. A scenario that //! would require comparing zit bytes to fossil-AUTHORED bytes (e.g. //! `fossil test-delta-create`) does not belong here. //! - On any mismatch, print the diff and the RFC citation and exit 1. This //! file never adjusts zit to match fossil -- a mismatch is an operator //! decision (RFC misread vs RFC wrong), recorded in the untracked //! ambiguities ledger, not "fixed" in place. //! //! Milestone-manual: this executable lives solely behind `zig build smoke`. //! `zig build test` compiles it (catching type errors) but never runs its //! scenarios; it is not imported by `root.zig`'s test aggregation. const std = @import("std"); const builtin = @import("builtin"); const zit = @import("zit"); const artifact = zit.artifact; const delta = zit.delta; const build_options = @import("build_options"); const net = std.Io.net; /// The compiled `zit` binary, threaded in by build.zig (a `LazyPath`, always /// rebuilt first). `build_options.zit_exe` is relative to the build root, which /// is this process's cwd; S5 resolves it to an absolute path before spawning it /// as a subprocess (`zit init/add/commit/pack/serve`). Confined to S5 — the /// other scenarios drive only the `zit` LIBRARY, never the binary. const zit_exe: []const u8 = build_options.zit_exe; // --------------------------------------------------------------------------- // RFC fixtures -- the same vectors src/artifact.zig's own tests cite. // --------------------------------------------------------------------------- const a1_1_manifest = @embedFile("rfc_vectors/artifact_a1_1_sha1_manifest.txt"); const a1_2_technote = @embedFile("rfc_vectors/artifact_a1_2_sha3_technote.txt"); const a3_manifest = @embedFile("rfc_vectors/artifact_a3_rcard_manifest.txt"); /// §5/Appendix B.1 delta worked-example pair (102/151-byte sizes), reused /// verbatim from delta.zig's own "create() then apply() round-trips the /// target" test so S4 exercises the same RFC-cited content. const delta_source = "The quick brown fox jumps over the lazy dog. A Fossil delta encodes byte-level differences compactly.\n"; const delta_target = "The quick brown fox jumps over the sleeping fox. A Fossil delta encodes byte-level differences compactly.\n An example delta target with appended tail.\n"; const ManifestScenario = struct { name: []const u8, citation: []const u8, fixture: []const u8, algo: artifact.HashAlgo, }; const manifest_scenarios = [_]ManifestScenario{ .{ .name = "S1", .citation = "§A.1.1/§A.2", .fixture = a1_1_manifest, .algo = .sha1 }, .{ .name = "S2", .citation = "§A.1.2", .fixture = a1_2_technote, .algo = .sha3_256 }, .{ .name = "S3", .citation = "§A.3", .fixture = a3_manifest, .algo = .sha3_256 }, }; pub fn main(init: std.process.Init) !void { const gpa = init.gpa; const io = init.io; var stdout_buffer: [4096]u8 = undefined; var stdout_writer: std.Io.File.Writer = .init(.stdout(), io, &stdout_buffer); const out = &stdout_writer.interface; // Record the exact fossil the scenarios confirm against, verbatim, at the // head of the smoke log: the version line and the PASS/FAIL results then sit // in one block the release notes cite (readiness item S7). A null return is // the milestone-manual skip — fossil absent, nothing to confirm against. if (try fossilVersion(gpa, io)) |ver| { defer gpa.free(ver); try out.print("fossil version: {s}\n", .{std.mem.trim(u8, ver, " \t\r\n")}); } else { try out.writeAll("fossil not found — smoke is milestone-manual\n"); try out.flush(); return; } var arena_state = std.heap.ArenaAllocator.init(gpa); defer arena_state.deinit(); const arena = arena_state.allocator(); const tmp_root = try tempRoot(arena, init.environ_map.*); std.Io.Dir.cwd().deleteTree(io, tmp_root) catch {}; try std.Io.Dir.cwd().createDirPath(io, tmp_root); defer std.Io.Dir.cwd().deleteTree(io, tmp_root) catch {}; var env = try init.environ_map.clone(gpa); defer env.deinit(); const home_dir = try std.fs.path.join(arena, &.{ tmp_root, "home" }); try std.Io.Dir.cwd().createDirPath(io, home_dir); try env.put("FOSSIL_HOME", home_dir); var all_pass = true; for (manifest_scenarios) |sc| { const pass = runManifestScenario(arena, gpa, io, tmp_root, &env, out, sc) catch |err| blk: { try out.print("FAIL {s} ({s}): unexpected error running scenario: {s}\n", .{ sc.name, sc.citation, @errorName(err) }); break :blk false; }; all_pass = all_pass and pass; } const s4_pass = runDeltaScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S4 (§5/§Appendix B.1): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s4_pass; const s5_pass = runCloneFromRepackedScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S5 (§7.5/§3): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s5_pass; const s6_pass = runOpenFromFossilServerScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S6 (§3.7/§4): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s6_pass; const s7_pass = runPushToFossilServerScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S7 (§3.3/§3.7): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s7_pass; const s8_pass = runPullFossilExtensionScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S8 (§3.2/§3.7): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s8_pass; const s9_pass = runCloneOfShunnedScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S9 (§9.5/§8.2): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s9_pass; const s10_pass = runPushOfShunnedScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S10 (§7.7): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s10_pass; try out.flush(); if (!all_pass) std.process.exit(1); } // --------------------------------------------------------------------------- // Preflight // --------------------------------------------------------------------------- /// The runner's `fossil version` output, captured verbatim, or null when fossil /// is absent or errors. Doubles as the availability gate (null ⇒ skip) and the /// version record printed at the head of the smoke log. Caller frees a non-null /// return. fn fossilVersion(gpa: std.mem.Allocator, io: std.Io) !?[]u8 { const res = std.process.run(gpa, io, .{ .argv = &.{ "fossil", "version" } }) catch return null; defer gpa.free(res.stderr); if (!okExit(res.term)) { gpa.free(res.stdout); return null; } return res.stdout; } fn tempRoot(arena: std.mem.Allocator, env: std.process.Environ.Map) ![]const u8 { const base = env.get("TMPDIR") orelse "/tmp"; const trimmed = std.mem.trimEnd(u8, base, "/"); return std.fs.path.join(arena, &.{ trimmed, "zit-smoke" }); } // --------------------------------------------------------------------------- // fossil process helper // --------------------------------------------------------------------------- fn okExit(term: std.process.Child.Term) bool { return switch (term) { .exited => |code| code == 0, else => false, }; } fn runFossil( gpa: std.mem.Allocator, io: std.Io, argv: []const []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, ) !std.process.RunResult { return std.process.run(gpa, io, .{ .argv = argv, .cwd = .{ .path = cwd_path }, .environ_map = env, }); } /// First whitespace-delimited token of `text` (the hash column of /// `fossil sha1sum`/`sha3sum` output: "<hash> <path>\n"). fn firstToken(text: []const u8) []const u8 { const end = std.mem.indexOfAny(u8, text, " \t\r\n") orelse text.len; return text[0..end]; } // --------------------------------------------------------------------------- // Failure reporting -- never authored from fossil's side, only printed. // --------------------------------------------------------------------------- fn printBytesPreview(out: *std.Io.Writer, label: []const u8, bytes: []const u8) !void { try out.print(" {s} (len {d}): ", .{ label, bytes.len }); const max = @min(bytes.len, 96); for (bytes[0..max]) |b| try out.print("{x:0>2}", .{b}); if (bytes.len > max) try out.writeAll("..."); try out.writeAll("\n"); } fn firstDiffOffset(a: []const u8, b: []const u8) ?usize { const n = @min(a.len, b.len); var i: usize = 0; while (i < n) : (i += 1) { if (a[i] != b[i]) return i; } if (a.len != b.len) return n; return null; } fn printDiffWindow(out: *std.Io.Writer, zit_bytes: []const u8, fossil_bytes: []const u8) !void { const off = firstDiffOffset(zit_bytes, fossil_bytes) orelse return; const start = if (off > 16) off - 16 else 0; const zit_end = @min(zit_bytes.len, off + 16); const fossil_end = @min(fossil_bytes.len, off + 16); try out.print(" first diff at byte {d}\n", .{off}); try out.writeAll(" zit : "); for (zit_bytes[start..zit_end]) |b| try out.print("{x:0>2}", .{b}); try out.writeAll("\n fossil : "); for (fossil_bytes[start..fossil_end]) |b| try out.print("{x:0>2}", .{b}); try out.writeAll("\n"); } fn printMismatchFooter(out: *std.Io.Writer, citation: []const u8) !void { try out.print(" RFC citation: {s}\n", .{citation}); try out.writeAll( \\ Fossil disagrees with zit's RFC-cited expectation. Operator decides \\ RFC-misread vs RFC-wrong; record in ambiguities.md. Never change zit \\ to match fossil. \\ ); } // --------------------------------------------------------------------------- // S1/S2/S3 -- manifest scenarios (parse -> create -> confirm via fossil) // --------------------------------------------------------------------------- fn runManifestScenario( arena: std.mem.Allocator, gpa: std.mem.Allocator, io: std.Io, tmp_root: []const u8, env: *const std.process.Environ.Map, out: *std.Io.Writer, sc: ManifestScenario, ) !bool { // zit's own product: the artifact fed to fossil is `create()`'s output, // never the raw RFC transcription (§6.2 round-trip; the two happen to be // byte-identical for S1/S3 and a strict-lexical reorder for S2, but the // input to fossil is always what zit itself wrote). var m = try artifact.parse(gpa, sc.fixture); defer m.deinit(); const zit_bytes = try artifact.create(&m, gpa); defer gpa.free(zit_bytes); var name_buf: [64]u8 = undefined; const zit_name = artifact.artifactName(sc.algo, zit_bytes, &name_buf); const base = try std.fs.path.join(arena, &.{ tmp_root, sc.name }); const repo_dir = try std.fs.path.join(arena, &.{ base, "repo" }); const checkout_dir = try std.fs.path.join(arena, &.{ base, "checkout" }); try std.Io.Dir.cwd().createDirPath(io, repo_dir); try std.Io.Dir.cwd().createDirPath(io, checkout_dir); const manifest_path = try std.fs.path.join(arena, &.{ repo_dir, "manifest.txt" }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = manifest_path, .data = zit_bytes }); // fossil test-parse-manifest: repo-free syntactic accept (§4/§5). Exit is // 0 either way -- the VERDICT TEXT is what confirms zit's parse. { const res = try runFossil(gpa, io, &.{ "fossil", "test-parse-manifest", manifest_path }, repo_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (std.mem.indexOf(u8, res.stdout, "reports the input is ok") == null) { try out.print("FAIL {s} ({s}): fossil test-parse-manifest did not report the input well-formed\n", .{ sc.name, sc.citation }); try printBytesPreview(out, "zit bytes", zit_bytes); try out.print(" fossil verdict: {s}\n", .{res.stdout}); try printMismatchFooter(out, sc.citation); return false; } } // fossil {sha1,sha3}sum: confirms zit's own artifactName computation. { const sum_cmd = if (sc.algo == .sha1) "sha1sum" else "sha3sum"; const res = try runFossil(gpa, io, &.{ "fossil", sum_cmd, manifest_path }, repo_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); const fossil_name = firstToken(res.stdout); if (!std.mem.eql(u8, fossil_name, zit_name)) { try out.print("FAIL {s} ({s}): fossil {s} disagrees with zit's artifactName\n", .{ sc.name, sc.citation, sum_cmd }); try out.print(" zit name: {s}\n", .{zit_name}); try out.print(" fossil name: {s}\n", .{fossil_name}); try printMismatchFooter(out, sc.citation); return false; } } // init + open + put + retrieve: confirms zit's create() output // round-trips through fossil's own content store under the given name. const repo_fossil = try std.fs.path.join(arena, &.{ repo_dir, "r.fossil" }); const init_argv: []const []const u8 = if (sc.algo == .sha1) &.{ "fossil", "init", "--sha1", repo_fossil } else &.{ "fossil", "init", repo_fossil }; { const res = try runFossil(gpa, io, init_argv, repo_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL {s} ({s}): fossil init failed\n stderr: {s}\n", .{ sc.name, sc.citation, res.stderr }); try printMismatchFooter(out, sc.citation); return false; } } { const res = try runFossil(gpa, io, &.{ "fossil", "open", repo_fossil }, checkout_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL {s} ({s}): fossil open failed\n stderr: {s}\n", .{ sc.name, sc.citation, res.stderr }); try printMismatchFooter(out, sc.citation); return false; } } { const res = try runFossil(gpa, io, &.{ "fossil", "test-content-put", manifest_path }, checkout_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL {s} ({s}): fossil test-content-put failed\n stderr: {s}\n", .{ sc.name, sc.citation, res.stderr }); try printMismatchFooter(out, sc.citation); return false; } } const artifact_out_path = try std.fs.path.join(arena, &.{ checkout_dir, "out.bin" }); { const res = try runFossil(gpa, io, &.{ "fossil", "artifact", zit_name, "out.bin" }, checkout_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL {s} ({s}): fossil artifact could not resolve zit's name {s}\n stderr: {s}\n", .{ sc.name, sc.citation, zit_name, res.stderr }); try printMismatchFooter(out, sc.citation); return false; } } const fossil_bytes = try std.Io.Dir.cwd().readFileAlloc(io, artifact_out_path, gpa, .limited(1 << 20)); defer gpa.free(fossil_bytes); if (!std.mem.eql(u8, zit_bytes, fossil_bytes)) { try out.print("FAIL {s} ({s}): fossil artifact returned bytes different from zit's create() output\n", .{ sc.name, sc.citation }); try printBytesPreview(out, "zit bytes", zit_bytes); try printBytesPreview(out, "fossil bytes", fossil_bytes); try printDiffWindow(out, zit_bytes, fossil_bytes); try printMismatchFooter(out, sc.citation); return false; } try out.print("PASS {s} ({s}): zit's create() output confirmed by fossil (name {s})\n", .{ sc.name, sc.citation, zit_name }); return true; } // --------------------------------------------------------------------------- // S4 -- delta round-trip (never compared against fossil test-delta-create; // fossil only applies zit's own delta and confirms the reconstructed target) // --------------------------------------------------------------------------- fn runDeltaScenario( arena: std.mem.Allocator, gpa: std.mem.Allocator, io: std.Io, tmp_root: []const u8, env: *const std.process.Environ.Map, out: *std.Io.Writer, ) !bool { const citation = "§5/§Appendix B.1"; const zit_delta = try delta.create(gpa, delta_source, delta_target); defer gpa.free(zit_delta); // zit's own apply() must already reconstruct the target -- fossil only // ever confirms this, never establishes it. const self_applied = try delta.apply(gpa, delta_source, zit_delta); defer gpa.free(self_applied); std.debug.assert(std.mem.eql(u8, self_applied, delta_target)); const base = try std.fs.path.join(arena, &.{ tmp_root, "S4" }); try std.Io.Dir.cwd().createDirPath(io, base); const src_path = try std.fs.path.join(arena, &.{ base, "src.bin" }); const delta_path = try std.fs.path.join(arena, &.{ base, "zit.delta" }); const out_path = try std.fs.path.join(arena, &.{ base, "out.bin" }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = src_path, .data = delta_source }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = delta_path, .data = zit_delta }); { const res = try runFossil(gpa, io, &.{ "fossil", "test-delta-apply", src_path, delta_path, out_path }, base, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S4 ({s}): fossil test-delta-apply rejected zit's delta.create() output\n stderr: {s}\n", .{ citation, res.stderr }); try printBytesPreview(out, "zit delta", zit_delta); try printMismatchFooter(out, citation); return false; } } const fossil_out = try std.Io.Dir.cwd().readFileAlloc(io, out_path, gpa, .limited(1 << 20)); defer gpa.free(fossil_out); if (!std.mem.eql(u8, fossil_out, delta_target)) { try out.print("FAIL S4 ({s}): fossil test-delta-apply produced bytes different from zit's target\n", .{citation}); try printBytesPreview(out, "zit target", delta_target); try printBytesPreview(out, "fossil bytes", fossil_out); try printDiffWindow(out, delta_target, fossil_out); try printMismatchFooter(out, citation); return false; } try out.print("PASS S4 ({s}): fossil test-delta-apply reconstructs zit's delta.create() target\n", .{citation}); return true; } // --------------------------------------------------------------------------- // S5 -- clone from a re-packed store (§7.5, covering the §3 // serve-regenerated-delta note). The one scenario that drives the real `zit` // BINARY: build a repo, evolve a file so its blob is stored as a delta, run // `zit pack` so that delta is REGENERATED against our size-minimizing base, then // serve the repacked store and have the real `fossil` client CLONE it. fossil // hash-verifies every artifact it reconstructs on receipt, so a clone that exits // 0 proves the regenerated delta container zit serves reconstructs byte-for-byte // over the wire; the checked-out file is compared to zit's own committed bytes as // the final oracle. Every byte fossil sees is zit's own output — fossil only // confirms, never authors (the file policy at the top of this file). // --------------------------------------------------------------------------- /// Allocates a fresh loopback port for a child server. The reservation listener /// closes before the child binds it; that close→child-bind race is unavoidable /// because child servers cannot inherit the listener. fn freshLoopbackPort(io: std.Io) !u16 { const addr = try net.IpAddress.parseIp4("127.0.0.1", 0); var server = try addr.listen(io, .{}); const port = server.socket.address.getPort(); server.deinit(io); if (port == 0) return error.NoLoopbackPort; return port; } /// Build the S5 file blob: 200 similar lines so `zit pack`'s best-of-N base /// selection produces a profitable delta. `changed` mutates one line and appends /// a tail, so the second commit's blob deltifies well against the first. fn buildBlob(arena: std.mem.Allocator, changed: bool) ![]u8 { var aw = std.Io.Writer.Allocating.init(arena); // arena-owned; no deinit var i: usize = 0; while (i < 200) : (i += 1) { if (changed and i == 50) { try aw.writer.print("line {d}: CHANGED CONTENT for the regenerated-delta smoke\n", .{i}); } else { try aw.writer.print("line {d}: the quick brown fox jumps over the lazy dog number {d}\n", .{ i, i }); } } if (changed) try aw.writer.writeAll("line 200: appended tail exercises the delta insert op\n"); return aw.toOwnedSlice(); } /// Absolute path to the compiled `zit` binary. `zit_exe` is relative to the /// build root (this process's cwd); a spawned child chdirs before exec, so it /// must be resolved first. Allocated on `arena`. fn zitAbsPath(arena: std.mem.Allocator, io: std.Io) ![]const u8 { const cwd_abs = try std.process.currentPathAlloc(io, arena); return std.fs.path.resolve(arena, &.{ cwd_abs, zit_exe }); } /// Blocking `zit <tail...>` with cwd `cwd_path` and the shared smoke env /// (FOSSIL_HOME set). Caller owns `result.stdout`/`result.stderr`. fn runZit( gpa: std.mem.Allocator, io: std.Io, abs_exe: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, tail: []const []const u8, ) !std.process.RunResult { var argv_buf: [8][]const u8 = undefined; argv_buf[0] = abs_exe; std.debug.assert(tail.len <= argv_buf.len - 1); for (tail, argv_buf[1..][0..tail.len]) |src, *dst| dst.* = src; return std.process.run(gpa, io, .{ .argv = argv_buf[0 .. 1 + tail.len], .cwd = .{ .path = cwd_path }, .environ_map = env, }); } /// Spawn `zit serve --port <port>` detached, cwd `cwd_path` (so `discoverRoot` /// finds `cwd_path/.zit`). stdout/stderr discarded — driven purely over the /// socket. Caller `kill`s the returned child. fn spawnZitServe( io: std.Io, abs_exe: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, port_str: []const u8, ) !std.process.Child { return std.process.spawn(io, .{ .argv = &.{ abs_exe, "serve", "--port", port_str }, .cwd = .{ .path = cwd_path }, .environ_map = env, .stdout = .ignore, .stderr = .ignore, }); } /// Bounded wait for a server to bind: retry-connect to the loopback port /// until accepted or a wall-clock deadline elapses (never a fixed sleep). A /// successful TCP connect proves the listener is up. fn waitForListen(io: std.Io, port: u16) !void { const addr = net.IpAddress.parseIp4("127.0.0.1", port) catch return error.BadTestAddress; const deadline_ns: i96 = 15_000_000_000; // 15 s cap — orders of magnitude of slack const start = std.Io.Clock.now(.awake, io).nanoseconds; while (true) { if (addr.connect(io, .{ .mode = .stream })) |stream| { stream.close(io); return; } else |_| {} if (std.Io.Clock.now(.awake, io).nanoseconds - start > deadline_ns) return error.ServerNeverBound; } } /// The count parsed from `zit pack`'s "<N> re-deltaed" clause, or null if the /// clause is absent. Used to assert S5's fixture still forces a REGENERATED /// delta (so the scenario keeps testing serve-of-a-regenerated-delta, not just a /// full-form clone). fn reDeltaCount(text: []const u8) ?usize { const idx = std.mem.indexOf(u8, text, " re-deltaed") orelse return null; var start = idx; while (start > 0 and std.ascii.isDigit(text[start - 1])) start -= 1; if (start == idx) return null; return std.fmt.parseInt(usize, text[start..idx], 10) catch null; } /// Run `zit <tail...>` and fail the scenario (printing stderr) unless it exits 0. /// Returns the captured result on success (caller frees), or null on failure /// (already freed + reported). fn zitStep( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, abs_exe: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, tail: []const []const u8, label: []const u8, ) !?std.process.RunResult { return zitStepIn(gpa, io, out, abs_exe, cwd_path, env, tail, label, "S5", "§7.5/§3"); } /// `zitStep` for a named scenario: the same run-or-report-and-abort shape, with /// the scenario name and citation supplied rather than fixed to S5's. fn zitStepIn( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, abs_exe: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, tail: []const []const u8, label: []const u8, scenario: []const u8, citation: []const u8, ) !?std.process.RunResult { const res = try runZit(gpa, io, abs_exe, cwd_path, env, tail); if (!okExit(res.term)) { try out.print("FAIL {s} ({s}): {s} failed\n stderr: {s}\n", .{ scenario, citation, label, res.stderr }); try printMismatchFooter(out, citation); gpa.free(res.stdout); gpa.free(res.stderr); return null; } return res; } fn runCloneFromRepackedScenario( arena: std.mem.Allocator, gpa: std.mem.Allocator, io: std.Io, tmp_root: []const u8, env: *const std.process.Environ.Map, out: *std.Io.Writer, ) !bool { const citation = "§7.5/§3"; const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, "S5" }); const src_dir = try std.fs.path.join(arena, &.{ base, "src" }); try std.Io.Dir.cwd().createDirPath(io, src_dir); // A source repo with two check-ins of an evolving file: the second blob is // stored as a delta, giving `pack` something to re-base. { const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init")) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } const blob_path = try std.fs.path.join(arena, &.{ src_dir, "big.txt" }); const seed = try buildBlob(arena, false); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = seed }); inline for (.{ .{ &[_][]const u8{ "add", "big.txt" }, "zit add (seed)" }, .{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit (seed)" }, }) |step| { const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, step[0], step[1])) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } const edited = try buildBlob(arena, true); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = edited }); inline for (.{ .{ &[_][]const u8{ "add", "big.txt" }, "zit add (grow)" }, .{ &[_][]const u8{ "commit", "-m", "grow", "--user", "u" }, "zit commit (grow)" }, }) |step| { const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, step[0], step[1])) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // Re-pack: every artifact is re-deltified against our chosen base, so a // served delta container is REGENERATED, not the one the write path stored. { const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, &.{"pack"}, "zit pack")) orelse return false; defer gpa.free(r.stdout); defer gpa.free(r.stderr); const n = reDeltaCount(r.stdout) orelse { try out.print("FAIL S5 ({s}): could not parse pack's re-delta count\n pack stdout: {s}\n", .{ citation, r.stdout }); return false; }; if (n == 0) { try out.print("FAIL S5 ({s}): pack regenerated no delta — the fixture no longer exercises a served regenerated delta container\n pack stdout: {s}\n", .{ citation, r.stdout }); return false; } } // Serve the repacked store; the real fossil client clones it over the wire. const s5_serve_port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{s5_serve_port}); var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str); defer server.kill(io); waitForListen(io, s5_serve_port) catch { try out.print("FAIL S5 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, s5_serve_port }); return false; }; // A bare-root URL: a conformant `fossil clone` POSTs to "/" (sync §4). This // is the fossil-client-from-root case e2e explicitly defers to smoke. const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{s5_serve_port}); const clone_repo = try std.fs.path.join(arena, &.{ base, "verify.fossil" }); { const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S5 ({s}): fossil clone from the repacked zit store failed\n stderr: {s}\n", .{ citation, res.stderr }); try printMismatchFooter(out, citation); return false; } } // Check out the clone (autosync off — the server is about to be killed) so // fossil reconstructs the file from the artifacts it received. const co_dir = try std.fs.path.join(arena, &.{ base, "co" }); try std.Io.Dir.cwd().createDirPath(io, co_dir); { const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S5 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr }); try printMismatchFooter(out, citation); return false; } } // THE PROOF: fossil's reconstructed file == zit's own committed bytes. A // regenerated delta that dropped or corrupted a byte fails here. const co_blob = try std.fs.path.join(arena, &.{ co_dir, "big.txt" }); const fossil_bytes = try std.Io.Dir.cwd().readFileAlloc(io, co_blob, gpa, .limited(1 << 20)); defer gpa.free(fossil_bytes); if (!std.mem.eql(u8, edited, fossil_bytes)) { try out.print("FAIL S5 ({s}): fossil clone from the repacked store did not reconstruct zit's committed bytes\n", .{citation}); try printBytesPreview(out, "zit committed", edited); try printBytesPreview(out, "fossil clone", fossil_bytes); try printDiffWindow(out, edited, fossil_bytes); try printMismatchFooter(out, citation); return false; } try out.print("PASS S5 ({s}): fossil clone from a repacked zit store reconstructs zit's committed bytes byte-for-byte\n", .{citation}); return true; } // --------------------------------------------------------------------------- // S6 -- clone → open against a real fossil SERVER (sync §3.7/§4, P3-F3). The // mirror of S5: there zit served and fossil cloned; here fossil serves and the // real `zit` binary clones AND opens, so the working tree zit materializes is // the thing under test. This is the half of the clone story digest-set equality // cannot see — a repository can converge perfectly on the wire and still be // unusable if nothing turns it into files on disk. // // The file policy at the top of this file holds: the compared bytes are OUR OWN // (we author `s6_payload`, hand it to `fossil add`/`fossil commit`, and compare // it to what zit materializes). fossil is the container and the transport in // between; it never authors a byte of the oracle. A mismatch means zit's // artifact reconstruction or its working-tree materialization dropped or // corrupted content that fossil carried faithfully. // --------------------------------------------------------------------------- /// The bytes S6 round-trips. Authored here, never by fossil: multi-line with a /// trailing newline, so a materialization that mangled line endings or dropped /// the tail is caught. const s6_payload = \\line one of the fossil-served source tree \\line two, so a truncation is visible \\line three \\ ; /// Run `fossil <argv...>` and fail the scenario (printing stderr) unless it /// exits 0. Returns true on success; the caller aborts the scenario on false. fn fossilStep( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, argv: []const []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, label: []const u8, ) !bool { const res = try runFossil(gpa, io, argv, cwd_path, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S6 (§3.7/§4): {s} failed\n stderr: {s}\n", .{ label, res.stderr }); return false; } return true; } fn runOpenFromFossilServerScenario( arena: std.mem.Allocator, gpa: std.mem.Allocator, io: std.Io, tmp_root: []const u8, env: *const std.process.Environ.Map, out: *std.Io.Writer, ) !bool { const citation = "§3.7/§4"; const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, "S6" }); const co_dir = try std.fs.path.join(arena, &.{ base, "co" }); try std.Io.Dir.cwd().createDirPath(io, co_dir); const repo_path = try std.fs.path.join(arena, &.{ base, "source.fossil" }); // A real fossil repository holding one check-in of OUR bytes. if (!try fossilStep(gpa, io, out, &.{ "fossil", "init", repo_path }, base, env, "fossil init")) return false; if (!try fossilStep(gpa, io, out, &.{ "fossil", "open", "--nosync", repo_path }, co_dir, env, "fossil open")) return false; const payload_path = try std.fs.path.join(arena, &.{ co_dir, "big.txt" }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = payload_path, .data = s6_payload }); if (!try fossilStep(gpa, io, out, &.{ "fossil", "add", "big.txt" }, co_dir, env, "fossil add")) return false; if (!try fossilStep( gpa, io, out, &.{ "fossil", "commit", "-m", "s6 seed", "--user-override", "u", "--no-warnings" }, co_dir, env, "fossil commit", )) return false; // Serve it with the real fossil server and clone it with the real zit // binary over the bare-root URL (sync §4: the client posts the repo URL path // unchanged and MUST NOT append /xfer). const s6_serve_port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{s6_serve_port}); var server = spawnFossilServe(gpa, io, out, repo_path, base, env, port_str) catch |err| { try out.print("FAIL S6 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) }); return false; }; defer stopFossilServe(io, &server); // The same retry-connect wait S5 uses: a successful TCP connect proves the // listener is up, whichever binary owns it. waitForListen(io, s6_serve_port) catch { try out.print("FAIL S6 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, s6_serve_port }); return false; }; const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{s6_serve_port}); { const r = (try zitStep(gpa, io, out, abs_exe, base, env, &.{ "clone", url, "cloned" }, "zit clone (from fossil server)")) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // OPEN: the clone's checkout is still unborn, so this is what turns the // converged repository into a working tree. const cloned_dir = try std.fs.path.join(arena, &.{ base, "cloned" }); { const r = (try zitStep(gpa, io, out, abs_exe, cloned_dir, env, &.{"open"}, "zit open")) orelse return false; defer gpa.free(r.stdout); defer gpa.free(r.stderr); if (std.mem.indexOf(u8, r.stdout, "trunk") == null) { try out.print("FAIL S6 ({s}): zit open did not report opening trunk\n stdout: {s}\n", .{ citation, r.stdout }); return false; } } // THE PROOF: the file zit materialized == the bytes we committed through // fossil. A dropped artifact, a mis-reconstructed delta, or a materialization // that never wrote the file fails here. const opened_blob = try std.fs.path.join(arena, &.{ cloned_dir, "big.txt" }); const zit_bytes = std.Io.Dir.cwd().readFileAlloc(io, opened_blob, gpa, .limited(1 << 20)) catch |err| { try out.print("FAIL S6 ({s}): zit open left no readable big.txt in the cloned tree: {s}\n", .{ citation, @errorName(err) }); try printMismatchFooter(out, citation); return false; }; defer gpa.free(zit_bytes); if (!std.mem.eql(u8, s6_payload, zit_bytes)) { try out.print("FAIL S6 ({s}): the tree zit opened does not match the bytes committed through fossil\n", .{citation}); try printBytesPreview(out, "committed", s6_payload); try printBytesPreview(out, "zit opened", zit_bytes); try printDiffWindow(out, s6_payload, zit_bytes); try printMismatchFooter(out, citation); return false; } try out.print("PASS S6 ({s}): zit clone+open against a real fossil server materializes the committed bytes byte-for-byte\n", .{citation}); return true; } // --------------------------------------------------------------------------- // S7/S8 -- the PUSH-direction interop pair (sync §3.3/§3.7 push, §3.2/§3.7 // pull). S5/S6 exercise only pull/clone wire (fossil pulls from zit, zit pulls // from fossil); the push-side wire -- zit as the sync CLIENT uploading to a // server, its igot/gimme ordering and capability negotiation -- was invisible. // Both scenarios share the same prelude: zit authors a seed check-in, a real // fossil repo CLONES it (so client and server share a project-code, without // which the sync layer refuses the exchange), zit grows a second check-in the // fossil repo lacks, then zit PUSHES that growth up to a live `fossil server`. // // The file policy at the top holds throughout: every compared byte is OUR OWN // -- we author the pushed file blob and (in S8) the file blob a fossil commit // carries. fossil is the container and the transport; it never authors a byte // of an oracle. A mismatch means zit's push wire, or its pull-and-reconstruct, // dropped or corrupted content fossil relayed faithfully. // --------------------------------------------------------------------------- /// The seed the fossil repo clones and the zit repo shares -- our own bytes. const interop_seed_payload = \\seed line shared by the fossil clone and the zit repo \\ ; /// The file zit authors, commits, and PUSHES to the fossil server. Multi-line /// with a trailing newline so a truncated or line-ending-mangled push is caught. const push_grow_payload = \\zit authored this line and pushed it to a fossil server \\second line so a dropped tail is visible \\ ; /// S8: authored here, committed THROUGH a fossil checkout (fossil is the /// container), that zit must PULL and reconstruct. The library reader confirms /// convergence by reading it back under its own artifact name. const fossil_ext_payload = \\authored here, carried by a fossil commit, pulled by zit \\second line so a truncation is visible \\ ; /// The prelude state both push scenarios build on. const InteropSetup = struct { base: []const u8, zsrc: []const u8, dst_fossil: []const u8, abs_exe: []const u8, }; /// Disable Fossil's detached backoffice worker, then spawn /// `fossil server --port <port> <repo>` in a dedicated process group. The /// repositories served here are disposable smoke fixtures; disabling their /// maintenance worker keeps every process inside the test-owned group. fn spawnFossilServe( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, repo_path: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, port_str: []const u8, ) !std.process.Child { const disable = try runFossil( gpa, io, &.{ "fossil", "settings", "backoffice-disable", "1", "-R", repo_path }, cwd_path, env, ); defer gpa.free(disable.stdout); defer gpa.free(disable.stderr); if (!okExit(disable.term)) { try out.print( "fossil settings backoffice-disable failed ({any})\n stderr: {s}\n", .{ disable.term, disable.stderr }, ); return error.CannotDisableFossilBackoffice; } return std.process.spawn(io, .{ .argv = &.{ "fossil", "server", "--port", port_str, repo_path }, .cwd = .{ .path = cwd_path }, .environ_map = env, .stdout = .ignore, .stderr = .ignore, .pgid = if (builtin.os.tag == .windows) null else 0, }); } /// Stop a Fossil server and all request handlers forked from it. `Child.kill` /// addresses only the original PID, which can leave a POSIX handler holding the /// listening socket after the parent is reaped. fn stopFossilServe(io: std.Io, server: *std.process.Child) void { if (comptime builtin.os.tag != .windows) { if (server.id) |pid| std.posix.kill(-pid, .KILL) catch {}; } server.kill(io); } /// `zit <tail...>` step, tagged with the scenario name/citation for failure /// reporting (the S5-scoped `zitStep` bakes in "S5"). Returns the captured /// result on success (caller frees), or null on a non-zero exit (already freed /// and reported). fn zitStepTagged( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, abs_exe: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, tail: []const []const u8, tag: []const u8, citation: []const u8, label: []const u8, ) !?std.process.RunResult { const res = try runZit(gpa, io, abs_exe, cwd_path, env, tail); if (!okExit(res.term)) { try out.print("FAIL {s} ({s}): {s} failed\n stderr: {s}\n", .{ tag, citation, label, res.stderr }); try printMismatchFooter(out, citation); gpa.free(res.stdout); gpa.free(res.stderr); return null; } return res; } /// `fossil <argv...>` step, tagged with the scenario name/citation (the /// S6-scoped `fossilStep` bakes in "S6"). True on exit 0; the caller aborts the /// scenario on false. fn fossilStepTagged( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, argv: []const []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, tag: []const u8, citation: []const u8, label: []const u8, ) !bool { const res = try runFossil(gpa, io, argv, cwd_path, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL {s} ({s}): {s} failed\n stderr: {s}\n", .{ tag, citation, label, res.stderr }); return false; } return true; } /// The shared prelude: a zit repo and a fossil repo that share a project-code, /// with a zit-authored `grow` check-in PUSHED from zit up to the fossil server. /// Returns the paths on success, or null on any failure (already reported under /// `tag`). fn establishPushedFossil( arena: std.mem.Allocator, gpa: std.mem.Allocator, io: std.Io, tmp_root: []const u8, env: *const std.process.Environ.Map, out: *std.Io.Writer, tag: []const u8, citation: []const u8, sub: []const u8, ) !?InteropSetup { const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, sub }); const zsrc = try std.fs.path.join(arena, &.{ base, "zsrc" }); const dst_fossil = try std.fs.path.join(arena, &.{ base, "dst.fossil" }); try std.Io.Dir.cwd().createDirPath(io, zsrc); // zit authors a seed check-in. { const r = (try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, &.{"init"}, tag, citation, "zit init")) orelse return null; gpa.free(r.stdout); gpa.free(r.stderr); } const seed_path = try std.fs.path.join(arena, &.{ zsrc, "a.txt" }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = seed_path, .data = interop_seed_payload }); inline for (.{ .{ &[_][]const u8{ "add", "a.txt" }, "zit add (seed)" }, .{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit (seed)" }, }) |step| { const r = (try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, step[0], tag, citation, step[1])) orelse return null; gpa.free(r.stdout); gpa.free(r.stderr); } // Serve the seed; the real fossil binary CLONES it, establishing the shared // project-code the sync layer requires before it will accept a push. const seed_port = try freshLoopbackPort(io); const seed_port_str = try std.fmt.allocPrint(arena, "{d}", .{seed_port}); { var server = try spawnZitServe(io, abs_exe, zsrc, env, seed_port_str); waitForListen(io, seed_port) catch { server.kill(io); try out.print("FAIL {s} ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ tag, citation, seed_port }); return null; }; const seed_url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{seed_port}); const ok_clone = try fossilStepTagged(gpa, io, out, &.{ "fossil", "clone", seed_url, dst_fossil }, base, env, tag, citation, "fossil clone of the zit seed"); server.kill(io); if (!ok_clone) return null; } // The clone saved a remote (the now-dead seed server) as the fossil repo's // autosync target; disable autosync so a fossil-side commit (S8) cannot // silently abort trying to reach it. Grant the anonymous loopback sync user // check-in ('i') so the push is authorized (a bare sync request logs in as // "nobody", whose cloned caps are read-only). if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "settings", "autosync", "off", "-R", dst_fossil }, base, env, tag, citation, "fossil settings autosync off")) return null; if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "user", "capabilities", "nobody", "gjorzwik", "-R", dst_fossil }, base, env, tag, citation, "fossil user capabilities nobody")) return null; // zit grows a check-in the fossil repo lacks. const grow_path = try std.fs.path.join(arena, &.{ zsrc, "b.txt" }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = grow_path, .data = push_grow_payload }); inline for (.{ .{ &[_][]const u8{ "add", "b.txt" }, "zit add (grow)" }, .{ &[_][]const u8{ "commit", "-m", "grow", "--user", "u" }, "zit commit (grow)" }, }) |step| { const r = (try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, step[0], tag, citation, step[1])) orelse return null; gpa.free(r.stdout); gpa.free(r.stderr); } // fossil serves the shared repo; zit PUSHES its growth up over the wire. const push_port = try freshLoopbackPort(io); const push_port_str = try std.fmt.allocPrint(arena, "{d}", .{push_port}); { var server = spawnFossilServe(gpa, io, out, dst_fossil, base, env, push_port_str) catch |err| { try out.print("FAIL {s} ({s}): could not spawn fossil server: {s}\n", .{ tag, citation, @errorName(err) }); return null; }; waitForListen(io, push_port) catch { stopFossilServe(io, &server); try out.print("FAIL {s} ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ tag, citation, push_port }); return null; }; const push_url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{push_port}); const push_res = try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, &.{ "push", push_url }, tag, citation, "zit push to the fossil server"); stopFossilServe(io, &server); const r = push_res orelse return null; gpa.free(r.stdout); gpa.free(r.stderr); } return .{ .base = base, .zsrc = zsrc, .dst_fossil = dst_fossil, .abs_exe = abs_exe }; } /// S7 -- zit push -> fossil server. After the shared prelude has pushed zit's /// grown check-in up to the fossil repo, a fresh fossil checkout of that repo /// must reconstruct the pushed file byte-for-byte: fossil hash-verifies every /// artifact it stores and reconstructs, so a checkout that yields our exact /// bytes proves the push delivered the artifacts intact over the wire. fn runPushToFossilServerScenario( arena: std.mem.Allocator, gpa: std.mem.Allocator, io: std.Io, tmp_root: []const u8, env: *const std.process.Environ.Map, out: *std.Io.Writer, ) !bool { const citation = "§3.3/§3.7"; const setup = (try establishPushedFossil(arena, gpa, io, tmp_root, env, out, "S7", citation, "S7")) orelse return false; // THE PROOF: open the pushed-to repo and read back zit's grown file. fossil // materializes it from the artifacts the push delivered; a push that dropped // or corrupted a byte fails here. const co = try std.fs.path.join(arena, &.{ setup.base, "verify-co" }); try std.Io.Dir.cwd().createDirPath(io, co); if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "open", "--nosync", setup.dst_fossil }, co, env, "S7", citation, "fossil open of the pushed repo")) return false; const co_blob = try std.fs.path.join(arena, &.{ co, "b.txt" }); const fossil_bytes = std.Io.Dir.cwd().readFileAlloc(io, co_blob, gpa, .limited(1 << 20)) catch |err| { try out.print("FAIL S7 ({s}): the pushed repo has no readable b.txt after checkout: {s}\n", .{ citation, @errorName(err) }); try printMismatchFooter(out, citation); return false; }; defer gpa.free(fossil_bytes); if (!std.mem.eql(u8, push_grow_payload, fossil_bytes)) { try out.print("FAIL S7 ({s}): the file fossil reconstructed after zit's push does not match zit's committed bytes\n", .{citation}); try printBytesPreview(out, "zit pushed", push_grow_payload); try printBytesPreview(out, "fossil rebuilt", fossil_bytes); try printDiffWindow(out, push_grow_payload, fossil_bytes); try printMismatchFooter(out, citation); return false; } try out.print("PASS S7 ({s}): fossil reconstructs zit's pushed check-in byte-for-byte from the upload\n", .{citation}); return true; } /// S8 -- fossil commit -> zit pull. On the repo zit pushed to in the prelude, a /// fossil checkout commits one more file (our bytes, carried inside a /// fossil-authored check-in). zit then PULLS that extension. Convergence is /// asserted through the library reader: `zit.Repository` opens the pulled store /// and returns the carried blob under its own artifact name, byte-identical to /// what we authored -- so the pull ingested and reconstructed a check-in it did /// not itself write. fn runPullFossilExtensionScenario( arena: std.mem.Allocator, gpa: std.mem.Allocator, io: std.Io, tmp_root: []const u8, env: *const std.process.Environ.Map, out: *std.Io.Writer, ) !bool { const citation = "§3.2/§3.7"; const setup = (try establishPushedFossil(arena, gpa, io, tmp_root, env, out, "S8", citation, "S8")) orelse return false; // A fossil checkout extends the shared repo with one more commit wrapping // OUR bytes. autosync is already off on the repo, so the commit cannot stall // reaching the dead seed remote; --nosync is belt-and-suspenders. const fco = try std.fs.path.join(arena, &.{ setup.base, "fossil-co" }); try std.Io.Dir.cwd().createDirPath(io, fco); if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "open", "--nosync", setup.dst_fossil }, fco, env, "S8", citation, "fossil open of the shared repo")) return false; if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "update", "trunk" }, fco, env, "S8", citation, "fossil update trunk")) return false; const ext_path = try std.fs.path.join(arena, &.{ fco, "c.txt" }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = ext_path, .data = fossil_ext_payload }); if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "add", "c.txt" }, fco, env, "S8", citation, "fossil add")) return false; if (!try fossilStepTagged( gpa, io, out, &.{ "fossil", "commit", "-m", "fossil extends", "--user-override", "u", "--no-warnings", "--nosync" }, fco, env, "S8", citation, "fossil commit", )) return false; // fossil serves the extended repo; zit PULLS the check-in it lacks. const pull_port = try freshLoopbackPort(io); const pull_port_str = try std.fmt.allocPrint(arena, "{d}", .{pull_port}); { var server = spawnFossilServe(gpa, io, out, setup.dst_fossil, setup.base, env, pull_port_str) catch |err| { try out.print("FAIL S8 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) }); return false; }; waitForListen(io, pull_port) catch { stopFossilServe(io, &server); try out.print("FAIL S8 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, pull_port }); return false; }; const pull_url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{pull_port}); const pull_res = try zitStepTagged(gpa, io, out, setup.abs_exe, setup.zsrc, env, &.{ "pull", pull_url }, "S8", citation, "zit pull from the fossil server"); stopFossilServe(io, &server); const r = pull_res orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // THE PROOF, through the library reader: the pulled store holds the // fossil-carried blob under its own artifact name (sha3-256 over our bytes), // and `Repository.blob` re-verifies content against that name (§7.2) before // returning it. A pull that fetched nothing, or reconstructed the blob // wrong, fails here. var root = std.Io.Dir.cwd().openDir(io, setup.zsrc, .{}) catch |err| { try out.print("FAIL S8 ({s}): could not open the pulled zit checkout root: {s}\n", .{ citation, @errorName(err) }); return false; }; defer root.close(io); var repo: zit.Repository = undefined; repo.open(gpa, io, root, .{}) catch |err| { try out.print("FAIL S8 ({s}): opening the pulled zit repo via the library failed: {s}\n", .{ citation, @errorName(err) }); try printMismatchFooter(out, citation); return false; }; defer repo.deinit(gpa); var name_buf: [64]u8 = undefined; const ext_name = artifact.artifactName(.sha3_256, fossil_ext_payload, &name_buf); const got = (repo.blob(gpa, ext_name) catch |err| { try out.print("FAIL S8 ({s}): reading the pulled blob {s} via the library failed: {s}\n", .{ citation, ext_name, @errorName(err) }); try printMismatchFooter(out, citation); return false; }) orelse { try out.print("FAIL S8 ({s}): the pull did not converge — blob {s} absent from zit's store after pull\n", .{ citation, ext_name }); try printMismatchFooter(out, citation); return false; }; defer gpa.free(got); if (!std.mem.eql(u8, fossil_ext_payload, got)) { try out.print("FAIL S8 ({s}): the pulled blob does not match the bytes committed through fossil\n", .{citation}); try printBytesPreview(out, "committed", fossil_ext_payload); try printBytesPreview(out, "zit pulled", got); try printDiffWindow(out, fossil_ext_payload, got); try printMismatchFooter(out, citation); return false; } try out.print("PASS S8 ({s}): zit pulls a fossil-authored check-in and the library reader confirms convergence\n", .{citation}); return true; } // --------------------------------------------------------------------------- // S9 -- a real fossil CLONE from `zit serve` of a DEFER-SHUNNED store: the // shunned artifact must be ABSENT, not CORRUPT (sync §9.5 L1988-1992, §8.2 // L1504-1506). // // §9.5 L1989-1991 — "A subsequent read-authorized transfer path requests igot // for every eligible holding after the Section 3.3 shun, phantom, private, and // peer-has filters" — is a statement about what the server sends. This scenario // is the only place a REAL peer judges the result. // // `zit shun --force --defer` records the row WITHOUT rewriting the generation, so // two things are simultaneously true in the served store: the shunned bytes are // still on disk, and a SURVIVING frame in the same generation is a delta rooted on // the shunned one. Both halves of the send-side filter are therefore under test: // // * the shunned artifact must not be announced or sent — fossil must end up // holding it as a PHANTOM, which is fossil's own word for "absent"; // * the surviving delta must arrive in FULL form. Handing out its delta wire // form would ship a DELTASRC this server never sends; fossil would store a // permanently dangling delta and gimme a basis it is silently skipped on // (§3.3 L532-535) — CORRUPT, the outcome this scenario rules out. // // The oracle is `fossil test-integrity`, read for two things fossil says in its // own words: the shunned name is skipped as a PHANTOM, and the blobs it did check // report ZERO errors. "Absent" and "corrupt" are different outcomes and only one // is acceptable. Every byte fossil sees is zit's own output; fossil only confirms. // --------------------------------------------------------------------------- /// The 64-hex artifact name `zit ls` prints for `want` (its lines are /// `HASH SP PATH`), or null when the path is not listed. fn lsHashOf(listing: []const u8, want: []const u8) ?[]const u8 { var it = std.mem.splitScalar(u8, listing, '\n'); while (it.next()) |line| { const sp = std.mem.indexOfScalar(u8, line, ' ') orelse continue; if (!std.mem.eql(u8, std.mem.trim(u8, line[sp + 1 ..], " \t\r"), want)) continue; return line[0..sp]; } return null; } /// The count parsed from `zit stat`'s `deltas N` line, or null if absent. Guards /// S9's premise: with no delta frame in the store, the surviving-delta half of the /// scenario is not being exercised at all. fn statDeltaCount(text: []const u8) ?usize { var it = std.mem.splitScalar(u8, text, '\n'); while (it.next()) |line| { if (!std.mem.startsWith(u8, line, "deltas ")) continue; return std.fmt.parseInt(usize, std.mem.trim(u8, line["deltas ".len..], " \t\r"), 10) catch null; } return null; } /// Does `text` carry a `skip phantom` line naming `hash`? That is fossil's own /// report that it holds the name but no content — ABSENT. fn reportsPhantom(text: []const u8, hash: []const u8) bool { var it = std.mem.splitScalar(u8, text, '\n'); while (it.next()) |line| { if (std.mem.indexOf(u8, line, "skip phantom") == null) continue; if (std.mem.indexOf(u8, line, hash) != null) return true; } return false; } /// Does the clone hold `hash` in fossil's OWN shun table? The stronger form of /// absence, and the one zit now produces (#73/#83). /// /// Before the `/shun` configuration group was carried, a reference to a shunned /// artifact left fossil holding a PHANTOM -- a name with no content, which /// fossil treats as something to FETCH. That is the takedown-defeating state: /// the clone would gimme those bytes from the next peer that had them. Now the /// shun row travels with the clone, so fossil records the artifact as /// SUPPRESSED and never asks anyone for it. /// /// Both are "absent"; only one stays absent. Asked of the database rather than /// of `test-integrity`'s prose because a row that exists is a fact, while the /// absence of a phantom line is only the absence of evidence. fn holdsShunRow(gpa: std.mem.Allocator, io: std.Io, repo_dir: []const u8, hash: []const u8, env: anytype) !bool { const sql = try std.fmt.allocPrint(gpa, "select count(*) from shun where uuid='{s}';", .{hash}); defer gpa.free(sql); const res = try runFossil(gpa, io, &.{ "fossil", "sqlite3", "--readonly", sql }, repo_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) return false; return std.mem.indexOfScalar(u8, std.mem.trim(u8, res.stdout, " \t\r\n"), '0') == null; } /// Does `text` report ZERO integrity errors? Anchored on the integer immediately /// before the word `errors`, because the substring "0 errors" is also inside /// "10 errors", "20 errors" and "100 errors" — the exact outputs that mean CORRUPT, /// which is the outcome this scenario exists to tell apart from ABSENT. True only /// when at least one `N errors` count was found and every one of them read zero. fn reportsZeroErrors(text: []const u8) bool { const word = "errors"; var found = false; var i: usize = 0; while (std.mem.indexOfPos(u8, text, i, word)) |p| { i = p + word.len; // Whole word only: `errorsomething` is not a count. if (i < text.len and (std.ascii.isAlphanumeric(text[i]) or text[i] == '_')) continue; var end = p; while (end > 0 and (text[end - 1] == ' ' or text[end - 1] == '\t')) end -= 1; var start = end; while (start > 0 and std.ascii.isDigit(text[start - 1])) start -= 1; if (start == end) continue; // no count in front of this occurrence // The digits must be their own token, not the tail of an identifier. if (start > 0 and (std.ascii.isAlphanumeric(text[start - 1]) or text[start - 1] == '_')) continue; const n = std.fmt.parseInt(u64, text[start..end], 10) catch continue; if (n != 0) return false; found = true; } return found; } test "smoke S9 oracle: a nonzero integrity count is never read as clean" { try std.testing.expect(reportsZeroErrors("low-level database integrity ok\n0 errors\n")); try std.testing.expect(reportsZeroErrors("skip phantom abc\n0 errors, 3 phantoms\n")); // The defect this helper exists to prevent: "0 errors" is a substring of these. try std.testing.expect(!reportsZeroErrors("10 errors\n")); try std.testing.expect(!reportsZeroErrors("100 errors detected\n")); try std.testing.expect(!reportsZeroErrors("0 errors\n20 errors\n")); // No count at all is not a pass either. try std.testing.expect(!reportsZeroErrors("errors\n")); try std.testing.expect(!reportsZeroErrors("integrity check complete\n")); } fn runCloneOfShunnedScenario( arena: std.mem.Allocator, gpa: std.mem.Allocator, io: std.Io, tmp_root: []const u8, env: *const std.process.Environ.Map, out: *std.Io.Writer, ) !bool { const citation = "§9.5/§8.2"; const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, "S9" }); const src_dir = try std.fs.path.join(arena, &.{ base, "src" }); try std.Io.Dir.cwd().createDirPath(io, src_dir); { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init", "S9", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // Two files: one to be shunned, one to prove the clone still converged on // something. The shunned file's FIRST version is the delta basis of its // second, which is what puts the surviving-delta half under test. const blob_path = try std.fs.path.join(arena, &.{ src_dir, "big.txt" }); const keep_path = try std.fs.path.join(arena, &.{ src_dir, "keep.txt" }); const keep_bytes = "a file with nothing against it\n"; const seed = try buildBlob(arena, false); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = seed }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = keep_path, .data = keep_bytes }); inline for (.{ .{ &[_][]const u8{ "add", "big.txt", "keep.txt" }, "zit add (seed)" }, .{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit (seed)" }, }) |st| { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S9", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // The seed blob's complete artifact name, read off the tip while the seed IS // the tip — no abbreviation to resolve and no timeline parsing. const doomed = blk: { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"ls"}, "zit ls (seed)", "S9", citation)) orelse return false; defer gpa.free(r.stdout); defer gpa.free(r.stderr); const h = lsHashOf(r.stdout, "big.txt") orelse { try out.print("FAIL S9 ({s}): 'zit ls' did not list big.txt\n stdout: {s}\n", .{ citation, r.stdout }); return false; }; break :blk try arena.dupe(u8, h); }; const edited = try buildBlob(arena, true); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = edited }); inline for (.{ .{ &[_][]const u8{ "add", "big.txt" }, "zit add (grow)" }, .{ &[_][]const u8{ "commit", "-m", "grow", "--user", "u" }, "zit commit (grow)" }, }) |st| { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S9", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // Record the shun WITHOUT reclaiming: the bytes stay, so only the send-side // filter can keep them off the wire. { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "shun", "--force", "--defer", doomed }, "zit shun --force --defer", "S9", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // The fixture's premise, stated rather than assumed: the store still carries // a delta frame. With none, the surviving-delta half proves nothing. { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"stat"}, "zit stat", "S9", citation)) orelse return false; defer gpa.free(r.stdout); defer gpa.free(r.stderr); const deltas = statDeltaCount(r.stdout) orelse { try out.print("FAIL S9 ({s}): could not parse 'zit stat' delta count\n stdout: {s}\n", .{ citation, r.stdout }); return false; }; if (deltas == 0) { try out.print("FAIL S9 ({s}): the store holds no delta frame — the surviving-delta half of this scenario is not being exercised\n stdout: {s}\n", .{ citation, r.stdout }); return false; } } const port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{port}); var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str); defer server.kill(io); waitForListen(io, port) catch { try out.print("FAIL S9 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port }); return false; }; const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port}); const clone_repo = try std.fs.path.join(arena, &.{ base, "verify.fossil" }); { const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); // A CORRUPT outcome shows up here first: a dangling DELTASRC aborts the // clone rather than completing it minus one artifact. if (!okExit(res.term)) { try out.print("FAIL S9 ({s}): fossil clone of a defer-shunned zit store failed — the shunned artifact must be ABSENT, not a broken clone\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } } const co_dir = try std.fs.path.join(arena, &.{ base, "co" }); try std.Io.Dir.cwd().createDirPath(io, co_dir); { const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S9 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr }); try printMismatchFooter(out, citation); return false; } } // The surviving delta arrived in a form fossil could reconstruct: the tip's // file is zit's own committed bytes, byte-for-byte. { const co_blob = try std.fs.path.join(arena, &.{ co_dir, "big.txt" }); const fossil_bytes = try std.Io.Dir.cwd().readFileAlloc(io, co_blob, gpa, .limited(1 << 20)); defer gpa.free(fossil_bytes); if (!std.mem.eql(u8, edited, fossil_bytes)) { try out.print("FAIL S9 ({s}): the clone did not reconstruct zit's committed tip bytes — a delta whose basis was shunned did not arrive in full form\n", .{citation}); try printBytesPreview(out, "zit committed", edited); try printBytesPreview(out, "fossil clone", fossil_bytes); try printDiffWindow(out, edited, fossil_bytes); try printMismatchFooter(out, citation); return false; } } // THE PROOF, in fossil's own words: the shunned artifact is ABSENT, and every // blob fossil DID receive checks clean. // // Absence now has a stronger shape than it used to. Before zit carried the // `/shun` configuration group (#73/#83), fossil ended up holding the name as // a PHANTOM — absent, but absent in the way that makes fossil go looking for // it, so the very next sync with any third party would undo the takedown. // Now the shun row arrives with the clone and fossil records the artifact as // suppressed instead, which is why the phantom is gone. The shun row is // REQUIRED rather than merely accepted: the phantom is the state the // carriage exists to eliminate, so tolerating it would leave this scenario // unable to notice the carriage silently regressing — which is the one // regression it is now best placed to catch. { const res = try runFossil(gpa, io, &.{ "fossil", "test-integrity" }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S9 ({s}): fossil test-integrity failed on the clone\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } if (!(holdsShunRow(gpa, io, co_dir, doomed, env) catch false)) { // The phantom probe survives as a DIAGNOSTIC, not as an accepted // outcome: it separates "the carriage regressed" from "the artifact // was served", which are different bugs with different fixes. const phantom_there = reportsPhantom(res.stdout, doomed) or reportsPhantom(res.stderr, doomed); if (phantom_there) { try out.print("FAIL S9 ({s}): the clone holds the artifact as a PHANTOM rather than a shun row — the /shun configuration group is no longer being carried, so the next sync with any peer would undo the takedown\n", .{citation}); } else { try out.print("FAIL S9 ({s}): the clone neither shuns nor phantoms the artifact — it received content zit must never have served\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); } try printMismatchFooter(out, citation); return false; } const clean = reportsZeroErrors(res.stdout) or reportsZeroErrors(res.stderr); if (!clean) { try out.print("FAIL S9 ({s}): fossil test-integrity did not report zero errors — the clone is CORRUPT, not merely missing the shunned artifact\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } } try out.print("PASS S9 ({s}): a fossil clone of a defer-shunned zit store completes, reconstructs the tip byte-for-byte, and records the shunned artifact in its OWN shun table with zero integrity errors\n", .{citation}); return true; } // --------------------------------------------------------------------------- // S10 -- a real fossil PUSH into `zit serve` against a shunned store (sync // §7.7 L1437-1440, §3.3 L527-530). // // S9 is the SEND direction of a shun against a real peer (fossil clones a // defer-shunned zit store and the artifact is absent). This is the direction // the harness had never driven at all: fossil as the sync CLIENT pushing INTO // `zit serve`. S7 pushes the other way (zit client -> fossil server), so this // closes the last untested corner of the four (client, server) x (zit, fossil) // combinations, and it is the one an operator's shun depends on. // // WHAT THIS PINS, EXACTLY. A real fossil `push --verily` against a repository // that has shunned and reclaimed one of its artifacts completes with no // protocol error and lands nothing. That is the interop claim, and it holds. // // WHAT THIS DOES NOT PIN, STATED PLAINLY. It is NOT coverage of §7.7's // receive-side discard: the scenario passes unchanged with the server's // admission check deleted outright (checked with a real mutant, not assumed). // The reason is structural rather than a harness limitation. Content transfer // in this protocol is RECEIVER-DRIVEN: §3.3 L527-530 already excludes a shunned // name from the receiver's phantom scan, so `zit serve` never emits a gimme for // it, and fossil -- whose push offers `igot` and then uploads what the peer asks // for, plus its own `unsent` artifacts -- has no reason to put the card on the // wire. Reaching the §7.7 arm needs a peer that pushes a shunned target // UNSOLICITED, which a conformant peer against this server never does. // // So §7.7 is defense in depth here, and its mutation-sensitive coverage lives // where an unsolicited card can actually be constructed: the `src/sync_test.zig` // §7.7 unit tests and the `src/e2e.zig` raw-POST scenario, both of which go red // under the same mutant this one survives. This scenario is kept for the interop // property in its PASS line and for the direction it opens, not as that proof -- // do not read its green as evidence the admission check is exercised. // // The store's `.zit` SIZE across the push is the oracle for "makes no repository // mutation": `serveRound` persists only when the round wrote something // (`overlay.writtenCount() > 0`), so a round that lands nothing appends nothing. // Size is the one measure the shun mask cannot fool -- `zit cat` refuses the // name whether or not the bytes came back, so it is no oracle here. // --------------------------------------------------------------------------- /// Byte length of the file at `path` (cwd-relative or absolute). fn fileSizeAt(io: std.Io, path: []const u8) !u64 { var f = try std.Io.Dir.cwd().openFile(io, path, .{}); defer f.close(io); const st = try f.stat(io); return st.size; } fn runPushOfShunnedScenario( arena: std.mem.Allocator, gpa: std.mem.Allocator, io: std.Io, tmp_root: []const u8, env: *const std.process.Environ.Map, out: *std.Io.Writer, ) !bool { const citation = "§7.7"; const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, "S10" }); const src_dir = try std.fs.path.join(arena, &.{ base, "src" }); try std.Io.Dir.cwd().createDirPath(io, src_dir); { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init", "S10", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // Two files: one the operator will remove, one that must be unaffected. const doomed_bytes = "content an operator ordered removed, which fossil will offer back\n"; const keep_bytes = "a file with nothing against it\n"; try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = try std.fs.path.join(arena, &.{ src_dir, "doomed.txt" }), .data = doomed_bytes, }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = try std.fs.path.join(arena, &.{ src_dir, "keep.txt" }), .data = keep_bytes, }); inline for (.{ .{ &[_][]const u8{ "add", "doomed.txt", "keep.txt" }, "zit add" }, .{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit" }, }) |st| { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S10", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } const doomed = blk: { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"ls"}, "zit ls", "S10", citation)) orelse return false; defer gpa.free(r.stdout); defer gpa.free(r.stderr); const h = lsHashOf(r.stdout, "doomed.txt") orelse { try out.print("FAIL S10 ({s}): 'zit ls' did not list doomed.txt\n stdout: {s}\n", .{ citation, r.stdout }); return false; }; break :blk try arena.dupe(u8, h); }; // A real fossil repo clones the whole thing, so it holds the doomed artifact // and shares zit's project code (without which the exchange is refused). const clone_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" }); const co_dir = try std.fs.path.join(arena, &.{ base, "co" }); try std.Io.Dir.cwd().createDirPath(io, co_dir); { const port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{port}); var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str); defer server.kill(io); waitForListen(io, port) catch { try out.print("FAIL S10 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port }); return false; }; const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port}); const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S10 ({s}): fossil clone of the zit store failed\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } } { const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S10 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr }); try printMismatchFooter(out, citation); return false; } } // The operator removes it. `--force` reclaims, so the bytes are gone and the // ONLY thing that can keep them out on the way back is §7.7 admission. { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "shun", "--force", doomed }, "zit shun --force", "S10", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } const store_path = try std.fs.path.join(arena, &.{ src_dir, ".zit" }); const size_before = try fileSizeAt(io, store_path); // fossil pushes back into `zit serve`. fossil uploads what its own inventory // says the peer lacks, so the shunned artifact goes on the wire unsolicited. { const port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{port}); var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str); defer server.kill(io); waitForListen(io, port) catch { try out.print("FAIL S10 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port }); return false; }; const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port}); // `--verily` is fossil's own "exchange extra information ... to ensure no // content is overlooked" mode: it re-offers what its sent-flags would // otherwise suppress, which is what puts a file card for the shunned // artifact back on the wire. A plain `fossil push` after a clone has // nothing marked unsent and sends no content at all. const res = try runFossil(gpa, io, &.{ "fossil", "push", "--verily", url }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); // "it emits no error" — fossil reports a protocol error card as a failed // push, so a clean exit is the interoperable reading of that clause. if (!okExit(res.term)) { try out.print("FAIL S10 ({s}): fossil push into zit serve failed — a discarded shunned target must raise NO protocol error\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } server.kill(io); } // THE PROOF: "makes no repository mutation". The store did not grow, so the // artifact the operator removed did not come back through the serve door. const size_after = try fileSizeAt(io, store_path); if (size_after != size_before) { try out.print("FAIL S10 ({s}): the served store GREW across a fossil push of shunned content ({d} -> {d} bytes) — the receiver re-admitted what the operator removed\n", .{ citation, size_before, size_after }); try printMismatchFooter(out, citation); return false; } try out.print("PASS S10 ({s}): a real fossil push of a shunned artifact into zit serve completes with no protocol error and lands nothing\n", .{citation}); return true; }