Skip to content
zit Artifact [36d95554c9]
Login

Artifact [36d95554c9]

Artifact 36d95554c99e060c811340a92072052c9ccc994f19ea7f6c0a04071718ed674c:


//! End-to-end tests for the `zit` CLI: they run the REAL compiled binary via
//! `std.process.Child`, with a pinned, isolated environment (TZ=UTC, an
//! isolated HOME, cwd bound to a fresh `std.testing.tmpDir`), assert the exit
//! code and the exact/structural stdout+stderr, then verify the resulting
//! `.zit`/`.zit-checkout` state READ BACK THROUGH THE LIBRARY. Teardown is
//! unconditional (`defer tmp.cleanup()`).
//!
//! Timestamps and hashes are never asserted against a wall clock: the success
//! line is checked structurally (its trailing token is an absolute `.zit`
//! path), and repository state is decoded with the library's own reader.
//!
//! Built by `zig build e2e`, which compiles `zit` first and passes its path in
//! via `build_options.zit_exe`. Behavior ids cite contracts/init.md.

const std = @import("std");
const build_options = @import("build_options");

// Private storage modules share the build graph's single `zit` module instance.
const logstore = @import("zit").logstore;
const storage = @import("zit").storage;
/// draft-fossil-submanifests-00 §6.3/§8.1 — the settings semantics the CLI
/// tests below assert against the stored config frame.
const submanifest = @import("zit").submanifest;
const checkout = @import("zit").checkout;
// The managed-path set is read back through the library to verify `add`'s
// on-disk effect (contracts/add.md); re-exported by the `zit` module.
const managed = @import("zit").managed;
/// The fully populated `.fossil-settings/` fixture this lane
/// and the fossil-interop smoke lane (S14) both build.
const settings_fixture = @import("zit").settings_fixture;
// The artifact reader used to decode a minted check-in back through the
// library — the `commit` e2e's state oracle is its own decoded manifest
// fields, never raw bytes compared to a hand-written expectation.
const artifact = @import("zit").artifact;

test {
    _ = @import("git/git_import_e2e.zig");
}
// The §4.1 container codec and the sync vocabulary: the §9.3 configuration
// tests below hand-build a request card no zit client emits, so they frame it
// with the same codec the transport uses and name the settings with the same
// constants the wire does.
const container = @import("zit").container;
const sync = @import("zit").sync;
// The dispatched-verb enum, imported straight from the source it lives in (as
// main.zig does) so the help-audit guard below drives off the SAME list the
// dispatcher switches on — a verb added there without a help line fails the
// test, never drifts.
const zit = @import("zit");
const cli = @import("zit").cli;
const File = std.Io.File;

/// Absolute path to the freshly built `zit` executable (a build-time
/// `LazyPath`, so the exe is always rebuilt before these tests run).
const zit_exe: []const u8 = build_options.zit_exe;

const success_prefix = "Initialized empty zit repository in ";

// --- helpers --------------------------------------------------------------

/// Spawns `zit init <tail...>` with a pinned environment and cwd bound to
/// `tmp_dir`. Caller owns `result.stdout` / `result.stderr`.
fn spawnInit(
    gpa: std.mem.Allocator,
    io: std.Io,
    tmp_dir: std.Io.Dir,
    tail: []const []const u8,
) !std.process.RunResult {
    var env = std.process.Environ.Map.init(gpa);
    defer env.deinit();
    try env.put("TZ", "UTC");
    try env.put("HOME", "/zit-e2e-isolated-home-does-not-exist");
    try env.put("PATH", "/usr/bin:/bin");

    // `zit_exe` is relative to the build root (the test's cwd); the child
    // chdir's into `tmp_dir` before exec, so resolve it to an absolute path
    // first. `resolve` returns it unchanged if it is already absolute.
    const cwd_abs = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd_abs);
    const abs_exe = try std.fs.path.resolve(gpa, &.{ cwd_abs, zit_exe });
    defer gpa.free(abs_exe);

    var argv_buf: [8][]const u8 = undefined;
    argv_buf[0] = abs_exe;
    argv_buf[1] = "init";
    std.debug.assert(tail.len <= argv_buf.len - 2);
    for (tail, argv_buf[2..][0..tail.len]) |src, *dst| dst.* = src;
    const argv = argv_buf[0 .. 2 + tail.len];

    return std.process.run(gpa, io, .{
        .argv = argv,
        .cwd = .{ .dir = tmp_dir },
        .environ_map = &env,
    });
}

fn freeResult(gpa: std.mem.Allocator, res: *std.process.RunResult) void {
    gpa.free(res.stdout);
    gpa.free(res.stderr);
}

fn expectNonzeroExit(term: std.process.Child.Term) !void {
    switch (term) {
        .exited => |c| try std.testing.expect(c != 0),
        else => {
            std.debug.print("expected nonzero exit, got {any}\n", .{term});
            return error.TestUnexpectedTerm;
        },
    }
}

fn expectExit(term: std.process.Child.Term, code: u8) !void {
    switch (term) {
        .exited => |c| try std.testing.expectEqual(code, c),
        else => {
            std.debug.print("expected exit {d}, got {any}\n", .{ code, term });
            return error.TestUnexpectedTerm;
        },
    }
}

/// Like `expectExit`, but on mismatch prints the child's captured stdout and
/// stderr before failing. Reserved for the spawned-CLIENT assertions in the
/// server-interop scenarios (clone/pull/push against a live `zit serve`): there
/// the child's stderr is the ONLY evidence of *why* it exited non-zero — a push
/// that reached the wrong server, a sync that never converged — and plain
/// `expectExit` discards it, leaving a failure that is countable but not
/// diagnosable. Elsewhere `expectExit` stays: those call sites
/// assert the child's output separately, so the code alone is enough.
fn expectExitDiag(term: std.process.Child.Term, code: u8, res: std.process.RunResult) !void {
    switch (term) {
        .exited => |c| {
            if (c == code) return;
            std.debug.print(
                "expected exit {d}, got {d}\n--- child stdout ---\n{s}\n--- child stderr ---\n{s}\n",
                .{ code, c, res.stdout, res.stderr },
            );
            return error.TestUnexpectedExitCode;
        },
        else => {
            std.debug.print(
                "expected exit {d}, got {any}\n--- child stdout ---\n{s}\n--- child stderr ---\n{s}\n",
                .{ code, term, res.stdout, res.stderr },
            );
            return error.TestUnexpectedTerm;
        },
    }
}

/// Asserts, through the library, that `dir` holds a freshly-initialized repo:
/// a `.zit` carrying the header plus a single config frame (its §9.1 project
/// code and defaults) and no content artifacts, and a `.zit-checkout` whose
/// trunk is unborn (`version == ""`, `branch == "trunk"`). The project code is
/// a real 40-hex value, never the all-zero placeholder. Returns the `.zit`
/// bytes (caller frees) so a same-directory caller can prove byte-identity.
fn expectFreshRepo(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]u8 {
    const bytes = try dir.readFileAlloc(io, ".zit", gpa, .limited(1 << 20));
    errdefer gpa.free(bytes);
    try std.testing.expect(bytes.len > logstore.header_len); // header + config frame

    const digests = try collectArtifactDigests(gpa, io, dir);
    defer freeDigests(gpa, digests);
    try std.testing.expectEqual(@as(usize, 0), digests.len); // no content yet

    const code = try readProjectCode(gpa, io, dir);
    defer gpa.free(code);
    try std.testing.expectEqual(@as(usize, 40), code.len); // §9.1 artifact-hash width
    try std.testing.expect(!std.mem.eql(u8, code, "0" ** 40)); // a real, non-placeholder code

    var ckbuf: [checkout.max_ck_payload + 1]u8 = undefined;
    const ck = (try checkout.read(io, dir, &ckbuf)).?;
    try std.testing.expectEqualStrings("", ck.version);
    try std.testing.expectEqualStrings("trunk", ck.branch);
    return bytes;
}

/// `update` narrates its receipt on stderr; a diagnostic is a line
/// that starts with `zit ` (every refusal and error does). This asserts none.
fn expectNoDiagnostic(stderr: []const u8) !void {
    var it = std.mem.splitScalar(u8, stderr, '\n');
    while (it.next()) |line| {
        if (std.mem.startsWith(u8, line, "zit ")) {
            std.debug.print("unexpected diagnostic: {s}\n", .{line});
            return error.TestUnexpectedResult;
        }
    }
}

fn fileExists(io: std.Io, dir: std.Io.Dir, name: []const u8) bool {
    var f = dir.openFile(io, name, .{}) catch return false;
    f.close(io);
    return true;
}

// --- add helpers ----------------------------------------------------------

/// Spawns `zit <tail...>` with a pinned environment and cwd bound to `tmp_dir`.
/// The generic sibling of `spawnInit` — any verb. Caller owns `result.stdout` /
/// `result.stderr`.
/// `runZit` with extra environment pairs. The global settings tier is located
/// through the environment, so a test that exercises it must be able to point
/// `$ZIT_CONFIG` at its own tmp directory -- the operator's real settings file
/// must never be read or written by the suite.
fn runZitEnv(
    gpa: std.mem.Allocator,
    io: std.Io,
    tmp_dir: std.Io.Dir,
    extra: []const [2][]const u8,
    tail: []const []const u8,
) !std.process.RunResult {
    var env = std.process.Environ.Map.init(gpa);
    defer env.deinit();
    try env.put("TZ", "UTC");
    try env.put("HOME", "/zit-e2e-isolated-home-does-not-exist");
    try env.put("PATH", "/usr/bin:/bin");
    for (extra) |pair| try env.put(pair[0], pair[1]);

    const cwd_abs = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd_abs);
    const abs_exe = try std.fs.path.resolve(gpa, &.{ cwd_abs, zit_exe });
    defer gpa.free(abs_exe);

    var argv_buf: [16][]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 = .{ .dir = tmp_dir },
        .environ_map = &env,
    });
}

fn runZit(
    gpa: std.mem.Allocator,
    io: std.Io,
    tmp_dir: std.Io.Dir,
    tail: []const []const u8,
) !std.process.RunResult {
    var env = std.process.Environ.Map.init(gpa);
    defer env.deinit();
    try env.put("TZ", "UTC");
    try env.put("HOME", "/zit-e2e-isolated-home-does-not-exist");
    try env.put("PATH", "/usr/bin:/bin");

    const cwd_abs = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd_abs);
    const abs_exe = try std.fs.path.resolve(gpa, &.{ cwd_abs, zit_exe });
    defer gpa.free(abs_exe);

    var argv_buf: [32][]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;
    const argv = argv_buf[0 .. 1 + tail.len];

    return std.process.run(gpa, io, .{
        .argv = argv,
        .cwd = .{ .dir = tmp_dir },
        .environ_map = &env,
    });
}

/// Materializes a fresh repository in `dir` by running the real `zit init`
/// (exit 0 asserted) — the same path a user takes before `add`.
fn initRepo(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !void {
    var res = try runZit(gpa, io, dir, &.{"init"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
}

/// Creates an empty regular file `name` under `dir`.
fn touch(io: std.Io, dir: std.Io.Dir, name: []const u8) !void {
    var f = try dir.createFile(io, name, .{});
    f.close(io);
}

/// Whether the `.zit-checkout` container carries the tagged section (legacy
/// sibling files answer for a pre-container checkout). The presence oracle for
/// the absorbed records — the sibling FILES no longer exist on a live checkout.
fn sectionExists(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, tag: checkout.SectionTag) bool {
    const raw = checkout.readSectionAlloc(gpa, io, dir, tag) catch return false;
    if (raw) |bytes| {
        gpa.free(bytes);
        return true;
    }
    return false;
}

/// The managed section's raw byte image ("" when absent) — the byte-identity
/// oracle the pre-container e2es got from reading `.zit-managed` whole.
fn readManagedImage(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]u8 {
    return (try checkout.readSectionAlloc(gpa, io, dir, .managed)) orelse try gpa.dupe(u8, "");
}

/// Plants an unparseable payload in the tagged section — the container-era
/// equivalent of writing garbage over the old sibling file. The container
/// itself stays valid; the section's own parser rejects the bytes.
fn corruptSection(io: std.Io, dir: std.Io.Dir, tag: checkout.SectionTag) !void {
    try checkout.updateSections(io, dir, &.{.{ .tag = tag, .payload = "not a valid record" }});
}

/// Reads `.zit-managed` back through the library and asserts its membership is
/// exactly `expected` (order-independent — the set is sorted internally). This
/// readback is the on-disk oracle for every `add` e2e.
fn expectManaged(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    expected: []const []const u8,
) !void {
    var set = try managed.ManagedSet.read(gpa, io, dir);
    defer set.deinit();
    try std.testing.expectEqual(expected.len, set.iterate().len);
    for (expected) |key| try std.testing.expect(set.contains(key));
}

/// Marks `paths` managed by running the real `zit add` (exit 0 asserted) — the
/// same path a user takes to set up state before exercising `rm`.
fn addManaged(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    paths: []const []const u8,
) !void {
    var argv_buf: [16][]const u8 = undefined;
    argv_buf[0] = "add";
    std.debug.assert(paths.len <= argv_buf.len - 1);
    for (paths, argv_buf[1..][0..paths.len]) |src, *dst| dst.* = src;
    var res = try runZit(gpa, io, dir, argv_buf[0 .. 1 + paths.len]);
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
}

/// Runs `/bin/sh -c <script>` under the same pinned, isolated environment as
/// `runZit`, with cwd bound to `tmp_dir` and the absolute `zit` path exposed as
/// `$ZIT`. Driving the binary through a real shell is the only faithful way to
/// exercise the output-composition guarantees (B-rm-14): `>> logfile` (O_APPEND)
/// and grouped/shared-fd redirections (`{ …; } > log`) inherit an OS file offset
/// that a process's stdout must honor. `std.process.run`'s captured pipe is not
/// seekable and so cannot reproduce these; the shell can. Caller owns the
/// returned `stdout`/`stderr`.
fn runShell(
    gpa: std.mem.Allocator,
    io: std.Io,
    tmp_dir: std.Io.Dir,
    script: []const u8,
) !std.process.RunResult {
    var env = std.process.Environ.Map.init(gpa);
    defer env.deinit();
    try env.put("TZ", "UTC");
    try env.put("HOME", "/zit-e2e-isolated-home-does-not-exist");
    try env.put("PATH", "/usr/bin:/bin");

    const cwd_abs = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd_abs);
    const abs_exe = try std.fs.path.resolve(gpa, &.{ cwd_abs, zit_exe });
    defer gpa.free(abs_exe);
    try env.put("ZIT", abs_exe);

    return std.process.run(gpa, io, .{
        .argv = &.{ "/bin/sh", "-c", script },
        .cwd = .{ .dir = tmp_dir },
        .environ_map = &env,
    });
}

/// Reads `name` under `dir` fully and returns its bytes (caller frees). Used by
/// the composition tests to read back a redirected log file.
fn readFile(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, name: []const u8) ![]u8 {
    return dir.readFileAlloc(io, name, gpa, .limited(1 << 16));
}

// --- tests ----------------------------------------------------------------

test "cli init B-init-2/-3/-4: happy path writes the header, unborn trunk, prints the absolute path" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try spawnInit(gpa, io, tmp.dir, &.{});
    defer freeResult(gpa, &res);

    try expectExit(res.term, 0);
    // stdout: the success line; its last whitespace-delimited token is an
    // absolute `.zit` path. Structural — no wall-clock, no hash asserted.
    try std.testing.expect(std.mem.startsWith(u8, res.stdout, success_prefix));
    try std.testing.expect(std.mem.endsWith(u8, res.stdout, "/.zit\n"));
    const path = res.stdout[success_prefix.len .. res.stdout.len - 1];
    try std.testing.expect(std.fs.path.isAbsolute(path));
    // discipline: nothing on stderr for a success (B-init-12).
    try std.testing.expectEqualStrings("", res.stderr);

    const bytes = try expectFreshRepo(gpa, io, tmp.dir);
    gpa.free(bytes);
}

test "cli init B-init-5: an explicit missing directory (and its parents) is created" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try spawnInit(gpa, io, tmp.dir, &.{"sub/deep"});
    defer freeResult(gpa, &res);

    try expectExit(res.term, 0);
    try std.testing.expect(std.mem.endsWith(u8, res.stdout, "/sub/deep/.zit\n"));
    try std.testing.expectEqualStrings("", res.stderr);

    var sub = try tmp.dir.openDir(io, "sub/deep", .{});
    defer sub.close(io);
    const bytes = try expectFreshRepo(gpa, io, sub);
    gpa.free(bytes);
}

test "cli init B-init-5: 'init .' produces the same observable state as the bare form" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_bare = std.testing.tmpDir(.{});
    defer tmp_bare.cleanup();
    var res_bare = try spawnInit(gpa, io, tmp_bare.dir, &.{});
    defer freeResult(gpa, &res_bare);
    try expectExit(res_bare.term, 0);
    const bytes_bare = try expectFreshRepo(gpa, io, tmp_bare.dir);
    defer gpa.free(bytes_bare);

    var tmp_dot = std.testing.tmpDir(.{});
    defer tmp_dot.cleanup();
    var res_dot = try spawnInit(gpa, io, tmp_dot.dir, &.{"."});
    defer freeResult(gpa, &res_dot);
    try expectExit(res_dot.term, 0);
    const bytes_dot = try expectFreshRepo(gpa, io, tmp_dot.dir);
    defer gpa.free(bytes_dot);

    // `zit init` and `zit init .` are equivalent: both parse to the cwd and
    // yield a valid fresh repo (asserted above). They are NOT byte-identical —
    // each repository gets its own §9.1 project code, so the two codes are
    // well-formed and, being per-repo, distinct.
    const code_bare = try readProjectCode(gpa, io, tmp_bare.dir);
    defer gpa.free(code_bare);
    const code_dot = try readProjectCode(gpa, io, tmp_dot.dir);
    defer gpa.free(code_dot);
    try std.testing.expect(!std.mem.eql(u8, code_bare, code_dot));
}

test "cli init B-init-6: -q suppresses the success line; on-disk state is identical" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_loud = std.testing.tmpDir(.{});
    defer tmp_loud.cleanup();
    var res_loud = try spawnInit(gpa, io, tmp_loud.dir, &.{});
    defer freeResult(gpa, &res_loud);
    try expectExit(res_loud.term, 0);
    const bytes_loud = try expectFreshRepo(gpa, io, tmp_loud.dir);
    defer gpa.free(bytes_loud);

    var tmp_quiet = std.testing.tmpDir(.{});
    defer tmp_quiet.cleanup();
    var res_quiet = try spawnInit(gpa, io, tmp_quiet.dir, &.{"-q"});
    defer freeResult(gpa, &res_quiet);
    try expectExit(res_quiet.term, 0);
    // quiet: nothing on stdout, nothing on stderr.
    try std.testing.expectEqualStrings("", res_quiet.stdout);
    try std.testing.expectEqualStrings("", res_quiet.stderr);
    const bytes_quiet = try expectFreshRepo(gpa, io, tmp_quiet.dir);
    defer gpa.free(bytes_quiet);

    // `-q` changes only stdout: both runs create a full, valid fresh repo
    // (asserted above via expectFreshRepo). The on-disk repos are not
    // byte-identical because each carries its own §9.1 project code — a
    // per-repo identity, not a run-to-run nondeterminism.
    const code_loud = try readProjectCode(gpa, io, tmp_loud.dir);
    defer gpa.free(code_loud);
    const code_quiet = try readProjectCode(gpa, io, tmp_quiet.dir);
    defer gpa.free(code_quiet);
    try std.testing.expect(!std.mem.eql(u8, code_loud, code_quiet));
}

test "cli init B-init-7: an existing repository refuses with exit 1 and no change" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var first = try spawnInit(gpa, io, tmp.dir, &.{});
    defer freeResult(gpa, &first);
    try expectExit(first.term, 0);
    const before = try expectFreshRepo(gpa, io, tmp.dir);
    defer gpa.free(before);

    var second = try spawnInit(gpa, io, tmp.dir, &.{});
    defer freeResult(gpa, &second);
    try expectExit(second.term, 1);
    try std.testing.expectEqualStrings("", second.stdout);
    try std.testing.expect(std.mem.startsWith(u8, second.stderr, "zit: a zit repository already exists at "));
    try std.testing.expect(std.mem.endsWith(u8, second.stderr, "/.zit\n"));

    // No change: the `.zit` bytes are byte-identical to after the first init.
    const after = try tmp.dir.readFileAlloc(io, ".zit", gpa, .limited(1 << 20));
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);
}

test "cli init B-init-7: an orphan .zit-checkout does not trigger already-exists" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Pre-seed a stale checkout with NO `.zit` present.
    try checkout.write(io, tmp.dir, .{ .version = "deadbeef", .branch = "feature", .merge_parent = "", .special_op = "", .manifest_flags = "" });

    var res = try spawnInit(gpa, io, tmp.dir, &.{});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    // `.zit` created and the orphan checkout atomically overwritten to unborn.
    const bytes = try expectFreshRepo(gpa, io, tmp.dir);
    gpa.free(bytes);
}

test "cli init B-init-8: a target path that is a file reports not-a-directory, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    {
        var f = try tmp.dir.createFile(io, "notdir", .{});
        f.close(io);
    }

    var res = try spawnInit(gpa, io, tmp.dir, &.{"notdir"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.startsWith(u8, res.stderr, "zit: cannot initialize: "));
    try std.testing.expect(std.mem.endsWith(u8, res.stderr, " is not a directory\n"));
    // Nothing created: no `.zit` beside the offending file.
    try std.testing.expect(!fileExists(io, tmp.dir, ".zit"));
}

test "cli init B-init-9: a failure after .zit is created rolls it back (all-or-nothing), exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Force the checkout-write step to fail deterministically: a DIRECTORY
    // named `.zit-checkout` cannot be atomically replaced by a file, so
    // `checkout.write` returns error.Storage AFTER `.zit` was created — the
    // exact rollback path.
    try tmp.dir.createDir(io, checkout.filename, .default_dir);

    var res = try spawnInit(gpa, io, tmp.dir, &.{});
    defer freeResult(gpa, &res);

    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.startsWith(u8, res.stderr, "zit: cannot initialize repository at "));
    try std.testing.expect(std.mem.endsWith(u8, res.stderr, "\n"));
    // Rollback: `.zit` must be absent so a re-run is not poisoned into B-init-7.
    try std.testing.expect(!fileExists(io, tmp.dir, ".zit"));
}

test "cli init B-init-10: an unknown option prints usage to stderr, exit 2" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try spawnInit(gpa, io, tmp.dir, &.{"-x"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit init: unknown option '-x'\ntry 'zit init --help'\n",
        res.stderr,
    );
    try std.testing.expect(!fileExists(io, tmp.dir, ".zit"));
}

test "cli init B-init-10: a second positional prints usage to stderr, exit 2" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try spawnInit(gpa, io, tmp.dir, &.{ "a", "b" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit init: unexpected argument 'b'\ntry 'zit init --help'\n",
        res.stderr,
    );
    try std.testing.expect(!fileExists(io, tmp.dir, ".zit"));
}

/// Independent transcription of the B-init-11 help text (contracts/init.md).
/// Hand-copied from the contract — never generated from the binary — so the
/// binary's output is checked against the spec, not against itself.
const expected_init_help =
    \\usage: zit init [-q|--quiet] [--] [directory]
    \\
    \\Create an empty zit repository in <directory> (default: the current
    \\directory). Writes two files there:
    \\
    \\  .zit            the repository — a single, self-contained content log
    \\  .zit-checkout   the working-state record (branch trunk, no check-in yet)
    \\
    \\The new repository has no check-ins: the default branch 'trunk' is unborn
    \\until your first `zit commit`, which records the root check-in. Artifacts
    \\are named with SHA3-256 by default.
    \\
    \\If <directory> does not exist it is created.
    \\
    \\options:
    \\  -q, --quiet   do not print the confirmation line
    \\  -h, --help    show this help and exit
    \\
    \\exit status:
    \\  0  repository created
    \\  1  a repository already exists, or the path could not be initialized
    \\  2  usage error (unknown option or extra argument)
    \\
    \\aliases: new
    \\
;

test "cli init B-init-11: --help and -h print the verbatim help to stdout, exit 0, no changes" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    inline for (.{ "--help", "-h" }) |flag| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();

        var res = try spawnInit(gpa, io, tmp.dir, &.{flag});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(expected_init_help, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
        // help makes no changes.
        try std.testing.expect(!fileExists(io, tmp.dir, ".zit"));
        try std.testing.expect(!fileExists(io, tmp.dir, ".zit-checkout"));
    }
}

test "cli init B-init-12: data goes to stdout and diagnostics to stderr, never mixed" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Success: stdout carries the line, stderr is empty.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try spawnInit(gpa, io, tmp.dir, &.{});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(res.stdout.len > 0);
        try std.testing.expectEqualStrings("", res.stderr);
    }
    // Usage error: stderr carries the diagnostic, stdout is empty.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try spawnInit(gpa, io, tmp.dir, &.{"-x"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(res.stderr.len > 0);
    }
}

// ==========================================================================
// `zit add` end-to-end (contracts/add.md B-add-1..18). Each spawns the REAL
// binary in an isolated, pinned environment and reads `.zit-managed` back
// through the library.
// ==========================================================================

test "cli add B-add-2/-14/-16: happy path marks files managed, prints added, persists the set" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try touch(io, tmp.dir, "b");

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "a", "b" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("ADDED  a\nADDED  b\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);

    try expectManaged(gpa, io, tmp.dir, &.{ "a", "b" });
}

test "cli add B-add-3: re-adding a managed path is idempotent (notice to stderr, set unchanged)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try touch(io, tmp.dir, "b");

    var first = try runZit(gpa, io, tmp.dir, &.{ "add", "a", "b" });
    defer freeResult(gpa, &first);
    try expectExit(first.term, 0);

    var second = try runZit(gpa, io, tmp.dir, &.{ "add", "a" });
    defer freeResult(gpa, &second);
    try expectExit(second.term, 0);
    try std.testing.expectEqualStrings("", second.stdout);
    try std.testing.expectEqualStrings("zit: 'a' is already tracked\n", second.stderr);

    try expectManaged(gpa, io, tmp.dir, &.{ "a", "b" });
}

test "cli add B-add-15: ./a, a, and sub/../a denote one managed entry" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try tmp.dir.createDir(io, "sub", .default_dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "./a", "a", "sub/../a" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // First normalizes-and-adds; the later two are already tracked.
    try std.testing.expectEqualStrings("ADDED  a\n", res.stdout);
    try std.testing.expectEqualStrings(
        "zit: 'a' is already tracked\nzit: 'a' is already tracked\n",
        res.stderr,
    );
    try expectManaged(gpa, io, tmp.dir, &.{"a"});
}

test "cli add B-add-4/-18: a missing path is partial — the valid sibling is still added, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "a", "nope" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("ADDED  a\n", res.stdout);
    try std.testing.expectEqualStrings("zit add: 'nope': no such file\n", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{"a"});
}

test "cli add B-add-5: a named symlink is rejected; a directory recurses; the regular sibling is added" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try tmp.dir.createDir(io, "d", .default_dir);
    try tmp.dir.symLink(io, "a", "s", .{});

    // Since P3-F4 a directory argument recurses (here `d` is empty, so it
    // contributes nothing and is not an error). Naming a symlink directly is
    // still B-add-5's refusal — since F-B3 with the symlink-specific wording,
    // so it reads the same as the skip `add <dir>` prints for the same entry
    // and does not read like a fifo/socket refusal.
    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "a", "d", "s" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("ADDED  a\n", res.stdout);
    try std.testing.expectEqualStrings("zit add: 's': symlink (not versioned in 0.1.0)\n", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{"a"});
}

test "cli add B-add-6: a path outside the root is refused, nothing added" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "../x", "/etc/hosts" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit add: '../x' is outside the repository\n" ++
            "zit add: '/etc/hosts' is outside the repository\n",
        res.stderr,
    );
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli add B-add-7: the three control files are refused" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", ".zit", ".zit-checkout", ".zit-cache" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit add: '.zit' is a zit control file and cannot be tracked\n" ++
            "zit add: '.zit-checkout' is a zit control file and cannot be tracked\n" ++
            "zit add: '.zit-cache' is a zit control file and cannot be tracked\n",
        res.stderr,
    );
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli add B-add-8: not a zit repository" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try touch(io, tmp.dir, "a"); // a file, but no repo anywhere above

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "a" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit: no zit checkout here (looked for .zit-checkout in this directory and its parents)\n" ++
            "if this is a bare repository, name it with -R, or run 'zit open' to create a working tree\n",
        res.stderr,
    );
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli add B-add-9: no paths is a usage error, exit 2" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"add"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit add: no paths given\ntry 'zit add --help'\n", res.stderr);
}

test "cli add B-add-10: an unknown option is a usage error, exit 2" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "-x" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit add: unknown option '-x'\ntry 'zit add --help'\n",
        res.stderr,
    );
}

test "cli add B-add-11: --dry-run previews and persists nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "-n", "a" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("would add a\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    // No write occurred: `.zit-managed` is absent.
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));

    // A dry run with a missing sibling still previews the valid one and reports
    // the error, exits 1, and writes nothing.
    var res2 = try runZit(gpa, io, tmp.dir, &.{ "add", "--dry-run", "a", "nope" });
    defer freeResult(gpa, &res2);
    try expectExit(res2.term, 1);
    try std.testing.expectEqualStrings("would add a\n", res2.stdout);
    try std.testing.expectEqualStrings("zit add: 'nope': no such file\n", res2.stderr);
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli add B-add-12: --quiet suppresses chatter but keeps effect and errors" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "-q", "a", "nope" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    // The `added a` line is suppressed; the error still prints.
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit add: 'nope': no such file\n", res.stderr);
    // Effect persisted despite quiet.
    try expectManaged(gpa, io, tmp.dir, &.{"a"});

    // Quiet also suppresses the already-tracked notice.
    var res2 = try runZit(gpa, io, tmp.dir, &.{ "add", "-q", "a" });
    defer freeResult(gpa, &res2);
    try expectExit(res2.term, 0);
    try std.testing.expectEqualStrings("", res2.stdout);
    try std.testing.expectEqualStrings("", res2.stderr);
}

/// Independent transcription of the B-add-13 help text (contracts/add.md §Help).
/// Hand-copied from the contract — never generated from the binary.
const expected_add_help =
    \\usage: zit add [-n|--dry-run] [-q|--quiet] [-f|--force] [--dotfiles]
    \\           [--] <path>...
    \\
    \\Mark one or more working-tree files as managed, so they are included in
    \\your next check-in. zit has no staging area: `add` records only *which*
    \\files to track — their contents are read from the working tree when you
    \\run `zit commit`, not now. Re-adding an already-tracked file does nothing.
    \\
    \\Files stay tracked until `zit rm` forgets them. You can add files before
    \\the first check-in (the default branch 'trunk' is unborn until then).
    \\
    \\Naming a directory adds every untracked file beneath it, recursively;
    \\`zit add .` adds the whole tree. zit's control files are never added, and
    \\names beginning with a dot are skipped unless --dotfiles is given (naming
    \\one directly always adds it).
    \\
    \\options:
    \\  -n, --dry-run   show what would be added; change nothing
    \\  -q, --quiet     do not print the added files
    \\  -f, --force     add a file named directly even when ignore-glob
    \\                  matches it (a directory expansion always skips those)
    \\      --dotfiles  a directory expansion also takes names beginning with a
    \\                  dot (also settable with `zit settings dotfiles on`)
    \\  -h, --help      show this help and exit
    \\
    \\exit status:
    \\  0  all named files are now tracked (or already were)
    \\  1  a file could not be added (missing, not a regular file, outside the
    \\     repo), or this is not a zit repository
    \\  2  usage error (no files given, or an unknown option)
    \\
;

test "cli add B-add-13: --help/-h prints the verbatim help, exit 0, changes nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Help wins even with no repo, no path, and extra tokens present.
    inline for (.{
        &[_][]const u8{ "add", "--help" },
        &[_][]const u8{ "add", "-h" },
        &[_][]const u8{ "add", "--help", "a", "b" },
    }) |argv| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(expected_add_help, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
        try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
    }
}

test "cli add B-add-16: an all-invalid invocation writes nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "nope", "also-nope" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    // No managed file created when nothing valid was added.
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli add B-add-17: a corrupt .zit-managed fails cleanly, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    // Pre-write garbage: the leading u32 decodes to an impossible entry count.
    try corruptSection(io, tmp.dir, .managed);

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "a" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit add: cannot read tracked files: .zit-checkout is corrupt\n",
        res.stderr,
    );
}

test "cli add B-add-16/-17: a write failure leaves the prior managed set intact, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");

    // Make the checkout root read-only so `ManagedSet.write` cannot create its
    // temp file → error.Storage, AFTER the (absent) set reads as empty. Restore
    // write permission before cleanup can run (defers are LIFO).
    try tmp.dir.setFilePermissions(io, ".", File.Permissions.fromMode(0o555), .{ .follow_symlinks = false });
    defer tmp.dir.setFilePermissions(io, ".", File.Permissions.fromMode(0o777), .{ .follow_symlinks = false }) catch {};

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "a" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings(
        "zit add: cannot update tracked files: input/output error\n",
        res.stderr,
    );
    // The prior state is intact: no managed file was left behind.
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

// ==========================================================================
// Regression coverage for the three refuted behaviors (deep-cwd crash;
// help-vs-unknown-option precedence; `zit add .` misclassification).
// ==========================================================================

/// Creates `levels` nested subdirectories each named `seg` under `base`, and
/// returns an open handle to the deepest one (caller closes it). Built one
/// component at a time through directory handles, so the total path may exceed
/// the platform PATH_MAX without any single syscall ever seeing an over-long
/// path — the only portable way to place a subprocess's cwd past that limit.
fn makeDeepDir(io: std.Io, base: std.Io.Dir, seg: []const u8, levels: usize) !std.Io.Dir {
    var cur = try base.openDir(io, ".", .{});
    errdefer cur.close(io);
    for (0..levels) |_| {
        cur.createDir(io, seg, .default_dir) catch |e| switch (e) {
            error.PathAlreadyExists => {},
            else => return e,
        };
        const child = try cur.openDir(io, seg, .{});
        cur.close(io);
        cur = child;
    }
    return cur;
}

test "cli add B-add-17/-18: a deep checkout diagnoses instead of crashing (no panic)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // A cwd whose absolute path exceeds the platform PATH_MAX. Running `zit add`
    // from there made std's `currentPathAlloc` abort the process (SIGABRT); the
    // fix must instead exit cleanly with a diagnostic. 24 × 200-char levels
    // ≈ 4800 bytes, past both the 1024 (macOS) and 4096 (Linux) limits.
    const seg = "d" ** 200;
    var deep = try makeDeepDir(io, tmp.dir, seg, 24);
    defer deep.close(io);
    try touch(io, deep, "deep.txt");

    var res = try runZit(gpa, io, deep, &.{ "add", "deep.txt" });
    defer freeResult(gpa, &res);
    // The regression guard: a clean exit 1, never a signal/abort term, and no
    // panic trace on stderr.
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "panic") == null);
    // Where getcwd names the deep path, the repo-relative key exceeds
    // managed.max_path_len → the honest B-add-17 message; where getcwd refuses
    // an over-long path, discovery reports no repository. Both are honest exit-1
    // diagnostics; neither is a crash.
    try std.testing.expect(
        std.mem.indexOf(u8, res.stderr, "path is too long to track") != null or
            std.mem.indexOf(u8, res.stderr, "no zit checkout here") != null,
    );
}

test "cli add B-add-17: a path longer than managed.max_path_len is rejected as too long" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // A single arg whose normalized repo-relative key exceeds
    // managed.max_path_len (4096), built from short "aa/" components so every
    // path component is well under NAME_MAX. The file need not exist: the length
    // bound is enforced before any filesystem stat, so this is deterministic on
    // every platform (cwd stays short).
    var buf: [managed.max_path_len + 64]u8 = undefined;
    var w: usize = 0;
    while (w + 3 <= buf.len) : (w += 3) {
        buf[w] = 'a';
        buf[w + 1] = 'a';
        buf[w + 2] = '/';
    }
    buf[w] = 'f';
    w += 1;
    const long_arg = buf[0..w];
    try std.testing.expect(long_arg.len > managed.max_path_len);

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", long_arg });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    var ebuf: [managed.max_path_len + 128]u8 = undefined;
    const want = try std.fmt.bufPrint(&ebuf, "zit add: '{s}': path is too long to track\n", .{long_arg});
    try std.testing.expectEqualStrings(want, res.stderr);
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli add B-add-13/-10: --help wins over an unknown option (exit 0, verbatim help)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    // Both orders print the help and exit 0, never the unknown-option usage
    // error — B-add-13's explicit precedence ("help wins over any other token")
    // beats B-add-10.
    inline for (.{
        &[_][]const u8{ "add", "--help", "-x" },
        &[_][]const u8{ "add", "-x", "--help" },
        &[_][]const u8{ "add", "-x", "-h", "nope.txt" },
    }) |argv| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(expected_add_help, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
        try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
    }
}

test "cli add B-add-5: 'zit add .' at the root sweeps the tree, never 'outside the repository'" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // `.` resolves to the checkout root itself — a directory INSIDE the repo,
    // never B-add-6's "outside the repository" (the most likely novice
    // invocation). Since P3-F4 it means the whole tree; a tree holding only the
    // control files therefore adds nothing and succeeds.
    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "." });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

// ==========================================================================
// `zit rm` end-to-end (contracts/rm.md B-rm-1..19). Each spawns the REAL binary
// in an isolated, pinned environment, sets up tracked state via the real `zit
// add`, and reads `.zit-managed` back through the library.
// ==========================================================================

test "cli rm B-rm-2/-14/-16: happy path forgets tracked files, prints removed, persists, keeps files on disk" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try touch(io, tmp.dir, "b");
    try touch(io, tmp.dir, "c");
    try addManaged(gpa, io, tmp.dir, &.{ "a", "b", "c" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "a", "b" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // stdout carries the removals in argument order, each stating the file was
    // kept on disk (D1); stderr is empty (B-rm-14).
    try std.testing.expectEqualStrings(
        "DELETED  a\nDELETED  b\n",
        res.stdout,
    );
    try std.testing.expectEqualStrings("", res.stderr);
    // Only c remains tracked (readback is the oracle, B-rm-16).
    try expectManaged(gpa, io, tmp.dir, &.{"c"});
    // Default rm keeps the working files on disk untouched (B-rm-2).
    try std.testing.expect(fileExists(io, tmp.dir, "a"));
    try std.testing.expect(fileExists(io, tmp.dir, "b"));
}

test "cli rm B-rm-2: membership not existence — a tracked file already deleted from disk is still forgettable" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "gone");
    try addManaged(gpa, io, tmp.dir, &.{"gone"});
    // The user deletes the working file themselves — the primary reason to rm.
    try tmp.dir.deleteFile(io, "gone");

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "gone" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("DELETED  gone\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli rm B-rm-3: --hard also deletes the working file from disk" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try touch(io, tmp.dir, "b");
    try addManaged(gpa, io, tmp.dir, &.{ "a", "b" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "a" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // Under --hard the line states the file was deleted from disk (D1).
    try std.testing.expectEqualStrings("DELETED  a (deleted from disk)\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{"b"});
    // --hard removed the working file; the untouched sibling remains.
    try std.testing.expect(!fileExists(io, tmp.dir, "a"));
    try std.testing.expect(fileExists(io, tmp.dir, "b"));
}

test "cli rm B-rm-3: --hard on a tracked file already gone from disk is success" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    try tmp.dir.deleteFile(io, "a"); // already gone before --hard

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "a" });
    defer freeResult(gpa, &res);
    // FileNotFound ⇒ postcondition ("no working file") already holds ⇒ success.
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("DELETED  a (deleted from disk)\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli rm B-rm-4/-19: a not-tracked path is partial — the tracked sibling is still forgotten, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try addManaged(gpa, io, tmp.dir, &.{"a"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "a", "nope" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("DELETED  a\n", res.stdout);
    try std.testing.expectEqualStrings("zit rm: 'nope' is not tracked\n", res.stderr);
    // The valid forget still persisted (do-as-much-as-possible).
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli rm B-rm-5: 'zit rm .' with nothing tracked is 'not tracked', never 'outside'" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // `.` is the checkout root — inside the repo, never B-rm-6's "outside".
    // Since P3-F4 it expands over the tracked keys; with none tracked the
    // expansion is empty, which is the existing not-tracked diagnostic.
    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "." });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit rm: '.' is not tracked\n", res.stderr);
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli rm B-rm-6: a path outside the root is refused" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "../x", "/etc/hosts" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit rm: '../x' is outside the repository\n" ++
            "zit rm: '/etc/hosts' is outside the repository\n",
        res.stderr,
    );
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli rm B-rm-7: the three control files are refused and never --hard-deleted" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // Give the set a real entry so a successful write would occur if any control
    // file slipped through; it must not, and the control files must survive.
    try touch(io, tmp.dir, "a");
    try addManaged(gpa, io, tmp.dir, &.{"a"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", ".zit", ".zit-checkout", ".zit-cache" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit rm: '.zit' is a zit control file and cannot be tracked\n" ++
            "zit rm: '.zit-checkout' is a zit control file and cannot be tracked\n" ++
            "zit rm: '.zit-cache' is a zit control file and cannot be tracked\n",
        res.stderr,
    );
    // The guard held: every control file still on disk, set unchanged.
    try std.testing.expect(fileExists(io, tmp.dir, ".zit"));
    try std.testing.expect(fileExists(io, tmp.dir, ".zit-checkout"));
    try std.testing.expect(sectionExists(gpa, io, tmp.dir, .managed));
    try expectManaged(gpa, io, tmp.dir, &.{"a"});
}

test "cli rm B-rm-8: not a zit repository" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try touch(io, tmp.dir, "a"); // a file, but no repo anywhere above

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "a" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit: no zit checkout here (looked for .zit-checkout in this directory and its parents)\n" ++
            "if this is a bare repository, name it with -R, or run 'zit open' to create a working tree\n",
        res.stderr,
    );
}

test "cli rm B-rm-9: no paths is a usage error, exit 2" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"rm"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit rm: no paths given\ntry 'zit rm --help'\n", res.stderr);
}

test "cli rm B-rm-10: an unknown option is a usage error, exit 2" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "-x" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit rm: unknown option '-x'\ntry 'zit rm --help'\n",
        res.stderr,
    );
}

test "cli rm B-rm-11: --dry-run previews and persists/deletes nothing (even with --hard)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try addManaged(gpa, io, tmp.dir, &.{"a"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "-n", "a" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("would remove a\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    // Nothing persisted: the set still tracks a, the file still exists.
    try expectManaged(gpa, io, tmp.dir, &.{"a"});
    try std.testing.expect(fileExists(io, tmp.dir, "a"));

    // `-n --hard` previews without unlinking.
    var res2 = try runZit(gpa, io, tmp.dir, &.{ "rm", "--dry-run", "--hard", "a" });
    defer freeResult(gpa, &res2);
    try expectExit(res2.term, 0);
    // `-n --hard` previews the disk deletion without performing it (D1: the
    // whole line is subjunctive under `would remove`).
    try std.testing.expectEqualStrings("would remove a (deleted from disk)\n", res2.stdout);
    try std.testing.expectEqualStrings("", res2.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{"a"});
    try std.testing.expect(fileExists(io, tmp.dir, "a"));
}

test "cli rm B-rm-12: --quiet suppresses removed lines but keeps effect and errors" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try addManaged(gpa, io, tmp.dir, &.{"a"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "-q", "a", "nope" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    // The `removed a` line is suppressed; the error still prints (B-rm-12).
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit rm: 'nope' is not tracked\n", res.stderr);
    // Effect persisted despite quiet.
    try expectManaged(gpa, io, tmp.dir, &.{});
}

/// Independent transcription of the B-rm-13 help text (contracts/rm.md §Help).
/// Hand-copied from the contract — never generated from the binary.
const expected_rm_help =
    \\usage: zit rm [-n|--dry-run] [--hard] [-q|--quiet] [--] <path>...
    \\
    \\Stop tracking one or more files: remove their paths from the managed set,
    \\so your next check-in records their deletion. zit has no staging area, so
    \\this takes effect when you run `zit commit`, not now.
    \\
    \\By default your working copy is left on disk untouched -- `rm` only forgets
    \\the file. Pass --hard to also delete the file from disk. (git deletes by
    \\default; zit keeps your file unless you ask for --hard.)
    \\
    \\Removing a path that is not tracked is an error. Naming a directory forgets
    \\every tracked file beneath it, whether or not the directory still exists.
    \\
    \\options:
    \\  -n, --dry-run   show what would be removed; change nothing
    \\      --hard      also delete the working-tree file from disk
    \\  -q, --quiet     do not print the removed files
    \\  -h, --help      show this help and exit
    \\
    \\exit status:
    \\  0  all named files are no longer tracked (and, with --hard, deleted)
    \\  1  a file could not be removed (not tracked, outside the repo, or --hard
    \\     could not delete it), or this is not a zit repository
    \\  2  usage error (no files given, or an unknown option)
    \\
    \\aliases: delete, forget, remove
    \\
;

test "cli rm B-rm-13: --help/-h prints the verbatim help, exit 0, changes nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Help wins even with no repo, no path, and extra/unknown tokens present.
    inline for (.{
        &[_][]const u8{ "rm", "--help" },
        &[_][]const u8{ "rm", "-h" },
        &[_][]const u8{ "rm", "--help", "a", "b" },
        &[_][]const u8{ "rm", "-x", "--help" },
    }) |argv| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(expected_rm_help, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
        try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
    }
}

test "cli rm B-rm-15: ./a, a, and sub/../a denote one managed entry (forget once, rest not tracked)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try tmp.dir.createDir(io, "sub", .default_dir);
    try addManaged(gpa, io, tmp.dir, &.{"a"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "./a", "a", "sub/../a" });
    defer freeResult(gpa, &res);
    // First forgets the normalized key `a`; the later two are now not tracked.
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("DELETED  a\n", res.stdout);
    try std.testing.expectEqualStrings(
        "zit rm: 'a' is not tracked\nzit rm: 'a' is not tracked\n",
        res.stderr,
    );
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli rm B-rm-16/-17: a write failure leaves the prior set intact and deletes nothing, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try addManaged(gpa, io, tmp.dir, &.{"a"});

    // Make the checkout root read-only so `ManagedSet.write` cannot create its
    // temp file → error.Storage, AFTER `a` is forgotten in memory. Restore write
    // permission before cleanup can run (defers are LIFO).
    try tmp.dir.setFilePermissions(io, ".", File.Permissions.fromMode(0o555), .{ .follow_symlinks = false });
    defer tmp.dir.setFilePermissions(io, ".", File.Permissions.fromMode(0o777), .{ .follow_symlinks = false }) catch {};

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "a" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings(
        "zit rm: cannot update tracked files: input/output error\n",
        res.stderr,
    );
    // The write failed, so NO `removed` line may reach stdout — the report is
    // withheld until after a durable write (B-rm-14/-17). stdout must be empty.
    try std.testing.expectEqualStrings("", res.stdout);
    // Persist-before-destroy (B-rm-16): the write failed, so `a` is still tracked
    // AND still on disk — nothing was unlinked.
    try expectManaged(gpa, io, tmp.dir, &.{"a"});
    try std.testing.expect(fileExists(io, tmp.dir, "a"));
}

test "cli rm B-rm-17/-14: a write failure claims NO removal on stdout; every named path stays tracked" {
    // The exact verify refutation: two tracked files, the checkout root made
    // unwritable so `ManagedSet.write` cannot create its temp file. The forgets
    // happen in memory, the atomic write fails → exit 1 with only the storage
    // error on stderr. stdout must NOT print `removed f1`/`removed f2` (nothing
    // was persisted), and a readback must show BOTH paths still tracked so that
    // `zit rm <files> | wc -l` counts exactly the real (zero) removals.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "f1.txt");
    try touch(io, tmp.dir, "f2.txt");
    try addManaged(gpa, io, tmp.dir, &.{ "f1.txt", "f2.txt" });

    try tmp.dir.setFilePermissions(io, ".", File.Permissions.fromMode(0o555), .{ .follow_symlinks = false });
    defer tmp.dir.setFilePermissions(io, ".", File.Permissions.fromMode(0o777), .{ .follow_symlinks = false }) catch {};

    // Default (non-hard) rm, two paths — mirrors the refutation repro exactly.
    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "f1.txt", "f2.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings(
        "zit rm: cannot update tracked files: input/output error\n",
        res.stderr,
    );
    try std.testing.expectEqualStrings("", res.stdout);

    // Restore write access so the readback (and cleanup) can open the set, then
    // confirm the on-disk set is unchanged — both paths still tracked.
    try tmp.dir.setFilePermissions(io, ".", File.Permissions.fromMode(0o777), .{ .follow_symlinks = false });
    try expectManaged(gpa, io, tmp.dir, &.{ "f1.txt", "f2.txt" });
}

test "cli rm B-rm-17: a corrupt .zit-managed fails cleanly, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // Pre-write garbage: the leading u32 decodes to an impossible entry count.
    try corruptSection(io, tmp.dir, .managed);

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "a" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit rm: cannot read tracked files: .zit-checkout is corrupt\n",
        res.stderr,
    );
}

test "cli rm B-rm-18: --hard forgets and persists but reports an unlink failure, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Seed the managed set (through the library) with a key that names a
    // DIRECTORY on disk. `rm --hard d` forgets `d` and writes the set, then the
    // unlink fails (deleteFile refuses a directory) — the honest partial: the
    // forget stands, only the disk delete failed. Directory recursion is out of
    // scope, so this is the deterministic non-FileNotFound unlink failure.
    try tmp.dir.createDir(io, "d", .default_dir);
    {
        var set = managed.ManagedSet.init(gpa);
        defer set.deinit();
        _ = try set.insert("d");
        try set.write(io, tmp.dir);
    }

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "d" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    // The unlink FAILED, so `d` is a failed path (B-rm-19): NOTHING about it may
    // reach stdout (B-rm-4/-14). The earlier defect printed
    // `removed d (deleted from disk)\n` here — a suffix claiming a deletion that
    // never happened — while the file remained on disk; stdout must be empty.
    try std.testing.expectEqualStrings("", res.stdout);
    // The unlink failure is an honest partial on stderr (the forget stands).
    try std.testing.expect(std.mem.startsWith(
        u8,
        res.stderr,
        "zit rm: 'd': removed from tracking but could not delete from disk: ",
    ));
    try std.testing.expect(std.mem.endsWith(u8, res.stderr, "\n"));
    // The forget is persisted (it preceded the failed unlink); the dir remains.
    try expectManaged(gpa, io, tmp.dir, &.{});
    var d = try tmp.dir.openDir(io, "d", .{});
    d.close(io);
}

test "cli rm B-rm-3/-18/-14: --hard routes a deleted file to stdout and an unlink failure to stderr" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // `ok` is a regular file that unlinks cleanly (→ stdout removal line); `d` is
    // a directory whose deleteFile fails (directory recursion is out of scope) —
    // a deterministic non-FileNotFound unlink failure (→ stderr only). Seeding
    // both through the library keeps this independent of `zit add`'s file check.
    try touch(io, tmp.dir, "ok");
    try tmp.dir.createDir(io, "d", .default_dir);
    {
        var set = managed.ManagedSet.init(gpa);
        defer set.deinit();
        _ = try set.insert("ok");
        _ = try set.insert("d");
        try set.write(io, tmp.dir);
    }

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "ok", "d" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    // Only the successfully-deleted file prints a stdout removal line (D6): the
    // failed path is stderr-only, so `| wc -l` counts exactly the real deletions
    // and no disposition suffix ever lies (B-rm-3/-4/-14/-18).
    try std.testing.expectEqualStrings("DELETED  ok (deleted from disk)\n", res.stdout);
    try std.testing.expect(std.mem.startsWith(
        u8,
        res.stderr,
        "zit rm: 'd': removed from tracking but could not delete from disk: ",
    ));
    try std.testing.expect(std.mem.endsWith(u8, res.stderr, "\n"));
    // Both forgets persisted (they precede the unlink); `ok` gone, `d` remains.
    try expectManaged(gpa, io, tmp.dir, &.{});
    try std.testing.expect(!fileExists(io, tmp.dir, "ok"));
    var d = try tmp.dir.openDir(io, "d", .{});
    d.close(io);
}

test "cli rm B-rm-14: `zit rm >> log` appends, never clobbers prior file content (O_APPEND honored)" {
    // Repro 1 of the composition refutation: a positional stdout writer wrote
    // from offset 0, overwriting `PRE-EXISTING-LI` with `removed g1\n`. A
    // streaming writer honors the inherited O_APPEND offset, so the line is
    // appended after the pre-existing content.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "g1");
    try addManaged(gpa, io, tmp.dir, &.{"g1"});

    var res = try runShell(gpa, io, tmp.dir, "printf 'PRE-EXISTING-LINE\\n' > log; \"$ZIT\" rm g1 >> log");
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    const log = try readFile(gpa, io, tmp.dir, "log");
    defer gpa.free(log);
    try std.testing.expectEqualStrings("PRE-EXISTING-LINE\nDELETED  g1\n", log);
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli rm B-rm-14: a grouped redirect keeps the shell's own lines around zit's output (shared fd offset)" {
    // Repro 2: `{ echo …; zit rm …; echo …; } > log` — zit shares one open file
    // description with the shell's `echo`s. A positional write at offset 0
    // overwrote `SHELL-LINE-1`; a streaming write lands at the shared, advanced
    // offset, so all three lines survive in order.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "g2");
    try addManaged(gpa, io, tmp.dir, &.{"g2"});

    var res = try runShell(gpa, io, tmp.dir, "{ echo SHELL-LINE-1; \"$ZIT\" rm g2; echo SHELL-LINE-3; } > log");
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    const log = try readFile(gpa, io, tmp.dir, "log");
    defer gpa.free(log);
    try std.testing.expectEqualStrings("SHELL-LINE-1\nDELETED  g2\nSHELL-LINE-3\n", log);
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli rm B-rm-14: two zit invocations sharing one redirected fd do not overwrite each other" {
    // Repro 3: `{ zit rm a; zit rm b; } > log` — the second invocation inherits
    // the fd whose offset the first advanced. Positional writes made the second
    // clobber the first (`removed g3a\n` → `.txt\n`); streaming writes append.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "g3a");
    try touch(io, tmp.dir, "g3b");
    try addManaged(gpa, io, tmp.dir, &.{ "g3a", "g3b" });

    var res = try runShell(gpa, io, tmp.dir, "{ \"$ZIT\" rm g3a; \"$ZIT\" rm g3b; } > log");
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    const log = try readFile(gpa, io, tmp.dir, "log");
    defer gpa.free(log);
    try std.testing.expectEqualStrings(
        "DELETED  g3a\nDELETED  g3b\n",
        log,
    );
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli rm B-rm-10 (DX): --force/-f teaches the safe-by-default model, exit 2, stderr only" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    // Parse rejects the flag before repo discovery, so no repo is needed; the
    // message must educate (files kept by default, --hard to delete) rather than
    // print a bare "unknown option".
    inline for (.{
        &[_][]const u8{ "rm", "--force", "a" },
        &[_][]const u8{ "rm", "-f", "a" },
    }) |argv| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings(
            "zit rm: never needs forcing: files are kept on disk by default; pass --hard to delete from disk\n",
            res.stderr,
        );
    }
}

test "cli rm: 'zit remove'/'zit delete' are aliases of 'rm'" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    // Unknown-command dispatch runs before any repo lookup; a git user's synonym
    // is redirected to the real verb instead of dead-ending (exit 2, stderr).
    // `remove`/`delete` are aliases of `rm` — they reach rm's own
    // usage error rather than a suggestion.
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    inline for (.{ "remove", "delete" }) |cmd| {
        var res = try runZit(gpa, io, tmp.dir, &.{cmd});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "unknown command") == null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "zit rm") != null);
    }
}

// ==========================================================================
// `zit mv OLD NEW` end-to-end tests (contracts/mv.md B-mv-1..18; derived
// manifest bytes from artifact §4.7.6/§4.6). Each spawns the REAL binary in an
// isolated, pinned environment, asserts exit code + exact stdout/stderr, and
// reads `.zit-managed`/the working tree/the minted manifest back through the
// library to prove the state the mv (and the following commit) produced.
// ==========================================================================

/// An independent transcription of contracts/mv.md §Help. Kept separate from the
/// binary's own `mv_help` so the e2e compares two hand-made copies — drift in
/// either is caught (B-mv-12).
const mv_help_expected =
    \\usage: zit mv [-n|--dry-run] [--soft|--hard] OLD... NEW|DIR
    \\
    \\Rename tracked files: move each working file on disk, swap the
    \\tracked-name entries, and record every rename so your next `zit commit`
    \\records the new name with the old one as its former name.
    \\
    \\With two operands and NEW not a directory, OLD becomes NEW. When the last
    \\operand is a directory (on disk, or a tracked prefix, or '.'), or there are
    \\more than two operands, each OLD moves into it keeping its base name. An
    \\OLD that is a tracked directory moves as a unit, every file beneath it
    \\recorded as its own rename.
    \\
    \\Every OLD must be tracked and present; no NEW may already be tracked or
    \\exist on disk. The whole request is checked before anything moves, and
    \\nothing overwrites. mv is not undone by `zit undo`; to reverse a rename,
    \\run `zit mv NEW OLD`.
    \\
    \\options:
    \\  -n, --dry-run print 'would rename OLD -> NEW' per file; change nothing
    \\      --soft    record the renames without touching the working tree (the
    \\                files stay at OLD; status then shows NEW missing until
    \\                you move them)
    \\      --hard    move the files too (the default)
    \\  -h, --help    show this help and exit
    \\
    \\exit status:
    \\  0  the files were renamed
    \\  1  an OLD is not tracked or missing, a NEW is already tracked or on
    \\     disk, or this is not a zit repository
    \\  2  usage error (fewer than two operands, or an unknown option)
    \\
    \\aliases: rename
    \\
;

test "cli mv B-mv-1: happy path moves the file, swaps the managed entry, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "old.txt", "contents\n");
    try addManaged(gpa, io, tmp.dir, &.{"old.txt"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "old.txt", "new.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("RENAME old.txt new.txt\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);

    // On disk: OLD gone, NEW present with the moved bytes.
    try std.testing.expect(!fileExists(io, tmp.dir, "old.txt"));
    try std.testing.expect(fileExists(io, tmp.dir, "new.txt"));
    const moved = try readFile(gpa, io, tmp.dir, "new.txt");
    defer gpa.free(moved);
    try std.testing.expectEqualStrings("contents\n", moved);
    // Managed set: OLD dropped, NEW gained.
    try expectManaged(gpa, io, tmp.dir, &.{"new.txt"});
}

test "cli mv B-mv-2: the managed set names NEW and the file is at NEW (end state consistent)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "x\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "a", "b" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // The persisted managed set names NEW and NOT OLD, and the file sits at NEW —
    // the write-then-move end state (the crash seam is covered by managed unit
    // tests); the two never disagree.
    var set = try managed.ManagedSet.read(gpa, io, tmp.dir);
    defer set.deinit();
    try std.testing.expect(set.contains("b"));
    try std.testing.expect(!set.contains("a"));
    try std.testing.expect(fileExists(io, tmp.dir, "b"));
    try std.testing.expect(!fileExists(io, tmp.dir, "a"));
}

test "cli mv B-mv-3: OLD not tracked refuses, exit 1, nothing moves" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "loose", "y\n"); // on disk but never added

    var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "loose", "dest" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit mv: 'loose' is not tracked\n", res.stderr);
    // Untouched: source still there, dest never created, set still empty.
    try std.testing.expect(fileExists(io, tmp.dir, "loose"));
    try std.testing.expect(!fileExists(io, tmp.dir, "dest"));
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli mv B-mv-4: NEW already tracked refuses, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "1\n");
    try writeFile(io, tmp.dir, "b", "2\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a", "b" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "a", "b" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit mv: 'b' is already tracked\n", res.stderr);
    // No clobber: both still tracked, both files intact with original bytes.
    try expectManaged(gpa, io, tmp.dir, &.{ "a", "b" });
    const bb = try readFile(gpa, io, tmp.dir, "b");
    defer gpa.free(bb);
    try std.testing.expectEqualStrings("2\n", bb);
}

test "cli mv B-mv-5: NEW exists on disk refuses, exit 1, OLD unmoved" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "src\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    try writeFile(io, tmp.dir, "b", "untracked-but-present\n"); // NEW exists on disk, not tracked

    var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "a", "b" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit mv: 'b' already exists on disk\n", res.stderr);
    // OLD unmoved and still tracked; NEW file untouched (never overwritten).
    try expectManaged(gpa, io, tmp.dir, &.{"a"});
    const bb = try readFile(gpa, io, tmp.dir, "b");
    defer gpa.free(bb);
    try std.testing.expectEqualStrings("untracked-but-present\n", bb);
}

test "cli mv B-mv-6: status after mv reports NEW tracked, OLD absent" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "old", "z\n");
    try addManaged(gpa, io, tmp.dir, &.{"old"});
    var mvres = try runZit(gpa, io, tmp.dir, &.{ "mv", "old", "new" });
    defer freeResult(gpa, &mvres);
    try expectExit(mvres.term, 0);

    var st = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &st);
    try expectExit(st.term, 0);
    // status reads the same managed set mv wrote: NEW appears, OLD does not.
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "new") != null);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "old") == null);
}

test "cli mv §4.7.6 B-mv-7: mv then commit emits the four-token old-name F card, sorted under NEW" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // A straddling file "mike" sits alphabetically between OLD ("alpha") and NEW
    // ("yankee"), so the sort order distinguishes current-name from old-name.
    try writeFile(io, tmp.dir, "alpha", "alpha bytes\n");
    try writeFile(io, tmp.dir, "mike", "mike bytes\n");
    try addManaged(gpa, io, tmp.dir, &.{ "alpha", "mike" });
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);

    var mvres = try runZit(gpa, io, tmp.dir, &.{ "mv", "alpha", "yankee" });
    defer freeResult(gpa, &mvres);
    try expectExit(mvres.term, 0);

    var rec = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "rename" });
    defer freeResult(gpa, &rec);
    try expectExit(rec.term, 0);
    const child_hash = try checkinHash(rec.stdout);

    var mc = try parseCheckin(gpa, io, tmp.dir, child_hash);
    defer mc.deinit();
    const f = findF(mc.m, "yankee") orelse return error.MissingFCard;
    // §4.7.6: NEW filename, an explicit `w` perm token (regular-file SHOULD-w so
    // old-name stays in the fourth field), OLD as the fourth (old-name) token,
    // and a `w` token yields regular-file behavior (contains no x/l).
    try std.testing.expectEqualStrings("yankee", f.filename);
    try std.testing.expectEqualStrings("w", f.perm.?);
    try std.testing.expectEqualStrings("alpha", f.old_name.?);
    try std.testing.expectEqual(artifact.FileMode.regular, f.mode);
    // Hash is over the moved (unchanged) bytes now at NEW (§4.7.1 artifact name).
    var namebuf: [64]u8 = undefined;
    const expected_hash = artifact.artifactName(.sha3_256, "alpha bytes\n", &namebuf);
    try std.testing.expectEqualStrings(expected_hash, f.hash.?);
    // §4.6 strict-ascending-by-current-filename: mike (straddle) sorts BEFORE
    // yankee — proving the entry sorts under NEW, not OLD ("alpha").
    try std.testing.expectEqual(@as(usize, 2), mc.m.f.len);
    try std.testing.expectEqualStrings("mike", mc.m.f[0].filename);
    try std.testing.expectEqualStrings("yankee", mc.m.f[1].filename);
}

test "cli mv B-mv-8: the record is one-shot — a later commit emits NEW three-token" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "alpha", "a\n");
    try writeFile(io, tmp.dir, "mike", "m\n");
    try addManaged(gpa, io, tmp.dir, &.{ "alpha", "mike" });
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);
    var mvres = try runZit(gpa, io, tmp.dir, &.{ "mv", "alpha", "yankee" });
    defer freeResult(gpa, &mvres);
    try expectExit(mvres.term, 0);
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "rename" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    // A second commit with NO further mv (a change elsewhere forces a non-no-op).
    try writeFile(io, tmp.dir, "mike", "m edited\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "edit mike" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    const h2 = try checkinHash(c2.stdout);

    var mc = try parseCheckin(gpa, io, tmp.dir, h2);
    defer mc.deinit();
    const f = findF(mc.m, "yankee") orelse return error.MissingFCard;
    // The record was consumed + cleared by the first commit: NEW is now a plain
    // current-name card — no perm, no old-name (§4.7.6 optional positions omitted).
    try std.testing.expectEqual(@as(?[]const u8, null), f.perm);
    try std.testing.expectEqual(@as(?[]const u8, null), f.old_name);
}

test "cli mv B-mv-9: with no rename pending, a commit is byte-identical to a fresh non-mv repo" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: a plain add+commit, no mv anywhere.
    var ta = std.testing.tmpDir(.{});
    defer ta.cleanup();
    try initRepo(gpa, io, ta.dir);
    try writeFile(io, ta.dir, "f", "same bytes\n");
    try addManaged(gpa, io, ta.dir, &.{"f"});
    var ca = try runCommitEnv(gpa, io, ta.dir, .{ .user = "u" }, &.{ "commit", "-m", "msg" });
    defer freeResult(gpa, &ca);
    try expectExit(ca.term, 0);
    const ha = try gpa.dupe(u8, try checkinHash(ca.stdout));
    defer gpa.free(ha);

    // Repo B: identical inputs. Same pinned epoch/user ⇒ identical manifest bytes
    // ⇒ identical check-in hash, proving the no-rename F card is unchanged (three
    // tokens: `F <name> <hash>`), since the mv mechanism never touched this path.
    var tb = std.testing.tmpDir(.{});
    defer tb.cleanup();
    try initRepo(gpa, io, tb.dir);
    try writeFile(io, tb.dir, "f", "same bytes\n");
    try addManaged(gpa, io, tb.dir, &.{"f"});
    var cb = try runCommitEnv(gpa, io, tb.dir, .{ .user = "u" }, &.{ "commit", "-m", "msg" });
    defer freeResult(gpa, &cb);
    try expectExit(cb.term, 0);
    const hb = try checkinHash(cb.stdout);
    try std.testing.expectEqualStrings(ha, hb);

    // And the F card really is three-token (no perm, no old-name).
    var mc = try parseCheckin(gpa, io, ta.dir, ha);
    defer mc.deinit();
    const f = findF(mc.m, "f") orelse return error.MissingFCard;
    try std.testing.expectEqual(@as(?[]const u8, null), f.perm);
    try std.testing.expectEqual(@as(?[]const u8, null), f.old_name);
}

test "cli mv B-mv-10: an uncommitted edit to OLD is preserved and hashed at NEW" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "alpha", "v1\n");
    try addManaged(gpa, io, tmp.dir, &.{"alpha"});
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);

    // Edit OLD after the check-in, THEN rename it.
    try writeFile(io, tmp.dir, "alpha", "v2 edited\n");
    var mvres = try runZit(gpa, io, tmp.dir, &.{ "mv", "alpha", "yankee" });
    defer freeResult(gpa, &mvres);
    try expectExit(mvres.term, 0);
    var rec = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "rename" });
    defer freeResult(gpa, &rec);
    try expectExit(rec.term, 0);
    const child_hash = try checkinHash(rec.stdout);

    var mc = try parseCheckin(gpa, io, tmp.dir, child_hash);
    defer mc.deinit();
    const f = findF(mc.m, "yankee") orelse return error.MissingFCard;
    // The commit captures the CURRENT bytes at NEW (the edit is preserved), with
    // OLD as the old-name token — mv moves, it never reverts or snapshots content.
    var namebuf: [64]u8 = undefined;
    const expected_hash = artifact.artifactName(.sha3_256, "v2 edited\n", &namebuf);
    try std.testing.expectEqualStrings(expected_hash, f.hash.?);
    try std.testing.expectEqualStrings("alpha", f.old_name.?);
    try std.testing.expectEqualStrings("w", f.perm.?);
}

test "cli mv B-mv-11: mv OLD OLD is a refusal, not a self-clobber" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f", "same\n");
    try addManaged(gpa, io, tmp.dir, &.{"f"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "f", "f" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1); // caught by the already-tracked / on-disk checks
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit mv: 'f' is already tracked\n", res.stderr);
    // Nothing changed: still tracked, file intact.
    try expectManaged(gpa, io, tmp.dir, &.{"f"});
    const ff = try readFile(gpa, io, tmp.dir, "f");
    defer gpa.free(ff);
    try std.testing.expectEqualStrings("same\n", ff);
}

test "cli mv B-mv-12: -h and --help print the verbatim help, exit 0, change nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    inline for (.{ "-h", "--help" }) |flag| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a", "x\n");
        try addManaged(gpa, io, tmp.dir, &.{"a"});

        // -h/--help win even over otherwise-valid positionals (B-mv-12).
        var res = try runZit(gpa, io, tmp.dir, &.{ "mv", flag, "a", "b" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(mv_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
        // No mutation: 'a' still tracked and on disk, 'b' never created.
        try expectManaged(gpa, io, tmp.dir, &.{"a"});
        try std.testing.expect(fileExists(io, tmp.dir, "a"));
        try std.testing.expect(!fileExists(io, tmp.dir, "b"));
    }
}

test "cli mv: several files into a directory, a directory rename, -n previews, --soft records without moving, and the commit carries every old name" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDirPath(io, "d");
    try tmp.dir.createDirPath(io, "dest");
    try writeFile(io, tmp.dir, "a", "a\n");
    try writeFile(io, tmp.dir, "b", "b\n");
    try writeFile(io, tmp.dir, "d/x", "x\n");
    try writeFile(io, tmp.dir, "d/y", "y\n");
    try writeFile(io, tmp.dir, "dest/.keep", "");
    try addManaged(gpa, io, tmp.dir, &.{ "a", "b", "d/x", "d/y" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c1" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    // -n names every move and touches nothing.
    var dry = try runZit(gpa, io, tmp.dir, &.{ "mv", "-n", "a", "b", "dest" });
    defer freeResult(gpa, &dry);
    try expectExitDiag(dry.term, 0, dry);
    try std.testing.expectEqualStrings("would rename a -> dest/a\nwould rename b -> dest/b\n", dry.stdout);
    try expectManaged(gpa, io, tmp.dir, &.{ "a", "b", "d/x", "d/y" });
    try expectFileContent(gpa, io, tmp.dir, "a", "a\n");

    // Two files into an existing directory, then a tracked directory as a unit.
    var into = try runZit(gpa, io, tmp.dir, &.{ "mv", "a", "b", "dest" });
    defer freeResult(gpa, &into);
    try expectExitDiag(into.term, 0, into);
    try expectManaged(gpa, io, tmp.dir, &.{ "d/x", "d/y", "dest/a", "dest/b" });
    try expectFileContent(gpa, io, tmp.dir, "dest/b", "b\n");
    var dir = try runZit(gpa, io, tmp.dir, &.{ "mv", "d", "e" });
    defer freeResult(gpa, &dir);
    try expectExitDiag(dir.term, 0, dir);
    try expectManaged(gpa, io, tmp.dir, &.{ "dest/a", "dest/b", "e/x", "e/y" });
    try expectFileContent(gpa, io, tmp.dir, "e/y", "y\n");
    try std.testing.expectError(error.FileNotFound, tmp.dir.access(io, "d/x", .{}));

    // The commit records all four renames: every new name carries its old one.
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "moves" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);
    const child_hash = try checkinHash(c2.stdout);
    var mc = try parseCheckin(gpa, io, tmp.dir, child_hash);
    defer mc.deinit();
    for ([_][]const u8{ "dest/a", "dest/b", "e/x", "e/y" }, [_][]const u8{ "a", "b", "d/x", "d/y" }) |new_name, old_name| {
        const f = findF(mc.m, new_name) orelse return error.MissingFCard;
        try std.testing.expectEqualStrings(old_name, f.old_name.?);
    }
    var st = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &st);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "nothing to report") != null);

    // --soft records the rename and leaves the file where it was.
    var soft = try runZit(gpa, io, tmp.dir, &.{ "mv", "--soft", "e/x", "e/z" });
    defer freeResult(gpa, &soft);
    try expectExitDiag(soft.term, 0, soft);
    try expectManaged(gpa, io, tmp.dir, &.{ "dest/a", "dest/b", "e/y", "e/z" });
    try expectFileContent(gpa, io, tmp.dir, "e/x", "x\n");
    try std.testing.expectError(error.FileNotFound, tmp.dir.access(io, "e/z", .{}));
}

test "cli add -f: expansion always skips ignore-glob matches; a named match needs --force" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDirPath(io, ".zit-settings");
    try tmp.dir.createDirPath(io, "src");
    try writeFile(io, tmp.dir, ".zit-settings/ignore-glob", "*.o\n");
    try writeFile(io, tmp.dir, ".zit-settings/clean-glob", "*.tmp\n");
    try writeFile(io, tmp.dir, "src/m.c", "c\n");
    try writeFile(io, tmp.dir, "src/m.o", "o\n");
    try writeFile(io, tmp.dir, "src/m.tmp", "t\n");
    // Expansion: both glob lists narrow it, forced or not (fossil add.c).
    var forced_dir = try runZit(gpa, io, tmp.dir, &.{ "add", "-f", "src" });
    defer freeResult(gpa, &forced_dir);
    try expectExitDiag(forced_dir.term, 0, forced_dir);
    try expectManaged(gpa, io, tmp.dir, &.{"src/m.c"});
    // A named ignore-glob match is skipped with a note, exit 0; a named
    // clean-glob match is added silently, as fossil does.
    var named = try runZit(gpa, io, tmp.dir, &.{ "add", "src/m.o", "src/m.tmp" });
    defer freeResult(gpa, &named);
    try expectExitDiag(named.term, 0, named);
    try std.testing.expect(std.mem.indexOf(u8, named.stderr, "'src/m.o' matches ignore-glob; use --force to add it") != null);
    try expectManaged(gpa, io, tmp.dir, &.{ "src/m.c", "src/m.tmp" });
    var forced = try runZit(gpa, io, tmp.dir, &.{ "add", "-f", "src/m.o" });
    defer freeResult(gpa, &forced);
    try expectExitDiag(forced.term, 0, forced);
    try std.testing.expectEqualStrings("", forced.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{ "src/m.c", "src/m.o", "src/m.tmp" });
}

test "cli mv B-mv-13: an unknown option is a usage error, exit 2, stderr only" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    inline for (.{
        &[_][]const u8{ "mv", "--force", "a", "b" },
        &[_][]const u8{ "mv", "-f", "a", "b" },
    }) |argv| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "unknown option '" ++ argv[1] ++ "'") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "try 'zit mv --help'") != null);
    }
}

test "cli mv B-mv-14: the wrong operand count is a usage error, exit 2, no mutation" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    inline for (.{
        &[_][]const u8{"mv"}, // zero operands
        &[_][]const u8{ "mv", "a" }, // one operand
    }) |argv| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a", "x\n");
        try addManaged(gpa, io, tmp.dir, &.{"a"});

        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "try 'zit mv --help'") != null);
        // No mutation regardless of which count failed.
        try expectManaged(gpa, io, tmp.dir, &.{"a"});
    }
}

test "cli mv B-mv-15: outside a repository is an expected failure, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try touch(io, tmp.dir, "a"); // a file, but no repo anywhere above

    var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "a", "b" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit: no zit checkout here (looked for .zit-checkout in this directory and its parents)\n" ++
            "if this is a bare repository, name it with -R, or run 'zit open' to create a working tree\n",
        res.stderr,
    );
}

test "cli mv B-mv-16: a corrupt .zit-managed fails cleanly before any move, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "x\n");
    try corruptSection(io, tmp.dir, .managed);

    var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "a", "b" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit mv: cannot read tracked files: .zit-checkout is corrupt\n",
        res.stderr,
    );
    // Nothing moved: source intact, dest never created.
    try std.testing.expect(fileExists(io, tmp.dir, "a"));
    try std.testing.expect(!fileExists(io, tmp.dir, "b"));
}

test "cli mv B-mv-17: streams stay separated under redirection (success vs refusal)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "x\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});

    // Success: the confirmation goes to stdout only; stderr stays empty.
    var ok = try runShell(gpa, io, tmp.dir, "\"$ZIT\" mv a b > out.txt 2> err.txt");
    defer freeResult(gpa, &ok);
    try expectExit(ok.term, 0);
    const ok_out = try readFile(gpa, io, tmp.dir, "out.txt");
    defer gpa.free(ok_out);
    const ok_err = try readFile(gpa, io, tmp.dir, "err.txt");
    defer gpa.free(ok_err);
    try std.testing.expectEqualStrings("RENAME a b\n", ok_out);
    try std.testing.expectEqualStrings("", ok_err);

    // Refusal: the diagnostic goes to stderr only; stdout stays empty.
    var bad = try runShell(gpa, io, tmp.dir, "\"$ZIT\" mv missing dest > out2.txt 2> err2.txt");
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 1);
    const bad_out = try readFile(gpa, io, tmp.dir, "out2.txt");
    defer gpa.free(bad_out);
    const bad_err = try readFile(gpa, io, tmp.dir, "err2.txt");
    defer gpa.free(bad_err);
    try std.testing.expectEqualStrings("", bad_out);
    try std.testing.expectEqualStrings("zit mv: 'missing' is not tracked\n", bad_err);
}

// ==========================================================================
// `zit merge` end-to-end tests (contracts/merge.md B-merge-1..29). Each spawns
// the REAL binary in an isolated, pinned environment and builds its DAG through
// real check-ins (commit → update → commit forks a diamond), then drives
// `zit merge` and asserts exit code + stdout/stderr + on-disk state read back
// through the library. Cases that need the commit P-card extension (the next,
// separate component) — B-merge-8/9/10/11/12 — are deferred to that slice.
// ==========================================================================

/// The verbatim `zit merge` help — must match `main.zig`'s `merge_help`
/// byte-for-byte (drift fails B-merge-25).
const merge_help_expected =
    \\usage: zit merge [-X ours|theirs] [--no-sync] [--no-precedent]
    \\                 [--baseline B] [--integrate] [NAME]
    \\       zit merge --probe|-n|--dry-run NAME... [--into TARGET]
    \\       zit merge --cherrypick NAME
    \\       zit merge --backout NAME
    \\
    \\Merge the check-in NAME into the working tree with a three-way merge and
    \\record it as a pending merge parent. NAME resolves like 'zit info' — a full
    \\or abbreviated artifact ID, a branch, or a tag; with no NAME, the one
    \\other open leaf is merged (several is an error naming them). This does
    \\not create a check-in; run 'zit commit' to record the merge. Conflicts are written into
    \\the working files with conflict markers and listed on stderr — resolve them
    \\and commit, or run 'zit undo' to abandon the merge.
    \\
    \\Uncommitted work does not block the merge: your working file is the merge's
    \\own "current" side, so a region only you changed stays yours and a region
    \\only NAME changed is applied around it. Where the two collide you get
    \\markers, the same as any other conflict, and 'zit undo' restores the
    \\uncommitted bytes.
    \\
    \\-X resolves every CONTENT conflict in favour of one side instead of writing
    \\markers: 'ours' keeps the working tree's version wherever the two sides
    \\disagree, 'theirs' takes NAME's. Regions only one side changed are still
    \\that side's either way -- -X answers only what the merge could not, so it
    \\is not "discard their work", it is "stop asking me". The mechanical cases
    \\are what it is for: a regenerated lockfile, a vendored tree taken whole
    \\from upstream.
    \\
    \\--probe answers "would this merge cleanly?" and writes NOTHING -- no merge,
    \\no working tree touched, no autosync. It prints one line, `merges` followed
    \\by clean, up-to-date, unrelated, or conflict (N), and then one
    \\`conflict PATH` line per unresolvable path. Exit status is 0 in every one
    \\of those cases: "has conflicts" is an answer, not a failure, so a caller
    \\reads the line rather than the status.
    \\
    \\Given several NAMEs, --probe answers a STACK: each NAME against TARGET
    \\with the NAMEs before it already landed, in memory, in order. One line
    \\per NAME -- `merges NAME` and its verdict -- with `conflict PATH` lines
    \\under the one that stops, and `not-reached` for every NAME after it: a
    \\merge that did not complete leaves nothing to continue from. This is
    \\not the same as probing each NAME against the one below it: that says
    \\nothing about what TARGET changed meanwhile, so it can call a rung
    \\clean that stops the landing.
    \\
    \\Being a read, --probe takes -R and needs no checkout -- but then there is
    \\no current check-in to merge into, so --into TARGET is required. In a
    \\checkout it defaults to the check-in you are on, exactly as a real merge
    \\takes its other side. A real merge refuses -R: it writes a working tree.
    \\
    \\-X ours IS NOT --backout. --backout reverses NAME's change; -X ours keeps
    \\your side where the two collide and reverses nothing. They read alike and
    \\do different things, which is the misread worth avoiding.
    \\
    \\One case -X deliberately does not answer: a file one side modified and the
    \\other deleted. That is a disagreement about whether the file should exist,
    \\not about its contents, and either reading silently destroys work -- so it
    \\stays a conflict for you to settle, with or without -X.
    \\
    \\--cherrypick and --backout are special merges that pick an alternate
    \\baseline instead of the common ancestor: --cherrypick applies only NAME's
    \\own change onto the working tree, --backout reverses it. 'zit commit' then
    \\records the operation as a cherry-pick/backout, not as a merge parent.
    \\
    \\options:
    \\  -h, --help          show this help and exit
    \\  -X, --strategy ours|theirs
    \\                      resolve content conflicts in favour of one side
    \\  --no-sync           skip the autosync pull for this invocation
    \\  --no-precedent      do not replay remembered conflict resolutions for
    \\                      this merge (see 'zit precedent'); nothing is
    \\                      forgotten, only declined
    \\  --cherrypick NAME   apply only NAME's own change onto the working tree
    \\  --backout NAME      reverse NAME's change on the working tree
    \\  -n, --dry-run       the same as --probe
    \\  --baseline B        use check-in B as the common ancestor instead of
    \\                      the computed merge base
    \\  --integrate         close NAME's leaf when this merge is committed
    \\
    \\exit status:
    \\  0  success
    \\  1  operational failure (not a repository, unreadable state, a refused
    \\     action) — the diagnostic on stderr names it
    \\  2  usage error (an unknown option or unexpected argument)
    \\
;

/// Commits the current working tree (message `msg`, pinned author/epoch), asserts
/// exit 0, and returns the new check-in's 64-hex hash copied onto `gpa`.
fn commitHashDup(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, msg: []const u8) ![]const u8 {
    var res = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", msg });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const h = try checkinHash(res.stdout);
    return gpa.dupe(u8, h);
}

/// `commitHashDup` with `--allow-fork`: for fixtures that DELIBERATELY build
/// a second head (merge diamonds, leaves/info fork scenes). The plain helper
/// stays guard-checked so ordinary fixture commits keep proving the fork-guard
/// refusal never fires on linear history.
fn commitForkDup(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, msg: []const u8) ![]const u8 {
    var res = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", msg, "--allow-fork" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const h = try checkinHash(res.stdout);
    return gpa.dupe(u8, h);
}

const MergeDiamond = struct {
    base: []const u8,
    other: []const u8,
    current: []const u8,
    fn deinit(self: *MergeDiamond, gpa: std.mem.Allocator) void {
        gpa.free(self.base);
        gpa.free(self.other);
        gpa.free(self.current);
    }
};

/// Builds a diamond over one file `poem`: root(base_content) → other(other_content)
/// on trunk, then `update base` and commit current(current_content). The two
/// leaves share `base` as their §3.3 merge base. On return the checkout sits at
/// `current` on a clean trunk with `poem` == current_content — ready to merge.
fn buildMergeDiamond(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    base_content: []const u8,
    other_content: []const u8,
    current_content: []const u8,
) !MergeDiamond {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "poem", base_content);
    try addManaged(gpa, io, dir, &.{"poem"});
    const base = try commitHashDup(gpa, io, dir, "root");
    errdefer gpa.free(base);

    try writeFile(io, dir, "poem", other_content);
    const other = try commitHashDup(gpa, io, dir, "other");
    errdefer gpa.free(other);

    var up = try runZit(gpa, io, dir, &.{ "update", base });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);

    try writeFile(io, dir, "poem", current_content);
    const current = try commitForkDup(gpa, io, dir, "current");
    return .{ .base = base, .other = other, .current = current };
}

/// Reads `.zit-checkout` and asserts its version and merge-parent fields.
fn expectMergeState(io: std.Io, dir: std.Io.Dir, version: []const u8, merge_parent: []const u8) !void {
    var ckbuf: [checkout.max_ck_payload + 1]u8 = undefined;
    const ck = (try checkout.read(io, dir, &ckbuf)).?;
    try std.testing.expectEqualStrings(version, ck.version);
    try std.testing.expectEqualStrings(merge_parent, ck.merge_parent);
}

test "cli merge B-merge-1/-2/-3: clean disjoint merge lands both sides, records the parent, mints no check-in" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Disjoint edits: current changed line 1, other changed line 3.
    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
    defer fx.deinit(gpa);

    // B-merge-2 oracle: `.zit` is byte-identical across the merge — no blob and
    // no manifest is appended, so the merge is provably not a check-in.
    const zit_before = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(zit_before);

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    // B-merge-1: the three-way result lands byte-for-byte, exit 0, one line to
    // stdout naming the other side and the changed-file count; stderr empty.
    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expectEqualStrings("A1\nB\nC1\n", poem);
    var line_buf: [256]u8 = undefined;
    const want_line = try std.fmt.bufPrint(&line_buf, "merged {s} into the working tree (1 file(s) changed)\n", .{fx.other});
    try std.testing.expectEqualStrings(want_line, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);

    // B-merge-1: the merge parent is recorded; the version is unchanged (no
    // advance — merge does not mint).
    try expectMergeState(io, tmp.dir, fx.current, fx.other);

    // B-merge-2: `.zit` unchanged.
    const zit_after = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(zit_after);
    try std.testing.expectEqualSlices(u8, zit_before, zit_after);

    // B-merge-3: the undo journal is armed (present after the merge).
    try std.testing.expect(sectionExists(gpa, io, tmp.dir, .undo));
}

test "cli merge B-merge-5: a disjoint add/delete file set merges from the §8.2 walk" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Root with two files. Other adds `their`; current adds `ours`. The merge
    // must union both additions (each added on exactly one side).
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "shared", "base\n");
    try addManaged(gpa, io, tmp.dir, &.{"shared"});
    const base = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(base);

    try writeFile(io, tmp.dir, "their", "theirs\n");
    try addManaged(gpa, io, tmp.dir, &.{"their"});
    const other = try commitHashDup(gpa, io, tmp.dir, "other-adds");
    defer gpa.free(other);

    var up = try runZit(gpa, io, tmp.dir, &.{ "update", base });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);
    try writeFile(io, tmp.dir, "ours", "ours\n");
    try addManaged(gpa, io, tmp.dir, &.{"ours"});
    const current = try commitForkDup(gpa, io, tmp.dir, "current-adds");
    defer gpa.free(current);

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", other });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    // `their` materialized from the other side; `ours`/`shared` retained.
    const their = try readFile(gpa, io, tmp.dir, "their");
    defer gpa.free(their);
    try std.testing.expectEqualStrings("theirs\n", their);
    try std.testing.expect(fileExists(io, tmp.dir, "ours"));
    try std.testing.expect(fileExists(io, tmp.dir, "shared"));
    try expectMergeState(io, tmp.dir, current, other);
}

test "cli merge B-merge-7: an overlapping edit conflicts — markers in-tree, path on stderr, exit 1, parent recorded" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Both sides change the SAME line differently → a content conflict.
    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nY\nC\n", "A\nX\nC\n");
    defer fx.deinit(gpa);

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);

    // Conflict surfacing: stdout empty; each conflicted path plus a summary on
    // stderr.
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "zit merge: conflict in poem\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "1 conflict(s); resolve and commit, or run 'zit undo'") != null);

    // The file carries fossil-style diff3 markers.
    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expect(std.mem.indexOf(u8, poem, "<<<<<<< current\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, poem, "||||||| base\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, poem, "=======\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, poem, ">>>>>>> other\n") != null);

    // The merge is NOT abandoned — the parent is recorded despite the conflict.
    try expectMergeState(io, tmp.dir, fx.current, fx.other);
}

test "cli merge B-merge-19: delete-vs-modify is a whole-file conflict" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Root has `poem`. Other modifies it; current deletes it (rm + commit).
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const base = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(base);

    try writeFile(io, tmp.dir, "poem", "A\nB\nMODIFIED\n");
    const other = try commitHashDup(gpa, io, tmp.dir, "other-modifies");
    defer gpa.free(other);

    var up = try runZit(gpa, io, tmp.dir, &.{ "update", base });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);
    var rmres = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "poem" });
    defer freeResult(gpa, &rmres);
    try expectExit(rmres.term, 0);
    const current = try commitForkDup(gpa, io, tmp.dir, "current-deletes");
    defer gpa.free(current);

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", other });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "zit merge: conflict in poem\n") != null);
    // The conflicting path is written with markers (never silently resolved).
    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expect(std.mem.indexOf(u8, poem, "<<<<<<< current\n") != null);
    try expectMergeState(io, tmp.dir, current, other);
}

test "cli commit B-merge-8/-9/-11/-12: a merge commit mints the two-token P card, clears it, and round-trips" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // A clean disjoint diamond → the merge is conflict-free, so `zit commit`
    // records the merge check-in.
    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
    defer fx.deinit(gpa);

    var mg = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &mg);
    try expectExit(mg.term, 0);
    try expectMergeState(io, tmp.dir, fx.current, fx.other);

    // B-merge-8: exactly one P card, two tokens — the pre-merge checkout (the
    // primary ancestor) first, the merge parent second.
    const merge_commit = try commitHashDup(gpa, io, tmp.dir, "merge poem");
    defer gpa.free(merge_commit);

    var stored = try parseCheckin(gpa, io, tmp.dir, merge_commit);
    defer stored.deinit();
    var pcard_buf: [160]u8 = undefined;
    const want_pcard = try std.fmt.bufPrint(&pcard_buf, "P {s} {s}\n", .{ fx.current, fx.other });
    try std.testing.expect(std.mem.indexOf(u8, stored.bytes, want_pcard) != null);
    try std.testing.expectEqual(@as(usize, 1), stored.m.p.len);
    try std.testing.expectEqual(@as(usize, 2), stored.m.p[0].count);

    // B-merge-11: a baseline manifest — no B card — whose F set is the full
    // reconstructed file set (here the single merged file), never a delta.
    try std.testing.expect(stored.m.b == null);
    try std.testing.expectEqual(@as(usize, 1), stored.m.f.len);
    try std.testing.expectEqualStrings("poem", stored.m.f[0].filename);

    // B-merge-9: the commit consumes the pending merge — the checkout advances to
    // the merge check-in with the merge parent cleared (one-shot).
    try expectMergeState(io, tmp.dir, merge_commit, "");

    // B-merge-12: `zit info` renders the merge parent through the existing
    // `renderMergeParents` path (a `merge:` line naming the other side), with the
    // pre-merge checkout still shown as the primary `parent:` line.
    var info = try runZit(gpa, io, tmp.dir, &.{ "info", merge_commit });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    var pline: [160]u8 = undefined;
    const want_parent_line = try std.fmt.bufPrint(&pline, "parent:  {s}\n", .{fx.current});
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, want_parent_line) != null);
    var mline: [160]u8 = undefined;
    const want_merge_line = try std.fmt.bufPrint(&mline, "merge:   {s}\n", .{fx.other});
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, want_merge_line) != null);

    // B-merge-9: a second commit with no fresh `zit merge` is an ordinary
    // single-parent check-in — the two-token path does not persist.
    try writeFile(io, tmp.dir, "poem", "A1\nB\nC1\nD\n");
    const next = try commitHashDup(gpa, io, tmp.dir, "follow-up");
    defer gpa.free(next);
    var stored2 = try parseCheckin(gpa, io, tmp.dir, next);
    defer stored2.deinit();
    try std.testing.expectEqual(@as(usize, 1), stored2.m.p.len);
    try std.testing.expectEqual(@as(usize, 1), stored2.m.p[0].count);
    try std.testing.expectEqualStrings(merge_commit, stored2.m.p[0].parents.?);
}

test "cli commit B-merge-10: a merge with unresolved conflict markers refuses, exit 1, mints nothing, keeps the merge pending" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Overlapping edits → a content conflict; markers land in `poem`.
    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nY\nC\n", "A\nX\nC\n");
    defer fx.deinit(gpa);

    var mg = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &mg);
    try expectExit(mg.term, 1); // a conflict merge; the parent is recorded anyway

    // The commit refuses while the marker is still in the working file.
    var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "try merge" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "zit commit: unresolved conflict in poem\n") != null);

    // Nothing minted: the checkout is un-advanced and the merge stays pending.
    try expectMergeState(io, tmp.dir, fx.current, fx.other);

    // Resolving the file (markers removed) lets the same commit through, and it
    // still records the two-token merge P card and clears the pending merge.
    try writeFile(io, tmp.dir, "poem", "A\nRESOLVED\nC\n");
    const resolved = try commitHashDup(gpa, io, tmp.dir, "resolved merge");
    defer gpa.free(resolved);
    var stored = try parseCheckin(gpa, io, tmp.dir, resolved);
    defer stored.deinit();
    try std.testing.expectEqual(@as(usize, 1), stored.m.p.len);
    try std.testing.expectEqual(@as(usize, 2), stored.m.p[0].count);
    try expectMergeState(io, tmp.dir, resolved, "");
}

test "cli commit B-merge-9 negative space: a non-merge commit is byte-identical across independent repos (no P-card regression)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Two independent repos built through the SAME pinned-epoch, non-merge
    // sequence must produce byte-for-byte identical child manifests, and each
    // child carries a single-token P card — proof the merge extension never
    // touches a commit with no pending merge.
    const build = struct {
        fn run(g: std.mem.Allocator, ioo: std.Io, dir: std.Io.Dir) ![]u8 {
            try initRepo(g, ioo, dir);
            try writeFile(ioo, dir, "poem", "A\nB\nC\n");
            try addManaged(g, ioo, dir, &.{"poem"});
            var r0 = try runCommitEnv(g, ioo, dir, .{ .user = "u", .epoch = "1000000000" }, &.{ "commit", "-m", "root" });
            defer freeResult(g, &r0);
            try expectExit(r0.term, 0);
            try writeFile(ioo, dir, "poem", "A\nB\nC1\n");
            var r1 = try runCommitEnv(g, ioo, dir, .{ .user = "u", .epoch = "1000000000" }, &.{ "commit", "-m", "child" });
            defer freeResult(g, &r1);
            try expectExit(r1.term, 0);
            const h = try checkinHash(r1.stdout);
            var stored = try parseCheckin(g, ioo, dir, h);
            defer stored.deinit();
            try std.testing.expectEqual(@as(usize, 1), stored.m.p.len);
            try std.testing.expectEqual(@as(usize, 1), stored.m.p[0].count);
            return g.dupe(u8, stored.bytes);
        }
    };

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();

    const a = try build.run(gpa, io, tmp_a.dir);
    defer gpa.free(a);
    const b = try build.run(gpa, io, tmp_b.dir);
    defer gpa.free(b);
    try std.testing.expectEqualSlices(u8, a, b);
}

test "cli merge B-merge-13: undo restores the pre-merge files and clears the merge parent" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nY\nC\n", "A\nX\nC\n");
    defer fx.deinit(gpa);

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1); // a conflict merge, but still recorded

    var undo = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &undo);
    try expectExit(undo.term, 0);

    // The working file is back to the pre-merge current content, and the merge
    // parent is cleared — the tree is indistinguishable from before the merge.
    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expectEqualStrings("A\nX\nC\n", poem);
    try expectMergeState(io, tmp.dir, fx.current, "");
}

/// Whether `name` has any execute bit set.
fn isExecutableFile(io: std.Io, dir: std.Io.Dir, name: []const u8) !bool {
    const st = try dir.statFile(io, name, .{});
    return st.permissions.toMode() & 0o111 != 0;
}

/// A repository in `repo/` with trunk and a branch `feat` that merge
/// cleanly: each side edits a different line of a.txt, feat also makes run.sh
/// executable, adds new.txt and removes gone.txt.
fn seedMergeTreeRepo(gpa: std.mem.Allocator, io: std.Io, repo: std.Io.Dir) !void {
    try initRepo(gpa, io, repo);
    try writeFile(io, repo, "a.txt", "one\ntwo\nthree\n");
    try writeFile(io, repo, "run.sh", "echo hi\n");
    try writeFile(io, repo, "keep.txt", "keep\n");
    try writeFile(io, repo, "gone.txt", "gone\n");
    try addManaged(gpa, io, repo, &.{ "a.txt", "run.sh", "keep.txt", "gone.txt" });
    var root = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);

    try writeFile(io, repo, "a.txt", "ONE\ntwo\nthree\n");
    try chmodFile(io, repo, "run.sh", 0o755);
    try writeFile(io, repo, "new.txt", "new\n");
    try addManaged(gpa, io, repo, &.{"new.txt"});
    var rm = try runZit(gpa, io, repo, &.{ "rm", "gone.txt" });
    defer freeResult(gpa, &rm);
    try expectExitDiag(rm.term, 0, rm);
    try repo.deleteFile(io, "gone.txt"); // rm untracks; the file stays, as in Fossil
    var feat = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "feat work", "--branch", "feat" });
    defer freeResult(gpa, &feat);
    try expectExitDiag(feat.term, 0, feat);

    var back = try runZit(gpa, io, repo, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);
    try writeFile(io, repo, "a.txt", "one\ntwo\nTHREE\n");
    var edit = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "trunk work" });
    defer freeResult(gpa, &edit);
    try expectExitDiag(edit.term, 0, edit);
}

/// `a` and `b` hold the same files: same names, bytes and execute bits.
fn expectSameTree(gpa: std.mem.Allocator, io: std.Io, a: std.Io.Dir, b: std.Io.Dir, names: []const []const u8) !void {
    for (names) |name| {
        const x = try readFile(gpa, io, a, name);
        defer gpa.free(x);
        const y = try readFile(gpa, io, b, name);
        defer gpa.free(y);
        std.testing.expectEqualStrings(x, y) catch |e| {
            std.debug.print("\ndiffers: {s}\n", .{name});
            return e;
        };
        try std.testing.expectEqual(try isExecutableFile(io, a, name), try isExecutableFile(io, b, name));
    }
}

test "cli merge-tree: a clean pair writes the tree zit merge leaves, modes included, and the store is unchanged" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try seedMergeTreeRepo(gpa, io, repo);
    const before = try storeBytesAlloc(gpa, io, repo);
    defer gpa.free(before);

    var mt = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "feat", "--into", "trunk", "--out", "mt", "-R", "repo" });
    defer freeResult(gpa, &mt);
    try expectExitDiag(mt.term, 0, mt);
    // Two runs of one pair give one archive, byte for byte.
    var tar1 = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "feat", "--into", "trunk", "--tar", "-R", "repo" });
    defer freeResult(gpa, &tar1);
    try expectExitDiag(tar1.term, 0, tar1);
    var tar2 = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "feat", "--into", "trunk", "--tar", "-R", "repo" });
    defer freeResult(gpa, &tar2);
    try std.testing.expectEqualSlices(u8, tar1.stdout, tar2.stdout);
    {
        const after = try storeBytesAlloc(gpa, io, repo);
        defer gpa.free(after);
        try std.testing.expectEqualSlices(u8, before, after);
    }

    // The same merge in a checkout of trunk.
    var merged = try runZit(gpa, io, repo, &.{ "merge", "feat" });
    defer freeResult(gpa, &merged);
    try expectExitDiag(merged.term, 0, merged);
    var out = try tmp.dir.openDir(io, "mt", .{ .iterate = true });
    defer out.close(io);
    const names = [_][]const u8{ "a.txt", "keep.txt", "new.txt", "run.sh" };
    try expectSameTree(gpa, io, repo, out, &names);
    try std.testing.expect(try isExecutableFile(io, out, "run.sh"));
    try std.testing.expectError(error.FileNotFound, out.statFile(io, "gone.txt", .{}));
    var count: usize = 0;
    var walk = out.iterate();
    while (try walk.next(io)) |_| count += 1;
    try std.testing.expectEqual(names.len, count);

    // The archive unpacks to the same tree.
    try writeFile(io, tmp.dir, "mt.tar.gz", tar1.stdout);
    try tmp.dir.createDir(io, "untar", .default_dir);
    var untar = try tmp.dir.openDir(io, "untar", .{});
    defer untar.close(io);
    var tar_x = try std.process.run(gpa, io, .{ .argv = &.{ "/usr/bin/tar", "-xzf", "../mt.tar.gz" }, .cwd = .{ .dir = untar } });
    defer freeResult(gpa, &tar_x);
    try expectExitDiag(tar_x.term, 0, tar_x);
    try expectSameTree(gpa, io, out, untar, &names);
}

test "cli merge-tree: a conflicting pair exits 5, lists every conflicting path, and writes nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try initRepo(gpa, io, repo);
    try writeFile(io, repo, "a.txt", "base\n");
    try writeFile(io, repo, "b.txt", "base\n");
    try writeFile(io, repo, "c.txt", "base\n");
    try addManaged(gpa, io, repo, &.{ "a.txt", "b.txt", "c.txt" });
    var root = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);
    try writeFile(io, repo, "a.txt", "feat\n");
    try writeFile(io, repo, "b.txt", "feat\n");
    var feat = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "feat", "--branch", "feat" });
    defer freeResult(gpa, &feat);
    try expectExitDiag(feat.term, 0, feat);
    var back = try runZit(gpa, io, repo, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);
    try writeFile(io, repo, "a.txt", "trunk\n");
    try writeFile(io, repo, "b.txt", "trunk\n");
    try writeFile(io, repo, "c.txt", "trunk\n");
    var edit = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "trunk" });
    defer freeResult(gpa, &edit);
    try expectExitDiag(edit.term, 0, edit);
    const before = try storeBytesAlloc(gpa, io, repo);
    defer gpa.free(before);

    var mt = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "feat", "--into", "trunk", "--out", "mt", "-R", "repo" });
    defer freeResult(gpa, &mt);
    try expectExit(mt.term, 5);
    try std.testing.expectEqualStrings("a.txt\nb.txt\n", mt.stdout);
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "mt", .{}));
    var tar = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "feat", "--into", "trunk", "--tar", "-R", "repo" });
    defer freeResult(gpa, &tar);
    try expectExit(tar.term, 5);
    try std.testing.expectEqualStrings("a.txt\nb.txt\n", tar.stdout);
    const after = try storeBytesAlloc(gpa, io, repo);
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);

    // Already in the target: the target's own tree, said on stderr.
    var root_in = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "feat", "--into", "feat", "--out", "same", "-R", "repo" });
    defer freeResult(gpa, &root_in);
    try expectExitDiag(root_in.term, 0, root_in);
    try std.testing.expect(std.mem.indexOf(u8, root_in.stderr, "already in") != null);
    var same = try tmp.dir.openDir(io, "same", .{});
    defer same.close(io);
    const a = try readFile(gpa, io, same, "a.txt");
    defer gpa.free(a);
    try std.testing.expectEqualStrings("feat\n", a);
}

/// A repository in `repo/` where trunk and `feat` conflict every way a path
/// can: w.go region by region (feat changes A and C, trunk changes C and
/// appends a function), logo.bin binary on both, old.txt deleted on feat and
/// edited on trunk, add.txt added by both.
fn seedConflictRepo(gpa: std.mem.Allocator, io: std.Io, repo: std.Io.Dir) !void {
    try initRepo(gpa, io, repo);
    try writeFile(io, repo, "w.go", "package w\ntype T struct {\n\tA int\n\tB int\n\tC int\n}\n");
    try writeFile(io, repo, "logo.bin", "\x00base");
    try writeFile(io, repo, "old.txt", "old\n");
    try writeFile(io, repo, "keep.txt", "keep\n");
    try addManaged(gpa, io, repo, &.{ "w.go", "logo.bin", "old.txt", "keep.txt" });
    var root = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);

    try writeFile(io, repo, "w.go", "package w\ntype T struct {\n\tA int64\n\tB int\n\tC bool\n}\n");
    try writeFile(io, repo, "logo.bin", "\x00feat");
    try writeFile(io, repo, "add.txt", "feat\n");
    try addManaged(gpa, io, repo, &.{"add.txt"});
    var rm = try runZit(gpa, io, repo, &.{ "rm", "old.txt" });
    defer freeResult(gpa, &rm);
    try expectExitDiag(rm.term, 0, rm);
    try repo.deleteFile(io, "old.txt");
    var feat = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "feat work", "--branch", "feat" });
    defer freeResult(gpa, &feat);
    try expectExitDiag(feat.term, 0, feat);

    var back = try runZit(gpa, io, repo, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);
    try writeFile(io, repo, "w.go", "package w\ntype T struct {\n\tA int\n\tB int\n\tC string\n}\nfunc f() {}\n");
    try writeFile(io, repo, "logo.bin", "\x00trunk");
    try writeFile(io, repo, "old.txt", "old, edited\n");
    try writeFile(io, repo, "add.txt", "trunk\n");
    try addManaged(gpa, io, repo, &.{"add.txt"});
    var edit = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "trunk work" });
    defer freeResult(gpa, &edit);
    try expectExitDiag(edit.term, 0, edit);
}

/// One region of the -F json document, as `[kind, base, current, other]`.
fn expectJsonRegion(v: std.json.Value, kind: []const u8, b: [2]i64, c: [2]i64, o: [2]i64) !void {
    try std.testing.expectEqualStrings(kind, v.object.get("kind").?.string);
    inline for (.{ "base", "current", "other" }, .{ b, c, o }) |side, want| {
        const arr = v.object.get(side).?.array.items;
        try std.testing.expectEqual(want[0], arr[0].integer);
        try std.testing.expectEqual(want[1], arr[1].integer);
    }
}

fn jsonFile(doc: std.json.Value, path: []const u8) ?std.json.Value {
    for (doc.object.get("files").?.array.items) |f| {
        if (std.mem.eql(u8, f.object.get("path").?.string, path)) return f;
    }
    return null;
}

test "cli merge-tree -F json: every conflicting file, its blobs and modes, and the regions the merge found" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try seedConflictRepo(gpa, io, repo);
    const before = try storeBytesAlloc(gpa, io, repo);
    defer gpa.free(before);

    // The author's direction: trunk merged into feat, so current is feat.
    var mt = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "-F", "json", "-R", "repo" });
    defer freeResult(gpa, &mt);
    try expectExit(mt.term, 5);
    const parsed = try std.json.parseFromSlice(std.json.Value, gpa, mt.stdout, .{});
    defer parsed.deinit();
    const doc = parsed.value;
    try std.testing.expectEqualStrings("conflict", doc.object.get("verdict").?.string);
    for ([_][]const u8{ "base", "current", "other" }) |k| try std.testing.expectEqual(@as(usize, 64), doc.object.get(k).?.string.len);
    const files = doc.object.get("files").?.array.items;
    const want_paths = [_][]const u8{ "add.txt", "logo.bin", "old.txt", "w.go" };
    const want_kinds = [_][]const u8{ "add-add", "binary", "delete-modify", "text" };
    try std.testing.expectEqual(want_paths.len, files.len);
    for (files, want_paths, want_kinds) |f, p, k| {
        try std.testing.expectEqualStrings(p, f.object.get("path").?.string);
        try std.testing.expectEqualStrings(k, f.object.get("kind").?.string);
        try std.testing.expectEqual(std.mem.eql(u8, k, "text"), f.object.get("regions") != null);
    }
    const add = jsonFile(doc, "add.txt").?;
    try std.testing.expect(add.object.get("base").? == .null);
    try std.testing.expect(add.object.get("mode").?.object.get("base").? == .null);
    const old = jsonFile(doc, "old.txt").?;
    try std.testing.expect(old.object.get("current").? == .null);
    try std.testing.expectEqualStrings("regular", old.object.get("mode").?.object.get("other").?.string);
    const w = jsonFile(doc, "w.go").?;
    const regions = w.object.get("regions").?.array.items;
    try std.testing.expectEqual(@as(usize, 6), regions.len);
    try expectJsonRegion(regions[0], "same", .{ 0, 2 }, .{ 0, 2 }, .{ 0, 2 });
    try expectJsonRegion(regions[1], "current", .{ 2, 3 }, .{ 2, 3 }, .{ 2, 3 });
    try expectJsonRegion(regions[2], "same", .{ 3, 4 }, .{ 3, 4 }, .{ 3, 4 });
    try expectJsonRegion(regions[3], "conflict", .{ 4, 5 }, .{ 4, 5 }, .{ 4, 5 });
    try expectJsonRegion(regions[4], "same", .{ 5, 6 }, .{ 5, 6 }, .{ 5, 6 });
    try expectJsonRegion(regions[5], "other", .{ 6, 6 }, .{ 6, 6 }, .{ 6, 7 });
    // The blob hashes name the exact bytes.
    var cat = try runZit(gpa, io, tmp.dir, &.{ "artifact", w.object.get("other").?.string, "--raw", "-R", "repo" });
    defer freeResult(gpa, &cat);
    try expectExitDiag(cat.term, 0, cat);
    try std.testing.expectEqualStrings("package w\ntype T struct {\n\tA int\n\tB int\n\tC string\n}\nfunc f() {}\n", cat.stdout);

    // The other direction swaps the labels, never the boundaries.
    var sw = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "feat", "--into", "trunk", "-F", "json", "-R", "repo" });
    defer freeResult(gpa, &sw);
    try expectExit(sw.term, 5);
    const sw_parsed = try std.json.parseFromSlice(std.json.Value, gpa, sw.stdout, .{});
    defer sw_parsed.deinit();
    try std.testing.expectEqualStrings(doc.object.get("current").?.string, sw_parsed.value.object.get("other").?.string);
    try std.testing.expectEqualStrings("modify-delete", jsonFile(sw_parsed.value, "old.txt").?.object.get("kind").?.string);
    const sw_regions = jsonFile(sw_parsed.value, "w.go").?.object.get("regions").?.array.items;
    try expectJsonRegion(sw_regions[1], "other", .{ 2, 3 }, .{ 2, 3 }, .{ 2, 3 });
    try expectJsonRegion(sw_regions[5], "current", .{ 6, 6 }, .{ 6, 7 }, .{ 6, 6 });

    // Every verdict is a document.
    var same = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "feat", "--into", "feat", "-F", "json", "-R", "repo" });
    defer freeResult(gpa, &same);
    try expectExitDiag(same.term, 0, same);
    const same_parsed = try std.json.parseFromSlice(std.json.Value, gpa, same.stdout, .{});
    defer same_parsed.deinit();
    try std.testing.expectEqualStrings("up-to-date", same_parsed.value.object.get("verdict").?.string);
    try std.testing.expect(same_parsed.value.object.get("base").? == .null);
    try std.testing.expectEqual(@as(usize, 0), same_parsed.value.object.get("files").?.array.items.len);

    var both = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "-F", "json", "--tar", "-R", "repo" });
    defer freeResult(gpa, &both);
    try expectExit(both.term, 2);

    const after = try storeBytesAlloc(gpa, io, repo);
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);
}

test "cli merge-tree -F json: a mode both sides changed is not a conflict, and binary on one side is" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try initRepo(gpa, io, repo);
    try writeFile(io, repo, "run.sh", "one\ntwo\nthree\n");
    try writeFile(io, repo, "pic.dat", "text\n");
    try addManaged(gpa, io, repo, &.{ "run.sh", "pic.dat" });
    var root = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);
    try writeFile(io, repo, "run.sh", "ONE\ntwo\nthree\n");
    try chmodFile(io, repo, "run.sh", 0o755);
    try writeFile(io, repo, "pic.dat", "\x00now binary\n");
    var feat = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "feat", "--branch", "feat" });
    defer freeResult(gpa, &feat);
    try expectExitDiag(feat.term, 0, feat);
    var back = try runZit(gpa, io, repo, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);
    try writeFile(io, repo, "run.sh", "one\ntwo\nTHREE\n");
    try chmodFile(io, repo, "run.sh", 0o755);
    try writeFile(io, repo, "pic.dat", "text, edited\n");
    var edit = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "trunk" });
    defer freeResult(gpa, &edit);
    try expectExitDiag(edit.term, 0, edit);

    var mt = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "-F", "json", "-R", "repo" });
    defer freeResult(gpa, &mt);
    try expectExit(mt.term, 5);
    const parsed = try std.json.parseFromSlice(std.json.Value, gpa, mt.stdout, .{});
    defer parsed.deinit();
    const files = parsed.value.object.get("files").?.array.items;
    try std.testing.expectEqual(@as(usize, 1), files.len);
    try std.testing.expectEqualStrings("pic.dat", files[0].object.get("path").?.string);
    try std.testing.expectEqualStrings("binary", files[0].object.get("kind").?.string);
    try std.testing.expect(files[0].object.get("regions") == null);
}

test "cli merge-tree -F json: a clean pair is a clean document with its base" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try seedMergeTreeRepo(gpa, io, repo);
    var mt = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "feat", "--into", "trunk", "-F", "json", "-R", "repo" });
    defer freeResult(gpa, &mt);
    try expectExitDiag(mt.term, 0, mt);
    const parsed = try std.json.parseFromSlice(std.json.Value, gpa, mt.stdout, .{});
    defer parsed.deinit();
    try std.testing.expectEqualStrings("clean", parsed.value.object.get("verdict").?.string);
    try std.testing.expectEqual(@as(usize, 64), parsed.value.object.get("base").?.string.len);
    try std.testing.expectEqual(@as(usize, 0), parsed.value.object.get("files").?.array.items.len);
}

/// The resolutions both tests below give: w.go settled by hand, the binary
/// and the add/add taken from feat and trunk, old.txt deleted.
const conflict_resolution_w = "package w\ntype T struct {\n\tA int64\n\tB int\n\tC string\n}\nfunc f() {}\n";

fn writeConflictResolutions(io: std.Io, dir: std.Io.Dir) !void {
    try writeFile(io, dir, "w.go", conflict_resolution_w);
    try writeFile(io, dir, "logo.bin", "\x00feat");
    try writeFile(io, dir, "add.txt", "feat\ntrunk\n");
}

fn zitStdoutAlloc(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, argv: []const []const u8) ![]u8 {
    var r = try runZit(gpa, io, dir, argv);
    defer freeResult(gpa, &r);
    try expectExitDiag(r.term, 0, r);
    return gpa.dupe(u8, r.stdout);
}

test "cli merge-tree --commit: records the merge with the caller's resolutions, as merge and commit in a checkout would" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try seedConflictRepo(gpa, io, repo);

    var mt = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "-F", "json", "-R", "repo" });
    defer freeResult(gpa, &mt);
    try expectExit(mt.term, 5);
    const parsed = try std.json.parseFromSlice(std.json.Value, gpa, mt.stdout, .{});
    defer parsed.deinit();
    const cur = parsed.value.object.get("current").?.string;
    const oth = parsed.value.object.get("other").?.string;

    try tmp.dir.createDir(io, "res", .default_dir);
    var res = try tmp.dir.openDir(io, "res", .{});
    defer res.close(io);
    try writeConflictResolutions(io, res);
    const before = try storeBytesAlloc(gpa, io, repo);
    defer gpa.free(before);

    // Each refusal writes nothing.
    {
        // A page drawn against another pair.
        var stale = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "--commit", "--resolved", "res", "--delete", "old.txt", "-m", "settle", "--user", "jan", "--expect-current", oth, "--expect-other", oth, "-R", "repo" });
        defer freeResult(gpa, &stale);
        try expectExit(stale.term, 5);
        // A conflicting path left unanswered.
        var short = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "--commit", "--resolved", "res", "-m", "settle", "--user", "jan", "--expect-current", cur, "--expect-other", oth, "-R", "repo" });
        defer freeResult(gpa, &short);
        try expectExit(short.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, short.stderr, "no resolution for 'old.txt'") != null);
        // An answer for a path that did not conflict.
        try writeFile(io, res, "keep.txt", "keep\n");
        var extra = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "--commit", "--resolved", "res", "--delete", "old.txt", "-m", "settle", "--user", "jan", "--expect-current", cur, "--expect-other", oth, "-R", "repo" });
        defer freeResult(gpa, &extra);
        try expectExit(extra.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, extra.stderr, "'keep.txt' is not a conflicting path") != null);
        try res.deleteFile(io, "keep.txt");
        // An answer that is still a conflict.
        try writeFile(io, res, "w.go", "<<<<<<< current\nA\n=======\nB\n>>>>>>> other\n");
        var marked = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "--commit", "--resolved", "res", "--delete", "old.txt", "-m", "settle", "--user", "jan", "--expect-current", cur, "--expect-other", oth, "-R", "repo" });
        defer freeResult(gpa, &marked);
        try expectExit(marked.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, marked.stderr, "'w.go' still holds a conflict") != null);
        try writeFile(io, res, "w.go", conflict_resolution_w);
        const after = try storeBytesAlloc(gpa, io, repo);
        defer gpa.free(after);
        try std.testing.expectEqualSlices(u8, before, after);
    }

    var ok = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "--commit", "--resolved", "res", "--delete", "old.txt", "-m", "settle", "--user", "jan", "--expect-current", cur, "--expect-other", oth, "-R", "repo" });
    defer freeResult(gpa, &ok);
    try expectExitDiag(ok.term, 0, ok);
    try std.testing.expect(std.mem.startsWith(u8, ok.stdout, "check-in "));
    try std.testing.expect(std.mem.endsWith(u8, ok.stdout, " on feat\n"));
    const merged = ok.stdout["check-in ".len..][0..64];
    // No working area is left behind.
    var listing = try tmp.dir.openDir(io, "repo", .{ .iterate = true });
    defer listing.close(io);
    var walk = listing.iterate();
    while (try walk.next(io)) |e| try std.testing.expect(!std.mem.startsWith(u8, e.name, ".zit-merge-tree-"));

    // The branch now holds trunk, and the landing direction merges cleanly.
    var again = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "-F", "json", "-R", "repo" });
    defer freeResult(gpa, &again);
    try expectExitDiag(again.term, 0, again);
    try std.testing.expect(std.mem.indexOf(u8, again.stdout, "\"verdict\":\"up-to-date\"") != null);
    var land = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "feat", "--into", "trunk", "-F", "json", "-R", "repo" });
    defer freeResult(gpa, &land);
    try expectExitDiag(land.term, 0, land);
    try std.testing.expect(std.mem.indexOf(u8, land.stdout, "\"verdict\":\"clean\"") != null);

    // The same answers given by hand in a checkout of an identical history
    // record the same file set.
    try tmp.dir.createDir(io, "hand", .default_dir);
    var hand = try tmp.dir.openDir(io, "hand", .{});
    defer hand.close(io);
    try seedConflictRepo(gpa, io, hand);
    var up = try runZit(gpa, io, hand, &.{ "update", "feat" });
    defer freeResult(gpa, &up);
    try expectExitDiag(up.term, 0, up);
    var hm = try runZit(gpa, io, hand, &.{ "merge", "trunk" });
    defer freeResult(gpa, &hm);
    try expectExit(hm.term, 1);
    try writeConflictResolutions(io, hand);
    var hrm = try runZit(gpa, io, hand, &.{ "rm", "old.txt" });
    defer freeResult(gpa, &hrm);
    hand.deleteFile(io, "old.txt") catch {};
    var hc = try runCommitEnv(gpa, io, hand, .{ .user = "jan" }, &.{ "commit", "-m", "settle" });
    defer freeResult(gpa, &hc);
    try expectExitDiag(hc.term, 0, hc);
    const by_store = try zitStdoutAlloc(gpa, io, tmp.dir, &.{ "ls", merged, "--long", "-R", "repo" });
    defer gpa.free(by_store);
    const by_hand = try zitStdoutAlloc(gpa, io, hand, &.{ "ls", "--long" });
    defer gpa.free(by_hand);
    try std.testing.expectEqualStrings(by_hand, by_store);
    try std.testing.expect(std.mem.indexOf(u8, by_store, "old.txt") == null);
}

test "cli merge-tree: a store without one side's content reports that file unreadable, and --commit refuses it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try seedConflictRepo(gpa, io, repo);
    var mt = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "-F", "json", "-R", "repo" });
    defer freeResult(gpa, &mt);
    try expectExit(mt.term, 5);
    const parsed = try std.json.parseFromSlice(std.json.Value, gpa, mt.stdout, .{});
    defer parsed.deinit();
    const cur = parsed.value.object.get("current").?.string;
    const oth = parsed.value.object.get("other").?.string;
    const w_feat = jsonFile(parsed.value, "w.go").?.object.get("current").?.string;

    // Take feat's w.go out of the store, as a store that never fetched it.
    var sh = try runZit(gpa, io, repo, &.{ "shun", "--force", w_feat });
    defer freeResult(gpa, &sh);
    try expectExitDiag(sh.term, 0, sh);

    var gone = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "-F", "json", "-R", "repo" });
    defer freeResult(gpa, &gone);
    try expectExit(gone.term, 5);
    const doc = try std.json.parseFromSlice(std.json.Value, gpa, gone.stdout, .{});
    defer doc.deinit();
    const w = jsonFile(doc.value, "w.go").?;
    try std.testing.expectEqualStrings("unreadable", w.object.get("kind").?.string);
    try std.testing.expectEqualStrings(w_feat, w.object.get("current").?.string);
    try std.testing.expect(w.object.get("regions") == null);
    // The other conflicts are still reported as they were.
    try std.testing.expectEqualStrings("binary", jsonFile(doc.value, "logo.bin").?.object.get("kind").?.string);
    try std.testing.expectEqual(
        parsed.value.object.get("files").?.array.items.len,
        doc.value.object.get("files").?.array.items.len,
    );

    try tmp.dir.createDir(io, "res", .default_dir);
    var res = try tmp.dir.openDir(io, "res", .{});
    defer res.close(io);
    try writeConflictResolutions(io, res);
    const before = try storeBytesAlloc(gpa, io, repo);
    defer gpa.free(before);
    var refused = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "--commit", "--resolved", "res", "--delete", "old.txt", "-m", "settle", "--user", "jan", "--expect-current", cur, "--expect-other", oth, "-R", "repo" });
    defer freeResult(gpa, &refused);
    try expectExit(refused.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, refused.stderr, "does not hold the content of 'w.go'") != null);
    const after = try storeBytesAlloc(gpa, io, repo);
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);
}

test "cli merge-tree --commit: a path one side deleted is kept by resolving it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try seedConflictRepo(gpa, io, repo);
    var mt = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "-F", "json", "-R", "repo" });
    defer freeResult(gpa, &mt);
    try expectExit(mt.term, 5);
    const parsed = try std.json.parseFromSlice(std.json.Value, gpa, mt.stdout, .{});
    defer parsed.deinit();
    try tmp.dir.createDir(io, "res", .default_dir);
    var res = try tmp.dir.openDir(io, "res", .{});
    defer res.close(io);
    try writeConflictResolutions(io, res);
    try writeFile(io, res, "old.txt", "old, kept\n");
    var ok = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", "trunk", "--into", "feat", "--commit", "--resolved", "res", "-m", "keep it", "--user", "jan", "--expect-current", parsed.value.object.get("current").?.string, "--expect-other", parsed.value.object.get("other").?.string, "-R", "repo" });
    defer freeResult(gpa, &ok);
    try expectExitDiag(ok.term, 0, ok);
    const merged = ok.stdout["check-in ".len..][0..64];
    var mto = try runZit(gpa, io, tmp.dir, &.{ "merge-tree", merged, "--into", merged, "--out", "tree", "-R", "repo" });
    defer freeResult(gpa, &mto);
    try expectExitDiag(mto.term, 0, mto);
    var tree = try tmp.dir.openDir(io, "tree", .{});
    defer tree.close(io);
    const old = try readFile(gpa, io, tree, "old.txt");
    defer gpa.free(old);
    try std.testing.expectEqualStrings("old, kept\n", old);
}

test "cli merge: a mode the other side alone changed is merged, as Fossil does" {
    // The merge decided each path by blob hash and then always kept the
    // target's mode, so a branch that made a script executable merged as
    // "nothing changed". Fossil applies it: EXECUTABLE run.sh.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "run.sh", "echo hi\n");
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try addManaged(gpa, io, tmp.dir, &.{ "run.sh", "a.txt" });
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);

    try chmodFile(io, tmp.dir, "run.sh", 0o755);
    var feat = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "exec", "--branch", "feat" });
    defer freeResult(gpa, &feat);
    try expectExitDiag(feat.term, 0, feat);

    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);
    try std.testing.expect(!try isExecutableFile(io, tmp.dir, "run.sh"));
    try writeFile(io, tmp.dir, "a.txt", "a\nb\n");
    var edit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "trunk edit" });
    defer freeResult(gpa, &edit);
    try expectExitDiag(edit.term, 0, edit);

    var merged = try runZit(gpa, io, tmp.dir, &.{ "merge", "feat" });
    defer freeResult(gpa, &merged);
    try expectExitDiag(merged.term, 0, merged);
    try std.testing.expect(try isExecutableFile(io, tmp.dir, "run.sh"));
    const a_now = try readFile(gpa, io, tmp.dir, "a.txt");
    defer gpa.free(a_now);
    try std.testing.expectEqualStrings("a\nb\n", a_now);

    // And it is what the merge check-in records.
    var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "merge feat" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    var away = try runZit(gpa, io, tmp.dir, &.{ "update", "feat" });
    defer freeResult(gpa, &away);
    var again = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &again);
    try expectExitDiag(again.term, 0, again);
    try std.testing.expect(try isExecutableFile(io, tmp.dir, "run.sh"));
}

test "cli merge: a dirty working tree merges into, and the local edit survives" {
    // Was B-merge-14, which refused a dirty tree outright. It refused because
    // the merge computed over the checkout's EFFECTIVE content — what the
    // check-in says is on disk — and a dirty tree contradicts that. Making the
    // current side the working tree removes the contradiction rather than
    // carving an exception around it.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Five lines so the three contributions can occupy genuinely separate
    // regions: the current check-in changes line 1, the other side line 5, and
    // the uncommitted edit line 3. Adjacent edits legitimately conflict, so a
    // test for the CLEAN case has to keep them apart.
    var fx = try buildMergeDiamond(
        gpa,
        io,
        tmp.dir,
        "A\nB\nC\nD\nE\n",
        "A\nB\nC\nD\nE1\n",
        "A1\nB\nC\nD\nE\n",
    );
    defer fx.deinit(gpa);

    // An uncommitted edit makes the tree dirty, in a region neither check-in
    // touched.
    try writeFile(io, tmp.dir, "poem", "A1\nB\nDIRTY\nD\nE\n");

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr);

    // The merge parent is recorded exactly as a clean merge records it.
    try expectMergeState(io, tmp.dir, fx.current, fx.other);

    // All three contributions survive: the current check-in's A1, the other
    // side's E1, and the uncommitted DIRTY line that used to block the merge.
    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expectEqualStrings("A1\nB\nDIRTY\nD\nE1\n", poem);
}

test "cli merge: a dirty region the other side also changed conflicts, and keeps both" {
    // The other half: carrying the edit is not the same as preferring it. Where
    // the uncommitted work and the merged branch touch the same region, the
    // merge writes an envelope rather than silently choosing.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildMergeDiamond(
        gpa,
        io,
        tmp.dir,
        "A\nB\nC\nD\nE\n",
        "A\nB\nC\nD\nE1\n",
        "A1\nB\nC\nD\nE\n",
    );
    defer fx.deinit(gpa);

    // Uncommitted work on the SAME line the other side changed.
    try writeFile(io, tmp.dir, "poem", "A1\nB\nC\nD\nMINE\n");

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "conflict in poem") != null);

    // Pending merge still recorded, so `zit undo` can abandon it (B-merge-3/7/35).
    try expectMergeState(io, tmp.dir, fx.current, fx.other);

    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expect(std.mem.indexOf(u8, poem, "MINE") != null);
    try std.testing.expect(std.mem.indexOf(u8, poem, "E1") != null);

    // `undo` puts the uncommitted bytes back — the merge is fully abandonable
    // even though it started from a dirty tree.
    var undo = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &undo);
    try expectExit(undo.term, 0);
    const restored = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(restored);
    try std.testing.expectEqualStrings("A1\nB\nC\nD\nMINE\n", restored);
}

test "cli merge B-merge-15: a merge already pending refuses, exit 1, mutating nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nY\nC\n", "A\nX\nC\n");
    defer fx.deinit(gpa);

    var first = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &first);
    try expectExit(first.term, 1); // records the pending merge (conflict)

    var second = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &second);
    try expectExit(second.term, 1);
    try std.testing.expectEqualStrings("", second.stdout);
    try std.testing.expectEqualStrings(
        "zit merge: a merge is already pending; resolve and commit it, or run 'zit undo'\n",
        second.stderr,
    );
    // Still the first merge's recorded parent — the second refusal changed nothing.
    try expectMergeState(io, tmp.dir, fx.current, fx.other);
}

test "cli merge B-merge-16: NAME is the checkout or an ancestor is up to date, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
    defer fx.deinit(gpa);

    // base is a proper ancestor of the current checkout.
    var anc = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.base });
    defer freeResult(gpa, &anc);
    try expectExit(anc.term, 1);
    try std.testing.expectEqualStrings("", anc.stdout);
    try std.testing.expectEqualStrings("zit merge: already up to date; nothing to merge\n", anc.stderr);
    try expectMergeState(io, tmp.dir, fx.current, "");

    // The checkout itself is trivially up to date.
    var self = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.current });
    defer freeResult(gpa, &self);
    try expectExit(self.term, 1);
    try std.testing.expectEqualStrings("zit merge: already up to date; nothing to merge\n", self.stderr);
}

test "cli merge B-merge-16: a proper ancestor the merge-base mtime rule skips is still up to date, exit 1" {
    // The bug the `base == other` proxy missed: a linear chain root <- mid <- tip
    // whose event.mtimes are NON-monotone in DAG order (root carries the greatest
    // mtime). Merging `mid` — a proper ancestor of the checkout `tip` — must be
    // refused. But `mergeBaseOf(tip, mid)` selects the greatest-mtime common
    // ancestor, which is `root`, NOT `mid`; so `base == other` (mid) is false and
    // the old refusal wrongly let the merge proceed, recording an ancestor as a
    // merge parent. The mtime-independent `isAncestorOf` gate catches it.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    // root: the LATEST mtime, so it wins mergeBaseOf's greatest-mtime selection.
    var c_root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u", .epoch = "2000000000" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &c_root);
    try expectExit(c_root.term, 0);

    try writeFile(io, tmp.dir, "poem", "A\nB\n");
    // mid: an EARLIER mtime than its own parent — a proper ancestor of tip that
    // the merge-base walk therefore never selects.
    var c_mid = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u", .epoch = "1000000000" }, &.{ "commit", "-m", "mid" });
    defer freeResult(gpa, &c_mid);
    try expectExit(c_mid.term, 0);
    const mid = try gpa.dupe(u8, try checkinHash(c_mid.stdout));
    defer gpa.free(mid);

    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    var c_tip = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u", .epoch = "3000000000" }, &.{ "commit", "-m", "tip" });
    defer freeResult(gpa, &c_tip);
    try expectExit(c_tip.term, 0);
    const tip = try gpa.dupe(u8, try checkinHash(c_tip.stdout));
    defer gpa.free(tip);

    // Merge the proper ancestor `mid`: up to date, refuse, mutate nothing.
    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", mid });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit merge: already up to date; nothing to merge\n", res.stderr);
    // No merge parent recorded; checkout still at tip; working tree untouched.
    try expectMergeState(io, tmp.dir, tip, "");
    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expectEqualStrings("A\nB\nC\n", poem);
}

test "cli merge B-merge-18: an unresolvable NAME fails with the resolver diagnostic, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
    defer fx.deinit(gpa);

    const absent = "deadbeef" ** 8; // 64 hex, no such check-in
    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", absent });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not found") != null);
    // Nothing mutated.
    try expectMergeState(io, tmp.dir, fx.current, "");
}

test "cli merge B-merge-25: -h and --help print the verbatim help, exit 0, change nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    for ([_][]const u8{ "-h", "--help" }) |flag| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", flag });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(merge_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
    // help wins even over an otherwise-invalid trailing token.
    var win = try runZit(gpa, io, tmp.dir, &.{ "merge", "--help", "-z" });
    defer freeResult(gpa, &win);
    try expectExit(win.term, 0);
    try std.testing.expectEqualStrings(merge_help_expected, win.stdout);
}

test "cli merge B-merge-26: a missing NAME, an unknown option, or a second positional is a usage error, exit 2" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const cases = [_][]const []const u8{
        &.{ "merge", "--probe" }, // a probe needs NAME (a bare merge takes the sole other leaf)
        &.{ "merge", "-x" }, // unknown option
        &.{ "merge", "--force" }, // no override flag exists
        &.{ "merge", "a", "b" }, // second positional
    };
    for (cases) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "try 'zit merge --help'") != null);
    }
}

test "cli merge B-merge-27: outside a repository fails with the not-a-repo diagnostic, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{}); // NOT initialized
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "trunk" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no zit checkout here") != null);
}

test "cli merge B-merge-28/-29: streams stay separated and the verb is wired into help" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // B-merge-29: the top-level help names the merge verb.
    {
        var help = try runZit(gpa, io, tmp.dir, &.{"help"});
        defer freeResult(gpa, &help);
        try expectExit(help.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, help.stdout, "\n  merge      ") != null);
    }

    // B-merge-28: a clean merge writes the confirmation to stdout only.
    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
    defer fx.deinit(gpa);
    {
        var script_buf: [128]u8 = undefined;
        const script = try std.fmt.bufPrint(&script_buf, "\"$ZIT\" merge {s} > out.txt 2> err.txt", .{fx.other});
        var ok = try runShell(gpa, io, tmp.dir, script);
        defer freeResult(gpa, &ok);
        try expectExit(ok.term, 0);
        const out = try readFile(gpa, io, tmp.dir, "out.txt");
        defer gpa.free(out);
        const err = try readFile(gpa, io, tmp.dir, "err.txt");
        defer gpa.free(err);
        try std.testing.expect(std.mem.startsWith(u8, out, "merged "));
        try std.testing.expectEqualStrings("", err);
    }

    // B-merge-28: a conflict merge writes diagnostics to stderr only. Fresh repo.
    var tmp2 = std.testing.tmpDir(.{});
    defer tmp2.cleanup();
    var fx2 = try buildMergeDiamond(gpa, io, tmp2.dir, "A\nB\nC\n", "A\nY\nC\n", "A\nX\nC\n");
    defer fx2.deinit(gpa);
    {
        var script_buf: [128]u8 = undefined;
        const script = try std.fmt.bufPrint(&script_buf, "\"$ZIT\" merge {s} > out.txt 2> err.txt", .{fx2.other});
        var bad = try runShell(gpa, io, tmp2.dir, script);
        defer freeResult(gpa, &bad);
        try expectExit(bad.term, 1);
        const out = try readFile(gpa, io, tmp2.dir, "out.txt");
        defer gpa.free(out);
        const err = try readFile(gpa, io, tmp2.dir, "err.txt");
        defer gpa.free(err);
        try std.testing.expectEqualStrings("", out);
        try std.testing.expect(std.mem.indexOf(u8, err, "zit merge: conflict in poem\n") != null);
    }
}

// ==========================================================================
// `zit merge --cherrypick / --backout` end-to-end tests (contracts/
// merge-flags.md B-merge-30..41). Special merges are alternate baseline choices
// inside the SAME merge machinery — no new verb. Each builds its DAG through
// real check-ins and asserts exit code + streams + on-disk state (working file
// bytes, the `.zit-checkout` special_op field, and the minted Q card) read back
// through the library.
// ==========================================================================

/// Reads `.zit-checkout` and asserts version, an EMPTY merge_parent (a special
/// merge is never a plain merge), and the pending special_op (sign + REV hash).
fn expectSpecialState(io: std.Io, dir: std.Io.Dir, version: []const u8, special_op: []const u8) !void {
    var ckbuf: [checkout.max_ck_payload + 1]u8 = undefined;
    const ck = (try checkout.read(io, dir, &ckbuf)).?;
    try std.testing.expectEqualStrings(version, ck.version);
    try std.testing.expectEqualStrings("", ck.merge_parent);
    try std.testing.expectEqualStrings(special_op, ck.special_op);
}

test "cli merge: no NAME merges the sole other open leaf, -n probes, --integrate closes the leaf at commit, --baseline names the ancestor" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "a\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c1" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    const c1_hash = try checkinHash(c1.stdout);
    try writeFile(io, tmp.dir, "b", "b\n");
    try addManaged(gpa, io, tmp.dir, &.{"b"});
    var feat = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "feat", "--branch", "feat" });
    defer freeResult(gpa, &feat);
    try expectExitDiag(feat.term, 0, feat);
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);

    // -n is --probe: one answer line, nothing written.
    var probe = try runZit(gpa, io, tmp.dir, &.{ "merge", "-n", "feat" });
    defer freeResult(gpa, &probe);
    try expectExitDiag(probe.term, 0, probe);
    try std.testing.expect(std.mem.startsWith(u8, probe.stdout, "merges"));
    try std.testing.expectError(error.FileNotFound, tmp.dir.access(io, "b", .{}));

    // No NAME: feat is the one other open leaf.
    var bare = try runZit(gpa, io, tmp.dir, &.{"merge"});
    defer freeResult(gpa, &bare);
    try expectExitDiag(bare.term, 0, bare);
    try std.testing.expect(std.mem.startsWith(u8, bare.stdout, "merged "));
    try expectFileContent(gpa, io, tmp.dir, "b", "b\n");
    var un = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &un);
    try expectExit(un.term, 0);

    // --baseline with the true merge base changes nothing; --integrate makes
    // the commit close feat's leaf.
    var integ = try runZit(gpa, io, tmp.dir, &.{ "merge", "--integrate", "--baseline", c1_hash, "feat" });
    defer freeResult(gpa, &integ);
    try expectExitDiag(integ.term, 0, integ);
    var cm = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "land feat" });
    defer freeResult(gpa, &cm);
    try expectExitDiag(cm.term, 0, cm);
    try std.testing.expect(std.mem.indexOf(u8, cm.stdout, "(integrated)") != null);
    var closed = try runZit(gpa, io, tmp.dir, &.{ "branch", "-c" });
    defer freeResult(gpa, &closed);
    try expectExitDiag(closed.term, 0, closed);
    try std.testing.expect(std.mem.indexOf(u8, closed.stdout, "feat") != null);

    // Nothing left to merge without a name.
    var none = try runZit(gpa, io, tmp.dir, &.{"merge"});
    defer freeResult(gpa, &none);
    try expectExit(none.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, none.stderr, "no other open leaf") != null);
}

test "cli merge B-merge-30/-36: cherrypick lands REV's own change, preserves current's edit, records +REV" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // R0 root; R1 changes line 3 (C->Z) — its OWN change relative to its parent.
    // A fork off R0 (current, R2) changes a DISJOINT line 1 (A->Q).
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const r0 = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(r0);
    try writeFile(io, tmp.dir, "poem", "A\nB\nZ\n");
    const r1 = try commitHashDup(gpa, io, tmp.dir, "rev");
    defer gpa.free(r1);
    var up = try runZit(gpa, io, tmp.dir, &.{ "update", r0 });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);
    try writeFile(io, tmp.dir, "poem", "Q\nB\nC\n");
    const r2 = try commitForkDup(gpa, io, tmp.dir, "fork");
    defer gpa.free(r2);

    const zit_before = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(zit_before);

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", r1 });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    // Exactly REV's edit (line 3) is applied; current's own edit (line 1) is
    // preserved — a three-way merge, not a copy.
    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expectEqualStrings("Q\nB\nZ\n", poem);
    var line_buf: [256]u8 = undefined;
    const want_line = try std.fmt.bufPrint(&line_buf, "cherry-picked {s} into the working tree (1 file(s) changed)\n", .{r1});
    try std.testing.expectEqualStrings(want_line, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);

    // The pending +REV op is recorded; version unchanged (no advance).
    var sbuf: [70]u8 = undefined;
    const want_op = try std.fmt.bufPrint(&sbuf, "+{s}", .{r1});
    try expectSpecialState(io, tmp.dir, r2, want_op);

    // Mints nothing: `.zit` byte-identical (B-merge-30). B-merge-36: the undo
    // journal is armed (present) — durability ordering inherited from applyMerge.
    const zit_after = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(zit_after);
    try std.testing.expectEqualSlices(u8, zit_before, zit_after);
    try std.testing.expect(sectionExists(gpa, io, tmp.dir, .undo));
}

test "cli merge B-merge-32: backout reverses REV's change, records -REV" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // R0 root; R1 changes line 3 (C->Z). Checkout sits at R1; backing out R1
    // reverses its change onto the working tree (base = R1, other = R1's parent).
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const r0 = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(r0);
    try writeFile(io, tmp.dir, "poem", "A\nB\nZ\n");
    const r1 = try commitHashDup(gpa, io, tmp.dir, "rev");
    defer gpa.free(r1);

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "--backout", r1 });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expectEqualStrings("A\nB\nC\n", poem); // line 3 reverted Z->C
    var line_buf: [256]u8 = undefined;
    const want_line = try std.fmt.bufPrint(&line_buf, "backed out {s} into the working tree (1 file(s) changed)\n", .{r1});
    try std.testing.expectEqualStrings(want_line, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);

    var sbuf: [70]u8 = undefined;
    const want_op = try std.fmt.bufPrint(&sbuf, "-{s}", .{r1});
    try expectSpecialState(io, tmp.dir, r1, want_op);
}

test "cli commit B-merge-33/-34: a cherrypick commit mints one Q +REV card, single P parent, cleared one-shot" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const r0 = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(r0);
    try writeFile(io, tmp.dir, "poem", "A\nB\nZ\n");
    const r1 = try commitHashDup(gpa, io, tmp.dir, "rev");
    defer gpa.free(r1);
    var up = try runZit(gpa, io, tmp.dir, &.{ "update", r0 });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);
    try writeFile(io, tmp.dir, "poem", "Q\nB\nC\n");
    const r2 = try commitForkDup(gpa, io, tmp.dir, "fork");
    defer gpa.free(r2);

    var cp = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", r1 });
    defer freeResult(gpa, &cp);
    try expectExit(cp.term, 0);

    const commit = try commitHashDup(gpa, io, tmp.dir, "cherry commit");
    defer gpa.free(commit);

    var stored = try parseCheckin(gpa, io, tmp.dir, commit);
    defer stored.deinit();

    // B-merge-33: exactly one Q card, sign glued onto the target, NO baseline
    // token — byte-asserted against §4.7.14's grammar.
    var qbuf: [80]u8 = undefined;
    const want_q = try std.fmt.bufPrint(&qbuf, "Q +{s}\n", .{r1});
    try std.testing.expect(std.mem.indexOf(u8, stored.bytes, want_q) != null);
    try std.testing.expectEqual(@as(usize, 1), stored.m.q.len);
    try std.testing.expect(stored.m.q[0].sign == .cherrypick);
    try std.testing.expectEqualStrings(r1, stored.m.q[0].target);
    try std.testing.expect(stored.m.q[0].baseline == null);

    // The P card is the ORDINARY single-token parent (R2) — REV is not a merge
    // parent, so linkage gains no edge to it.
    try std.testing.expectEqual(@as(usize, 1), stored.m.p.len);
    try std.testing.expectEqual(@as(usize, 1), stored.m.p[0].count);
    try std.testing.expectEqualStrings(r2, stored.m.p[0].parents.?);

    // B-merge-34: the pending op is cleared one-shot; the checkout advanced.
    var ckbuf: [checkout.max_ck_payload + 1]u8 = undefined;
    const ck = (try checkout.read(io, tmp.dir, &ckbuf)).?;
    try std.testing.expectEqualStrings(commit, ck.version);
    try std.testing.expectEqualStrings("", ck.special_op);

    // A following commit with no fresh cherrypick emits NO Q card.
    try writeFile(io, tmp.dir, "poem", "Q\nB\nZ\nD\n");
    const next = try commitHashDup(gpa, io, tmp.dir, "follow-up");
    defer gpa.free(next);
    var stored2 = try parseCheckin(gpa, io, tmp.dir, next);
    defer stored2.deinit();
    try std.testing.expectEqual(@as(usize, 0), stored2.m.q.len);

    // B-merge-41: `zit info` renders the cherrypick provenance — a
    // `cherrypick: <REV>` line derived from the parsed Q card (target = r1).
    var info = try runZit(gpa, io, tmp.dir, &.{ "info", commit });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    var qline: [80]u8 = undefined;
    const want_line = try std.fmt.bufPrint(&qline, "cherrypick: {s}\n", .{r1});
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, want_line) != null);

    // Negative space: the plain follow-up check-in `next` carries no Q card, so
    // its info output is byte-identical to its pre-B-merge-41 baseline — no
    // cherrypick/backout line appears.
    var ninfo = try runZit(gpa, io, tmp.dir, &.{ "info", next });
    defer freeResult(gpa, &ninfo);
    try expectExit(ninfo.term, 0);
    var nbuf: [256]u8 = undefined;
    const want_plain = try std.fmt.bufPrint(&nbuf, "{s}\nbranch:  trunk\nleaf:    open\nparent:  {s}\n", .{ next, commit });
    try std.testing.expectEqualStrings(want_plain, ninfo.stdout);

    // The provenance line comes from the state, not a manifest
    // read — `info` reads the same number of manifests with a Q card as
    // without one.
    var reads: [2]u64 = undefined;
    for ([2][]const u8{ commit, next }, 0..) |name, i| {
        var r = try runZitEnv(gpa, io, tmp.dir, &.{.{ "ZIT_MANIFEST_READS", "1" }}, &.{ "info", name });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        reads[i] = manifestReadsOf(&r) orelse return error.NoReadCount;
    }
    try std.testing.expectEqual(reads[1], reads[0]);

    // Many names in one call are the single-name blocks in
    // argv order, separated by one blank line; a name that does not resolve
    // is reported on stderr, the rest are still described, and the exit is 1.
    var both = try runZit(gpa, io, tmp.dir, &.{ "info", commit, "nope", next });
    defer freeResult(gpa, &both);
    try expectExit(both.term, 1);
    const want_both = try std.mem.concat(gpa, u8, &.{ info.stdout, "\n", ninfo.stdout });
    defer gpa.free(want_both);
    try std.testing.expectEqualStrings(want_both, both.stdout);
    try std.testing.expectEqualStrings("zit info: check-in 'nope' not found\n", both.stderr);

    // `-F json`: one object per name in input order — the failed name is a
    // row, not a hole — and the same facts the text block carries.
    try writeFile(io, tmp.dir, "names.txt", try std.fmt.bufPrint(&nbuf, "{s}\nnope\n\n{s}\n", .{ commit, next }));
    var js = try runZitStdin(gpa, io, tmp.dir, "names.txt", &.{ "info", "-F", "json", "-" });
    defer freeResult(gpa, &js);
    try expectExit(js.term, 1);
    try std.testing.expectEqualStrings("", js.stderr);
    var rows = std.mem.splitScalar(u8, std.mem.trimEnd(u8, js.stdout, "\n"), '\n');
    const Row = struct {
        name: []const u8,
        hash: ?[]const u8 = null,
        branch: ?[]const u8 = null,
        leaf: ?[]const u8 = null,
        parent: ?[]const u8 = null,
        merge: ?[]const []const u8 = null,
        cherrypick: ?[]const []const u8 = null,
        backout: ?[]const []const u8 = null,
        @"error": ?[]const u8 = null,
    };
    var n_rows: usize = 0;
    while (rows.next()) |line| : (n_rows += 1) {
        var parsed = try std.json.parseFromSlice(Row, gpa, line, .{});
        defer parsed.deinit();
        const row = parsed.value;
        switch (n_rows) {
            0 => {
                try std.testing.expectEqualStrings(commit, row.name);
                try std.testing.expectEqualStrings(commit, row.hash.?);
                try std.testing.expectEqualStrings("trunk", row.branch.?);
                try std.testing.expectEqualStrings("no", row.leaf.?);
                try std.testing.expectEqual(@as(usize, 0), row.merge.?.len);
                try std.testing.expectEqual(@as(usize, 1), row.cherrypick.?.len);
                try std.testing.expectEqualStrings(r1, row.cherrypick.?[0]);
                try std.testing.expectEqual(@as(usize, 0), row.backout.?.len);
                try std.testing.expect(row.@"error" == null);
            },
            1 => {
                try std.testing.expectEqualStrings("nope", row.name);
                try std.testing.expectEqualStrings("not found", row.@"error".?);
                try std.testing.expect(row.hash == null);
            },
            2 => {
                try std.testing.expectEqualStrings(next, row.hash.?);
                try std.testing.expectEqualStrings("open", row.leaf.?);
                try std.testing.expectEqualStrings(commit, row.parent.?);
                try std.testing.expectEqual(@as(usize, 0), row.cherrypick.?.len);
            },
            else => return error.TooManyRows,
        }
    }
    try std.testing.expectEqual(@as(usize, 3), n_rows);
}

test "cli commit B-merge-33: a backout commit mints Q -REV" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const r0 = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(r0);
    try writeFile(io, tmp.dir, "poem", "A\nB\nZ\n");
    const r1 = try commitHashDup(gpa, io, tmp.dir, "rev");
    defer gpa.free(r1);

    var bo = try runZit(gpa, io, tmp.dir, &.{ "merge", "--backout", r1 });
    defer freeResult(gpa, &bo);
    try expectExit(bo.term, 0);

    const commit = try commitHashDup(gpa, io, tmp.dir, "backout commit");
    defer gpa.free(commit);
    var stored = try parseCheckin(gpa, io, tmp.dir, commit);
    defer stored.deinit();
    var qbuf: [80]u8 = undefined;
    const want_q = try std.fmt.bufPrint(&qbuf, "Q -{s}\n", .{r1});
    try std.testing.expect(std.mem.indexOf(u8, stored.bytes, want_q) != null);
    try std.testing.expectEqual(@as(usize, 1), stored.m.q.len);
    try std.testing.expect(stored.m.q[0].sign == .backout);
    try std.testing.expectEqualStrings(r1, stored.m.q[0].target);
    // Single-parent (R1) — a backout is never a merge parent.
    try std.testing.expectEqual(@as(usize, 1), stored.m.p[0].count);
    try std.testing.expectEqualStrings(r1, stored.m.p[0].parents.?);

    // B-merge-41: `zit info` surfaces the backout provenance — a
    // `backout:    <REV>` line derived from the parsed Q card (target = r1).
    var info = try runZit(gpa, io, tmp.dir, &.{ "info", commit });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    var qline: [80]u8 = undefined;
    const want_line = try std.fmt.bufPrint(&qline, "backout:    {s}\n", .{r1});
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, want_line) != null);
}

test "cli merge B-merge-31: a plain merge mints NO Q card (negative-space regression)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
    defer fx.deinit(gpa);
    var mg = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &mg);
    try expectExit(mg.term, 0);
    // The plain merge records a merge_parent, never a special_op.
    var ckbuf: [checkout.max_ck_payload + 1]u8 = undefined;
    const ck = (try checkout.read(io, tmp.dir, &ckbuf)).?;
    try std.testing.expectEqualStrings(fx.other, ck.merge_parent);
    try std.testing.expectEqualStrings("", ck.special_op);

    const commit = try commitHashDup(gpa, io, tmp.dir, "plain merge");
    defer gpa.free(commit);
    var stored = try parseCheckin(gpa, io, tmp.dir, commit);
    defer stored.deinit();
    // Two-token P card, NO Q card — the shipped merge surface, byte-unchanged.
    try std.testing.expectEqual(@as(usize, 2), stored.m.p[0].count);
    try std.testing.expectEqual(@as(usize, 0), stored.m.q.len);
}

test "cli merge B-merge-35: a cherrypick conflict writes markers, refuses commit, undo unwinds the pending Q state" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // R1 and R2 both change line 3 differently (Z vs W) → overlapping edit.
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const r0 = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(r0);
    try writeFile(io, tmp.dir, "poem", "A\nB\nZ\n");
    const r1 = try commitHashDup(gpa, io, tmp.dir, "rev");
    defer gpa.free(r1);
    var up = try runZit(gpa, io, tmp.dir, &.{ "update", r0 });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);
    try writeFile(io, tmp.dir, "poem", "A\nB\nW\n");
    const r2 = try commitForkDup(gpa, io, tmp.dir, "fork");
    defer gpa.free(r2);

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", r1 });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "zit merge: conflict in poem\n") != null);
    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expect(std.mem.indexOf(u8, poem, "<<<<<<< current\n") != null);
    // The pending op is recorded despite the conflict (not abandoned).
    var sbuf: [70]u8 = undefined;
    const want_op = try std.fmt.bufPrint(&sbuf, "+{s}", .{r1});
    try expectSpecialState(io, tmp.dir, r2, want_op);

    // commit REFUSES over the unresolved marker, minting nothing.
    var bad = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "x" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, bad.stderr, "unresolved conflict in poem") != null);

    // undo unwinds the working file AND the pending Q state.
    var un = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &un);
    try expectExit(un.term, 0);
    const restored = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(restored);
    try std.testing.expectEqualStrings("A\nB\nW\n", restored);
    var ckbuf: [checkout.max_ck_payload + 1]u8 = undefined;
    const ck = (try checkout.read(io, tmp.dir, &ckbuf)).?;
    try std.testing.expectEqualStrings("", ck.special_op);
    try std.testing.expectEqualStrings(r2, ck.version);
}

test "cli merge B-merge-37: cherrypick of a root applies its adds; backout of a root deletes them" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Cherrypick of a root: base = EMPTY parent set, other = the root's tree.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a", "hello\n");
        try addManaged(gpa, io, tmp.dir, &.{"a"});
        const r0 = try commitHashDup(gpa, io, tmp.dir, "root adds a");
        defer gpa.free(r0);
        // A child deletes `a`; the checkout then has no `a`.
        var rm = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "a" });
        defer freeResult(gpa, &rm);
        try expectExit(rm.term, 0);
        const r1 = try commitHashDup(gpa, io, tmp.dir, "child deletes a");
        defer gpa.free(r1);

        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", r0 });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        const a = try readFile(gpa, io, tmp.dir, "a");
        defer gpa.free(a);
        try std.testing.expectEqualStrings("hello\n", a); // the root's add applied
    }

    // Backout of a root: base = the root's tree, other = EMPTY → its adds deleted.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a", "hello\n");
        try addManaged(gpa, io, tmp.dir, &.{"a"});
        const r0 = try commitHashDup(gpa, io, tmp.dir, "root adds a");
        defer gpa.free(r0);

        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "--backout", r0 });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(!fileExists(io, tmp.dir, "a")); // the root's add reversed
    }
}

test "cli merge B-merge-38: a pending op and an unresolvable REV refuse; a dirty tree merges" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Shared fork DAG: R0 root, R1 edits line 3, R2 forks off R0.
    const build = struct {
        fn dag(g: std.mem.Allocator, i: std.Io, d: std.Io.Dir, r: *[3][]const u8) !void {
            try initRepo(g, i, d);
            try writeFile(i, d, "poem", "A\nB\nC\n");
            try addManaged(g, i, d, &.{"poem"});
            r[0] = try commitHashDup(g, i, d, "root");
            try writeFile(i, d, "poem", "A\nB\nZ\n");
            r[1] = try commitHashDup(g, i, d, "rev");
            var up = try runZit(g, i, d, &.{ "update", r[0] });
            defer freeResult(g, &up);
            try expectExit(up.term, 0);
            try writeFile(i, d, "poem", "Q\nB\nC\n");
            r[2] = try commitForkDup(g, i, d, "fork");
        }
    };

    // (a) a dirty working tree is merged into, not refused. A special
    //     merge picks an alternate baseline but runs the same classifier, so it
    //     inherits the working-tree current side along with everything else.
    //     Here the uncommitted edit replaces the whole file, which collides with
    //     what the cherrypick wants to apply — a conflict, not a refusal.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var r: [3][]const u8 = undefined;
        try build.dag(gpa, io, tmp.dir, &r);
        defer for (r) |h| gpa.free(h);
        try writeFile(io, tmp.dir, "poem", "dirty\n"); // uncommitted edit
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", r[1] });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "conflict in poem") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "working tree is not clean") == null);
        // The uncommitted bytes are still reachable in the envelope.
        const poem = try readFile(gpa, io, tmp.dir, "poem");
        defer gpa.free(poem);
        try std.testing.expect(std.mem.indexOf(u8, poem, "dirty") != null);
    }

    // (b) a second cherrypick over a pending cherrypick refuses.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var r: [3][]const u8 = undefined;
        try build.dag(gpa, io, tmp.dir, &r);
        defer for (r) |h| gpa.free(h);
        var first = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", r[1] });
        defer freeResult(gpa, &first);
        try expectExit(first.term, 0);
        var second = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", r[1] });
        defer freeResult(gpa, &second);
        try expectExit(second.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, second.stderr, "already pending") != null);
    }

    // (c) a plain merge over a pending cherrypick refuses.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var r: [3][]const u8 = undefined;
        try build.dag(gpa, io, tmp.dir, &r);
        defer for (r) |h| gpa.free(h);
        var first = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", r[1] });
        defer freeResult(gpa, &first);
        try expectExit(first.term, 0);
        var plain = try runZit(gpa, io, tmp.dir, &.{ "merge", r[1] });
        defer freeResult(gpa, &plain);
        try expectExit(plain.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, plain.stderr, "already pending") != null);
    }

    // (d) a cherrypick over a pending PLAIN merge refuses.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
        defer fx.deinit(gpa);
        var plain = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
        defer freeResult(gpa, &plain);
        try expectExit(plain.term, 0);
        var cp = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", fx.other });
        defer freeResult(gpa, &cp);
        try expectExit(cp.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, cp.stderr, "already pending") != null);
    }

    // (e) an unresolvable REV fails with the resolver diagnostic (B-merge-18).
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var r: [3][]const u8 = undefined;
        try build.dag(gpa, io, tmp.dir, &r);
        defer for (r) |h| gpa.free(h);
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", "nope-not-a-rev" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not found") != null);
    }
}

test "cli merge B-merge-39: a cherrypick already contained in the checkout refuses, nothing mutated" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Checkout sits at R1 itself; cherrypicking R1 there classifies to zero
    // entries (the change is already present) → "nothing to apply".
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const r0 = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(r0);
    try writeFile(io, tmp.dir, "poem", "A\nB\nZ\n");
    const r1 = try commitHashDup(gpa, io, tmp.dir, "rev");
    defer gpa.free(r1);

    const zit_before = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(zit_before);

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", r1 });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "nothing to apply") != null);
    // Nothing mutated: no pending op recorded, `.zit` untouched.
    var ckbuf: [checkout.max_ck_payload + 1]u8 = undefined;
    const ck = (try checkout.read(io, tmp.dir, &ckbuf)).?;
    try std.testing.expectEqualStrings("", ck.special_op);
    try std.testing.expectEqualStrings(r1, ck.version);
    const zit_after = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(zit_after);
    try std.testing.expectEqualSlices(u8, zit_before, zit_after);
}

test "cli merge B-merge-40: usage errors — both flags, a flag with no REV, an extra positional, unknown option" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const cases = [_][]const []const u8{
        &.{ "merge", "--cherrypick", "--backout", "rev" }, // mutually exclusive
        &.{ "merge", "--cherrypick" }, // a flag with no REV
        &.{ "merge", "--backout", "a", "b" }, // an extra positional beyond REV
        &.{ "merge", "--bogus", "rev" }, // unknown option
    };
    for (cases) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "try 'zit merge --help'") != null);
    }
    // The combined-flags diagnostic names the conflict.
    var both = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", "--backout", "rev" });
    defer freeResult(gpa, &both);
    try std.testing.expect(std.mem.indexOf(u8, both.stderr, "--cherrypick and --backout cannot be combined") != null);

    // B-merge-40: the help text carries the two flag lines.
    var help = try runZit(gpa, io, tmp.dir, &.{ "merge", "--help" });
    defer freeResult(gpa, &help);
    try expectExit(help.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "--cherrypick NAME") != null);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "--backout NAME") != null);
}

test "cli merge -X: ours and theirs resolve content conflicts, and neither is --backout" {
    // The whole point of `-X` is the MECHANICAL conflict — a
    // regenerated lockfile, a vendored tree taken from upstream — where
    // resolving by hand is where mistakes enter. What it must NOT do is behave
    // like "discard the other side": a region only one side touched is still
    // that side's, whichever way -X points, and this pins that.
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    inline for (.{ "ours", "theirs" }) |strategy| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);

        // f.txt: a middle line both sides rewrite (the conflict), plus a first
        // line only CURRENT changes and a last line only OTHER changes. The
        // unchanged filler between them is load-bearing: diff3 resolves per
        // REGION between sync points, so edits on adjacent lines land in one
        // region and the one-sided change is swallowed by the conflict. Spacing
        // them apart is what makes "only one side changed it" a separate
        // question the strategy must leave alone.
        try writeFile(io, tmp.dir, "f.txt", "first\nq\nr\ncollide\ns\nt\nlast\n");
        try addManaged(gpa, io, tmp.dir, &.{"f.txt"});
        const base = try commitHashDup(gpa, io, tmp.dir, "base");
        defer gpa.free(base);

        try writeFile(io, tmp.dir, "f.txt", "first\nq\nr\nFROM_OTHER\ns\nt\nOTHER_ONLY\n");
        const other = try commitHashDup(gpa, io, tmp.dir, "other");
        defer gpa.free(other);

        {
            var up = try runZit(gpa, io, tmp.dir, &.{ "update", base });
            defer freeResult(gpa, &up);
            try expectExitDiag(up.term, 0, up);
        }
        try writeFile(io, tmp.dir, "f.txt", "CURRENT_ONLY\nq\nr\nFROM_CURRENT\ns\nt\nlast\n");
        var cur = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "current", "--allow-fork" });
        defer freeResult(gpa, &cur);
        try expectExitDiag(cur.term, 0, cur);

        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "-X", strategy, other });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res); // resolved: not a conflict exit
        try std.testing.expectEqualStrings("", res.stderr);

        const merged = try readFile(gpa, io, tmp.dir, "f.txt");
        defer gpa.free(merged);
        // No markers survive a resolving strategy.
        try std.testing.expect(std.mem.indexOf(u8, merged, "<<<<<<<") == null);
        // The collided line goes to the named side...
        const want = if (std.mem.eql(u8, strategy, "ours")) "FROM_CURRENT" else "FROM_OTHER";
        const unwanted = if (std.mem.eql(u8, strategy, "ours")) "FROM_OTHER" else "FROM_CURRENT";
        try std.testing.expect(std.mem.indexOf(u8, merged, want) != null);
        try std.testing.expect(std.mem.indexOf(u8, merged, unwanted) == null);
        // ...while BOTH one-sided changes survive either way. `-X ours` that
        // dropped OTHER_ONLY would be `--backout` wearing a different name.
        try std.testing.expect(std.mem.indexOf(u8, merged, "CURRENT_ONLY") != null);
        try std.testing.expect(std.mem.indexOf(u8, merged, "OTHER_ONLY") != null);
    }
}

test "cli merge -X: a modify-versus-delete still conflicts, and bad values are usage errors" {
    // -X answers "whose CONTENT wins". It does not answer "should this file
    // exist", and either reading of that silently destroys work — so the one
    // case it declines to guess is pinned here rather than left to drift.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "g.txt", "keep\n");
    try addManaged(gpa, io, tmp.dir, &.{"g.txt"});
    const base = try commitHashDup(gpa, io, tmp.dir, "base");
    defer gpa.free(base);

    var rm = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "g.txt" });
    defer freeResult(gpa, &rm);
    try expectExitDiag(rm.term, 0, rm);
    const other = try commitHashDup(gpa, io, tmp.dir, "deleted");
    defer gpa.free(other);

    {
        var up = try runZit(gpa, io, tmp.dir, &.{ "update", base });
        defer freeResult(gpa, &up);
        try expectExitDiag(up.term, 0, up);
    }
    try writeFile(io, tmp.dir, "g.txt", "keep\nedited\n");
    var cur = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "edited", "--allow-fork" });
    defer freeResult(gpa, &cur);
    try expectExitDiag(cur.term, 0, cur);

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "-X", "ours", other });
    defer freeResult(gpa, &res);
    try expectNonzeroExit(res.term);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "g.txt") != null);

    // A strategy that is neither ours nor theirs, and a dangling -X, are usage
    // errors rather than a silent fallback to leaving markers — which would
    // look exactly like a strategy that had been applied and found nothing.
    const bad = [_][]const []const u8{
        &.{ "merge", "-X", "sideways", "tip" },
        &.{ "merge", "-X" },
    };
    for (bad) |argv| {
        var r = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &r);
        try expectExit(r.term, 2);
        try std.testing.expectEqualStrings("", r.stdout);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "ours or theirs") != null);
    }

    // The help must distinguish -X ours from --backout, which is the misread
    // the flag is most exposed to.
    var help = try runZit(gpa, io, tmp.dir, &.{ "merge", "--help" });
    defer freeResult(gpa, &help);
    try expectExitDiag(help.term, 0, help);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "-X ours IS NOT --backout") != null);
}

// ==========================================================================
// `zit status` end-to-end tests (contracts/status.md B-status-1..17). Each
// spawns the REAL binary in an isolated, pinned environment, asserts exit code
// + exact stdout/stderr (structural around the born hash, never a wall clock),
// and — status being read-only — reads `.zit-checkout`/`.zit-managed` back
// through the library to prove no mutation occurred. Teardown is unconditional.
// ==========================================================================

/// Overwrites `.zit-checkout` with a chosen state via the library's own writer,
/// so the born-state header (B-status-4) is reachable with no `commit` verb (the
/// checkout record is independent of the log). Used only by the status e2e.
fn seedCheckout(io: std.Io, dir: std.Io.Dir, version: []const u8, branch: []const u8) !void {
    try checkout.write(io, dir, .{ .version = version, .branch = branch, .merge_parent = "", .special_op = "", .manifest_flags = "" });
}

/// Asserts, through the library, that `dir`'s checkout record still holds
/// exactly `version`/`branch` — the read-only oracle proving `status` did not
/// mutate `.zit-checkout`.
fn expectCheckout(io: std.Io, dir: std.Io.Dir, version: []const u8, branch: []const u8) !void {
    var ckbuf: [checkout.max_ck_payload + 1]u8 = undefined;
    const ck = (try checkout.read(io, dir, &ckbuf)).?;
    try std.testing.expectEqualStrings(version, ck.version);
    try std.testing.expectEqualStrings(branch, ck.branch);
}

/// An independent transcription of contracts/status.md §Help. Kept separate from
/// the binary's own `status_help` so the e2e compares two hand-made copies —
/// drift in either is caught (B-status-14/-16).
const status_help_expected =
    \\usage: zit status [-v] [--classify] [--added] [--edited] [--missing]
    \\                  [--conflict] [--extra] [--differ] [--dotfiles]
    \\                  [-h|--help]
    \\
    \\Show the state of the working copy: the current branch, the current
    \\check-in (if any), and the state of your tracked files.
    \\
    \\Under a pending merge, an edited file that still carries conflict
    \\markers is reported as CONFLICT. Rows use Fossil's words (ADDED,
    \\EDITED, MISSING, CONFLICT, EXTRA, MERGED_WITH) in an aligned column;
    \\--classify is accepted and changes nothing. The kind flags narrow the
    \\rows to those kinds; --differ is
    \\--edited plus --extra, and --extra lists the untracked files 'zit
    \\extras' would (names beginning with a dot only under --dotfiles).
    \\The report leads with Fossil's checkout block —
    \\repository, local-root, checkout, parent, tags, comment — with
    \\twelve-character names and the weekday in the times; 'zit changes'
    \\is the same report without the block, as in Fossil.
    \\
    \\zit has no staging area. `status` reports the files you marked with
    \\`zit add` and how each one stands in the working tree:
    \\
    \\  ADDED      <path>   tracked and new — it will enter your next check-in
    \\  EDITED     <path>   tracked, and its bytes or mode differ from the check-in
    \\  MISSING    <path>   tracked, but not present in the working tree
    \\
    \\A tracked file whose contents match the current check-in is not listed.
    \\
    \\Until your first `zit commit` the branch 'trunk' is unborn: there is no
    \\check-in yet, so every tracked file is reported as ADDED (it will form
    \\the root check-in). The current check-in, once one exists, is shown as its
    \\full artifact hash.
    \\
    \\Two things `status` does not report, because it walks only your tracked
    \\files: a file deleted from the check-in but no longer tracked (`zit diff`
    \\lists it) and untracked "extra" files (`zit extras` lists them).
    \\
    \\options:
    \\  -v, --verbose accepted; the block is printed either way
    \\     --classify  accepted; Fossil's words are the default
    \\     --added, --edited, --missing, --conflict, --extra
    \\               only rows of those kinds
    \\     --differ    --edited plus --extra
    \\  -h, --help   show this help and exit
    \\
    \\exit status:
    \\  0  the working-copy state was reported (with or without changes)
    \\  1  this is not a zit repository, or its state could not be read
    \\  2  usage error (an unknown option or an unexpected argument)
    \\
    \\aliases: changes
    \\
;

test "cli status B-status-3/-7/-15: fresh repo, nothing tracked, header + nothing tracked yet" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expect(std.mem.endsWith(u8, res.stdout, "nothing tracked yet\n")); // after the checkout block
    try std.testing.expectEqualStrings("", res.stderr); // report → stdout only

    // Read-only: the checkout record is unchanged and no managed set was written.
    try expectCheckout(io, tmp.dir, "", "trunk");
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli status B-status-3/-6/-8: unborn, two present tracked files → added in sorted order" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try touch(io, tmp.dir, "b");
    try addManaged(gpa, io, tmp.dir, &.{ "a", "b" });

    var res = try runZit(gpa, io, tmp.dir, &.{"changes"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "ADDED      a\nADDED      b\n",
        res.stdout,
    );
    try std.testing.expectEqualStrings("", res.stderr);
    // Read-only: the tracked set still holds exactly {a, b}.
    try expectManaged(gpa, io, tmp.dir, &.{ "a", "b" });
}

test "cli status B-status-5: a tracked file removed from disk → missing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    try tmp.dir.deleteFile(io, "a"); // gone from the working tree, still tracked

    var res = try runZit(gpa, io, tmp.dir, &.{"changes"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "MISSING    a\n",
        res.stdout,
    );
    try std.testing.expectEqualStrings("", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{"a"}); // still tracked (read-only)
}

test "cli status B-status-5/-8: present/absent interleave in path order, not grouped by kind" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try touch(io, tmp.dir, "b");
    try touch(io, tmp.dir, "c");
    try addManaged(gpa, io, tmp.dir, &.{ "a", "b", "c" });
    try tmp.dir.deleteFile(io, "b"); // the middle path is the one gone

    var res = try runZit(gpa, io, tmp.dir, &.{"changes"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "ADDED      a\nMISSING    b\nADDED      c\n",
        res.stdout,
    );
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli status B-status-5: a tracked path replaced by a directory folds to missing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    try tmp.dir.deleteFile(io, "a");
    try tmp.dir.createDir(io, "a", .default_dir); // non-regular now → missing

    var res = try runZit(gpa, io, tmp.dir, &.{"changes"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "MISSING    a\n",
        res.stdout,
    );
    try std.testing.expectEqualStrings("", res.stderr);
}

/// Seeds a born checkout whose current check-in is a real, resolvable manifest
/// listing `files` — the shape `status` and `diff` both need now that both
/// compare the working tree against the check-in's effective F-set. Returns the
/// owned check-in hash so a test can quote it verbatim in the header line.
fn seedBornCheckin(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    label: []const u8,
    files: []const artifact.FCard,
) ![]u8 {
    const hash = try appendDiffManifest(gpa, io, dir, label, null, files);
    errdefer gpa.free(hash);
    try seedCheckout(io, dir, hash, "trunk");
    return hash;
}

/// Appends `content` as a SHA-1 blob and returns the F card naming it at `path`.
/// The returned card borrows `path` and the returned hash, which the caller owns.
fn seedRegularFCard(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    path: []const u8,
    content: []const u8,
) !artifact.FCard {
    const blob = try appendArtifactAs(gpa, io, dir, .sha1, content);
    return .{ .filename = path, .hash = blob, .perm = null, .old_name = null, .mode = .regular };
}

test "cli status B-status-16: born state, an edited tracked file is reported as edited" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "a0\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    const card = try seedRegularFCard(gpa, io, tmp.dir, "a", "a0\n");
    defer gpa.free(card.hash.?);
    const born_hash = try seedBornCheckin(gpa, io, tmp.dir, "root", &.{card});
    defer gpa.free(born_hash);
    try writeFile(io, tmp.dir, "a", "a1\n"); // same path, different bytes

    var res = try runZit(gpa, io, tmp.dir, &.{"changes"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const expected = "EDITED     a\n";
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try expectCheckout(io, tmp.dir, born_hash, "trunk"); // read-only
}

test "cli status B-status-16: a rewrite with identical bytes (mtime bump) is not reported" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "a0\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    const card = try seedRegularFCard(gpa, io, tmp.dir, "a", "a0\n");
    defer gpa.free(card.hash.?);
    const born_hash = try seedBornCheckin(gpa, io, tmp.dir, "root", &.{card});
    defer gpa.free(born_hash);
    // Rewrite byte-identical content: the mtime moves, the artifact hash does
    // not. Classification is by content hash, so this is clean, not edited.
    try writeFile(io, tmp.dir, "a", "a0\n");

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const expected = "nothing to report\n";
    try std.testing.expect(std.mem.endsWith(u8, res.stdout, expected)); // after the checkout block
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli status B-status-6/-16: edited, missing and newly added interleave in path order" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "a0\n");
    try writeFile(io, tmp.dir, "b", "b0\n");
    try writeFile(io, tmp.dir, "d", "d0\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a", "b", "d" });
    const card_a = try seedRegularFCard(gpa, io, tmp.dir, "a", "a0\n");
    defer gpa.free(card_a.hash.?);
    const card_b = try seedRegularFCard(gpa, io, tmp.dir, "b", "b0\n");
    defer gpa.free(card_b.hash.?);
    const card_d = try seedRegularFCard(gpa, io, tmp.dir, "d", "d0\n");
    defer gpa.free(card_d.hash.?);
    const born_hash = try seedBornCheckin(gpa, io, tmp.dir, "root", &.{ card_a, card_b, card_d });
    defer gpa.free(born_hash);

    try writeFile(io, tmp.dir, "a", "a1\n"); // edited
    try tmp.dir.deleteFile(io, "b"); // missing
    try writeFile(io, tmp.dir, "c", "c0\n");
    try addManaged(gpa, io, tmp.dir, &.{"c"}); // added: tracked, not in the check-in
    // `d` is untouched → clean → no line.

    var res = try runZit(gpa, io, tmp.dir, &.{"changes"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const expected = "EDITED     a\nMISSING    b\nADDED      c\n";
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli status B-status-16: an executable-bit-only change is edited" {
    if (comptime !File.Permissions.has_executable_bit) return error.SkipZigTest;

    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    const original = "#!/bin/zit\nsame\n";
    try writeFile(io, tmp.dir, "script", original);
    try addManaged(gpa, io, tmp.dir, &.{"script"});
    const blob = try appendArtifactAs(gpa, io, tmp.dir, .sha1, original);
    defer gpa.free(blob);
    const card: artifact.FCard = .{
        .filename = "script",
        .hash = blob,
        .perm = "x",
        .old_name = null,
        .mode = .executable,
    };
    const born_hash = try seedBornCheckin(gpa, io, tmp.dir, "executable-root", &.{card});
    defer gpa.free(born_hash);
    // The bytes match the baseline; only the mode differs from the F card's `x`.

    var res = try runZit(gpa, io, tmp.dir, &.{"changes"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const expected = "EDITED     script\n";
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli status B-status-7/-16: born state, every tracked file clean → nothing to report" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "p", "p0\n");
    try addManaged(gpa, io, tmp.dir, &.{"p"});
    const card = try seedRegularFCard(gpa, io, tmp.dir, "p", "p0\n");
    defer gpa.free(card.hash.?);
    const born_hash = try seedBornCheckin(gpa, io, tmp.dir, "root", &.{card});
    defer gpa.free(born_hash);

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const expected = "nothing to report\n";
    try std.testing.expect(std.mem.endsWith(u8, res.stdout, expected)); // after the checkout block
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli status B-status-5/-16: a tracked path in the check-in replaced by a directory is missing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "a0\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    const card = try seedRegularFCard(gpa, io, tmp.dir, "a", "a0\n");
    defer gpa.free(card.hash.?);
    const born_hash = try seedBornCheckin(gpa, io, tmp.dir, "root", &.{card});
    defer gpa.free(born_hash);
    try tmp.dir.deleteFile(io, "a");
    try tmp.dir.createDir(io, "a", .default_dir); // non-regular now → missing

    var res = try runZit(gpa, io, tmp.dir, &.{"changes"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const expected = "MISSING    a\n";
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli status B-status-16: a file deleted from the check-in but untracked is still not reported" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // `gone` is in the check-in but was never added to the managed set here, so
    // it is a deleted-from-check-in file: `--help` still defers those, and
    // `status` iterates only the managed set, so it must stay silent.
    const card = try seedRegularFCard(gpa, io, tmp.dir, "gone", "g0\n");
    defer gpa.free(card.hash.?);
    const born_hash = try seedBornCheckin(gpa, io, tmp.dir, "root", &.{card});
    defer gpa.free(born_hash);

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const expected = "nothing to report\n";
    try std.testing.expect(std.mem.endsWith(u8, res.stdout, expected)); // after the checkout block
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli status B-status-10/-15: an unresolvable current check-in fails without a partial report" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "a0\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    try seedCheckout(io, tmp.dir, "a" ** 64, "trunk"); // no such artifact in the log

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit status: current check-in is not a valid manifest\n",
        res.stderr,
    );
}

/// Sets `name`'s permission bits through its own handle — `Dir.setPermissions`
/// applies to the directory itself, so a per-file chmod needs the open file.
fn chmodFile(io: std.Io, dir: std.Io.Dir, name: []const u8, mode: std.posix.mode_t) !void {
    var f = try dir.openFile(io, name, .{});
    defer f.close(io);
    try f.setPermissions(io, File.Permissions.fromMode(mode));
}

test "cli status B-status-15: an unreadable tracked file fails the walk, leaving stdout empty" {
    if (comptime !File.Permissions.has_executable_bit) return error.SkipZigTest;

    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "a0\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});

    // Unreadable, not absent: the no-follow stat still succeeds and reports a
    // regular file, so the walk reaches `openFile` and takes EACCES. That is a
    // comparison failure, not a dirty working copy — exit 1, and NOTHING on
    // stdout, not even the header line that precedes the rows.
    try chmodFile(io, tmp.dir, "a", 0o000);
    defer chmodFile(io, tmp.dir, "a", 0o644) catch {};

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit status: cannot compare working tree: input/output error\n",
        res.stderr,
    );
}

test "cli status B-status-15: a walk that aborts after a printable row still leaves stdout empty" {
    if (comptime !File.Permissions.has_executable_bit) return error.SkipZigTest;

    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // `a` sorts first and IS reportable (added); `z` is unreadable. Were the
    // report streamed, stdout would already carry the header and "added a"
    // before the failure — this is the negative space that check exists for.
    try writeFile(io, tmp.dir, "a", "a0\n");
    try writeFile(io, tmp.dir, "z", "z0\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a", "z" });
    try chmodFile(io, tmp.dir, "z", 0o000);
    defer chmodFile(io, tmp.dir, "z", 0o644) catch {};

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit status: cannot compare working tree: input/output error\n",
        res.stderr,
    );
}

test "cli status B-status-5/-16: a tracked path in the check-in replaced by a symlink is missing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "a0\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    const card = try seedRegularFCard(gpa, io, tmp.dir, "a", "a0\n");
    defer gpa.free(card.hash.?);
    const born_hash = try seedBornCheckin(gpa, io, tmp.dir, "root", &.{card});
    defer gpa.free(born_hash);

    // Replace the tracked regular file with a symlink whose target holds the
    // check-in's exact bytes: only the no-follow stat distinguishes them. A
    // followed link would compare equal and report nothing.
    try tmp.dir.deleteFile(io, "a");
    try writeFile(io, tmp.dir, "target", "a0\n");
    tmp.dir.symLink(io, "target", "a", .{}) catch |e| switch (e) {
        error.AccessDenied, error.Unexpected => return error.SkipZigTest,
        else => return e,
    };

    var res = try runZit(gpa, io, tmp.dir, &.{"changes"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const expected = "MISSING    a\n";
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli status B-status-16: a current delta checkout with an absent B-card baseline fails" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    const absent_baseline = "f" ** 64;
    const delta = try appendDiffManifest(gpa, io, tmp.dir, "missing-baseline", absent_baseline, &.{});
    defer gpa.free(delta);
    try seedCheckout(io, tmp.dir, delta, "trunk");

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit status: cannot resolve baseline\n", res.stderr);
}

test "cli status B-status-17: an empty branch field displays as trunk" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedCheckout(io, tmp.dir, "", ""); // empty branch → defensive trunk default

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expect(std.mem.endsWith(u8, res.stdout, "nothing tracked yet\n")); // after the checkout block
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli status B-status-9/-15: not a zit repository → exit 1, stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup(); // a bare tmpDir: no repo here or in any parent

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit: no zit checkout here (looked for .zit-checkout in this directory and its parents)\n" ++
            "if this is a bare repository, name it with -R, or run 'zit open' to create a working tree\n",
        res.stderr,
    );
}

test "cli status B-status-11: a corrupt .zit-managed → exit 1, stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try corruptSection(io, tmp.dir, .managed);

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout); // header not yet printed
    try std.testing.expectEqualStrings(
        "zit status: cannot read tracked files: .zit-checkout is corrupt\n",
        res.stderr,
    );
}

test "cli status B-status-10: a corrupt .zit-checkout → exit 1, stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // version_len 0xffff with no payload: a malformed TLV → error.Corrupt. The
    // file still exists, so discovery succeeds and the read is what fails.
    {
        var f = try tmp.dir.createFile(io, checkout.filename, .{});
        defer f.close(io);
        try f.writePositionalAll(io, &[_]u8{ 0xff, 0xff, 0xaa }, 0);
    }

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit status: cannot read checkout state: .zit-checkout is corrupt\n",
        res.stderr,
    );
}

test "cli status B-status-14/-16: --help and -h print the verbatim help to stdout, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    inline for (.{ "--help", "-h" }) |flag| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup(); // no repo needed: help wins before discovery
        var res = try runZit(gpa, io, tmp.dir, &.{ "status", flag });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(status_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
}

test "cli status B-status-12: an unknown option → exit 2, usage on stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "status", "-x" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit status: unknown option '-x'\ntry 'zit status --help'\n",
        res.stderr,
    );
}

test "cli status B-status-13: an unexpected argument → exit 2, usage on stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "status", "extra" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit status: unexpected argument 'extra'\ntry 'zit status --help'\n",
        res.stderr,
    );
}

// --- top-level discoverability & help routing (DX D4) ----------------------

test "cli status B-status-16 DX-D4: the command list names status" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    // Bare `zit` and `zit help` both print the command list on stdout; a user
    // scanning it must SEE `status` — the DX-D4 blocker was that it was absent,
    // so `zit help status` implied the verb did not exist.
    inline for (.{ &[_][]const u8{}, &[_][]const u8{"help"} }) |argv| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup(); // no repo needed: help is repo-independent
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "commands:") != null);
        try std.testing.expect(std.mem.indexOf(
            u8,
            res.stdout,
            "status     show the working checkout state\n",
        ) != null);
        try std.testing.expectEqualStrings("", res.stderr);
    }
}

test "cli status B-status-16 DX-D4: zit help status prints the status help, not the command list" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup(); // repo-independent: help wins before any discovery
    var res = try runZit(gpa, io, tmp.dir, &.{ "help", "status" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // Byte-identical to `zit status --help` — the same independent transcription
    // the --help e2e uses — proving the topic routed to the verb's own help.
    try std.testing.expectEqualStrings(status_help_expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli status B-status-16 DX-D4: zit help <verb> routes each verb to its own help, not the command list" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    // A verb's help carries `usage: zit <verb>` and never the top-level
    // `commands:` block, so these two probes distinguish routed help from the
    // fallback command list for every routed verb.
    const Case = struct { verb: []const u8, usage: []const u8 };
    const cases = [_]Case{
        .{ .verb = "init", .usage = "usage: zit init" },
        .{ .verb = "add", .usage = "usage: zit add" },
        .{ .verb = "rm", .usage = "usage: zit rm" },
    };
    inline for (cases) |c| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try runZit(gpa, io, tmp.dir, &.{ "help", c.verb });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, c.usage) != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "commands:") == null);
        try std.testing.expectEqualStrings("", res.stderr);
    }
}
// ==========================================================================
// `zit diff` end-to-end. The public surface is deliberately a compact
// metadata report, not a text patch: only managed paths participate, compared
// with the checkout's resolved F-set. Each invocation must be read-only.
// ==========================================================================

test "cli diff --by, timeline -F html, ui: static renders land, the ui verb explains itself" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f", "a\nb\nc\n");
    try addManaged(gpa, io, tmp.dir, &.{"f"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "first <one>" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    try writeFile(io, tmp.dir, "f", "a\nB\nc\nd\n");

    // --by writes a page and names it; no terminal, so nothing is opened.
    var by = try runZit(gpa, io, tmp.dir, &.{ "diff", "--by" });
    defer freeResult(gpa, &by);
    try expectExitDiag(by.term, 0, by);
    try std.testing.expect(std.mem.startsWith(u8, by.stdout, "wrote "));
    const page_path = std.mem.trimEnd(u8, by.stdout["wrote ".len..], "\n");
    const page = try std.Io.Dir.cwd().readFileAlloc(io, page_path, gpa, .limited(1 << 20));
    defer gpa.free(page);
    defer std.Io.Dir.cwd().deleteFile(io, page_path) catch {};
    try std.testing.expect(std.mem.indexOf(u8, page, "<table class=\"sbs\">") != null);
    try std.testing.expect(std.mem.indexOf(u8, page, "<tr class=\"chg\">") != null);
    try std.testing.expect(std.mem.indexOf(u8, page, "<tr class=\"ins\">") != null);
    try std.testing.expect(std.mem.indexOf(u8, page, "<h2>f</h2>") != null);

    // -F html: a table on stdout, text escaped.
    var tl = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "html" });
    defer freeResult(gpa, &tl);
    try expectExitDiag(tl.term, 0, tl);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "<table class=\"timeline\">") != null);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "first &lt;one&gt;") != null);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "class=\"current\"") != null);

    // ui: reserved; says whose view it is and exits 1; help is exit 0.
    var ui = try runZit(gpa, io, tmp.dir, &.{"ui"});
    defer freeResult(gpa, &ui);
    try expectExit(ui.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, ui.stderr, "quarry") != null);
    var uh = try runZit(gpa, io, tmp.dir, &.{ "ui", "--help" });
    defer freeResult(gpa, &uh);
    try expectExit(uh.term, 0);
    try std.testing.expect(std.mem.startsWith(u8, uh.stdout, "usage: zit ui"));
}

test "cli diff: PATH operands narrow, --numstat counts, --invert swaps, --checkin and --branch pick the pair" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDirPath(io, "sub");
    try writeFile(io, tmp.dir, "f", "a\nb\n");
    try writeFile(io, tmp.dir, "sub/g", "x\n");
    try addManaged(gpa, io, tmp.dir, &.{ "f", "sub/g" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c1" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    try writeFile(io, tmp.dir, "f", "a\nc\nd\n");
    try writeFile(io, tmp.dir, "sub/g", "x\ny\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c2" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    var side = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "side", "--branch", "side", "--allow-empty" });
    defer freeResult(gpa, &side);
    try expectExitDiag(side.term, 0, side);
    try writeFile(io, tmp.dir, "sub/g", "x\ny\nz\n");
    var s2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "s2" });
    defer freeResult(gpa, &s2);
    try expectExit(s2.term, 0);

    // Working tree: one edit; numstat counts it, a PATH operand narrows.
    try writeFile(io, tmp.dir, "f", "q\n");
    var ns = try runZit(gpa, io, tmp.dir, &.{ "diff", "--numstat" });
    defer freeResult(gpa, &ns);
    try expectExitDiag(ns.term, 0, ns);
    try std.testing.expectEqualStrings("1\t3\tf\n", ns.stdout);
    var only_sub = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief", "sub" });
    defer freeResult(gpa, &only_sub);
    try expectExitDiag(only_sub.term, 0, only_sub);
    try std.testing.expectEqualStrings("", only_sub.stdout);
    var only_f = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief", "f" });
    defer freeResult(gpa, &only_f);
    try std.testing.expectEqualStrings("M f\n", only_f.stdout);
    try tmp.dir.deleteFile(io, "f");
    var inv = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief", "--invert" });
    defer freeResult(gpa, &inv);
    try std.testing.expectEqualStrings("A f\n", inv.stdout);
    var rv = try runZit(gpa, io, tmp.dir, &.{ "revert", "f" });
    defer freeResult(gpa, &rv);
    try expectExit(rv.term, 0);

    // --checkin V is V against its parent; --branch B is B's tip against its fork point.
    var ck = try runZit(gpa, io, tmp.dir, &.{ "diff", "--checkin", "trunk", "--numstat" });
    defer freeResult(gpa, &ck);
    try expectExitDiag(ck.term, 0, ck);
    try std.testing.expectEqualStrings("2\t1\tf\n1\t0\tsub/g\n", ck.stdout);
    var br = try runZit(gpa, io, tmp.dir, &.{ "diff", "--branch", "side", "--numstat" });
    defer freeResult(gpa, &br);
    try expectExitDiag(br.term, 0, br);
    try std.testing.expectEqualStrings("1\t0\tsub/g\n", br.stdout);
    var both = try runZit(gpa, io, tmp.dir, &.{ "diff", "--checkin", "trunk", "--branch", "side" });
    defer freeResult(gpa, &both);
    try expectExit(both.term, 2);
}

test "cli diff: an unborn managed regular file is added" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "new.txt", "new\n");
    try addManaged(gpa, io, tmp.dir, &.{"new.txt"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("A new.txt\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

fn expectFileContent(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, path: []const u8, want: []const u8) !void {
    const got = try dir.readFileAlloc(io, path, gpa, .limited(1 << 20));
    defer gpa.free(got);
    try std.testing.expectEqualStrings(want, got);
}

test "cli stash: snapshot keeps the tree, diff shows hunks, apply --merge folds into a dirty file (exit 5 on conflict), goto moves to the entry's check-in" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f", "a\nb\nc\nd\ne\n");
    try addManaged(gpa, io, tmp.dir, &.{"f"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c1" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    try writeFile(io, tmp.dir, "f", "a\nb\nc\nd\nE\n");
    var snap = try runZit(gpa, io, tmp.dir, &.{ "stash", "snapshot", "-m", "snap" });
    defer freeResult(gpa, &snap);
    try expectExitDiag(snap.term, 0, snap);
    try expectFileContent(gpa, io, tmp.dir, "f", "a\nb\nc\nd\nE\n"); // untouched

    var d = try runZit(gpa, io, tmp.dir, &.{ "stash", "diff" });
    defer freeResult(gpa, &d);
    try expectExitDiag(d.term, 0, d);
    try std.testing.expect(std.mem.indexOf(u8, d.stdout, "-e\n+E\n") != null);

    // A dirty file: plain apply refuses; --merge folds a change to a distant
    // line in cleanly; a colliding change leaves markers and exits 5.
    try writeFile(io, tmp.dir, "f", "A\nb\nc\nd\ne\n");
    var refuse = try runZit(gpa, io, tmp.dir, &.{ "stash", "apply" });
    defer freeResult(gpa, &refuse);
    try expectExit(refuse.term, 1);
    var folded = try runZit(gpa, io, tmp.dir, &.{ "stash", "apply", "--merge" });
    defer freeResult(gpa, &folded);
    try expectExitDiag(folded.term, 0, folded);
    try expectFileContent(gpa, io, tmp.dir, "f", "A\nb\nc\nd\nE\n");
    try writeFile(io, tmp.dir, "f", "a\nb\nc\nd\nX\n");
    var clash = try runZit(gpa, io, tmp.dir, &.{ "stash", "apply", "--merge" });
    defer freeResult(gpa, &clash);
    try expectExit(clash.term, 5);
    try std.testing.expect(std.mem.indexOf(u8, clash.stderr, "conflict f") != null);
    var lst = try runZit(gpa, io, tmp.dir, &.{ "stash", "list" });
    defer freeResult(gpa, &lst);
    try std.testing.expect(std.mem.indexOf(u8, lst.stdout, "snap") != null); // kept

    // goto: from a later check-in, back to the entry's own and applied.
    var rv = try runZit(gpa, io, tmp.dir, &.{"revert"});
    defer freeResult(gpa, &rv);
    try expectExit(rv.term, 0);
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c2", "--allow-empty" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);
    var gt = try runZit(gpa, io, tmp.dir, &.{ "stash", "goto" });
    defer freeResult(gpa, &gt);
    try expectExitDiag(gt.term, 0, gt);
    try expectFileContent(gpa, io, tmp.dir, "f", "a\nb\nc\nd\nE\n");
    const c1_hash = try checkinHash(c1.stdout);
    var st = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &st);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, cli.shortHash(c1_hash)) != null);
}

test "cli stash: save captures A/M/D and restores the check-in; apply is fail-closed; pop round-trips exactly" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "keep\n");
    try writeFile(io, tmp.dir, "b.txt", "drop\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    const c1 = try commitHashDup(gpa, io, tmp.dir, "base");
    defer gpa.free(c1);

    // Dirty the tree three ways: edit, rm, add.
    try writeFile(io, tmp.dir, "a.txt", "edited\n");
    {
        var rm = try runZit(gpa, io, tmp.dir, &.{ "rm", "b.txt" });
        defer freeResult(gpa, &rm);
        try expectExit(rm.term, 0);
    }
    try writeFile(io, tmp.dir, "c.txt", "brand-new\n");
    try addManaged(gpa, io, tmp.dir, &.{"c.txt"});

    // Save: entry recorded, tree restored to the check-in exactly.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "stash", "-m", "wip" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "stashed a.txt, b.txt, c.txt as stash 1") != null);
    }
    try expectFileContent(gpa, io, tmp.dir, "a.txt", "keep\n");
    try expectFileContent(gpa, io, tmp.dir, "b.txt", "drop\n");
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "c.txt", .{}));
    {
        var st = try runZit(gpa, io, tmp.dir, &.{"status"});
        defer freeResult(gpa, &st);
        try expectExit(st.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, st.stdout, "nothing to report") != null);
    }
    {
        var show = try runZit(gpa, io, tmp.dir, &.{ "stash", "show" });
        defer freeResult(gpa, &show);
        try expectExit(show.term, 0);
        try std.testing.expectEqualStrings("M a.txt\nD b.txt\nA c.txt\n", show.stdout);
    }

    // Fail-closed apply: a dirty target refuses before anything moves.
    try writeFile(io, tmp.dir, "a.txt", "conflicting\n");
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "stash", "apply" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not clean") != null);
        // Nothing moved: b.txt intact, c.txt still absent.
        try expectFileContent(gpa, io, tmp.dir, "b.txt", "drop\n");
        try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "c.txt", .{}));
    }
    try writeFile(io, tmp.dir, "a.txt", "keep\n");

    // Pop: the exact working state returns, entry drops, sidecar vanishes.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "stash", "pop" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    try expectFileContent(gpa, io, tmp.dir, "a.txt", "edited\n");
    try expectFileContent(gpa, io, tmp.dir, "c.txt", "brand-new\n");
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "b.txt", .{}));
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, ".zit-stash", .{}));

    // ...and committing the restored state carries exactly the popped set.
    const c2 = try commitHashDup(gpa, io, tmp.dir, "resumed");
    defer gpa.free(c2);
    var ls = try runZit(gpa, io, tmp.dir, &.{ "ls", c2 });
    defer freeResult(gpa, &ls);
    try expectExit(ls.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "a.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "c.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "b.txt") == null);
}

test "cli stash: apply refuses a checkout positioned at a different check-in" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    const c1 = try commitHashDup(gpa, io, tmp.dir, "first");
    defer gpa.free(c1);
    try writeFile(io, tmp.dir, "a.txt", "wip\n");
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"stash"});
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    try writeFile(io, tmp.dir, "a.txt", "two\n");
    const c2 = try commitHashDup(gpa, io, tmp.dir, "second");
    defer gpa.free(c2);
    var res = try runZit(gpa, io, tmp.dir, &.{ "stash", "apply" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "update there first") != null);
}

test "cli commit delta dialect: a repository carrying delta manifests keeps emitting them — baseline reused, carried files complete" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    for ([_][]const u8{ "f1.txt", "f2.txt", "f3.txt", "f4.txt" }) |name| {
        try writeFile(io, tmp.dir, name, name);
        try addManaged(gpa, io, tmp.dir, &.{name});
    }
    const c1 = try commitHashDup(gpa, io, tmp.dir, "baseline");
    defer gpa.free(c1);

    // Hand-author a DELTA child of c1 (fossil-imported shape): new f1, f3
    // deleted, f2/f4 carried by the baseline. Raw append, exactly as a sync
    // ingest would land it.
    const f1v2 = "f1 second version";
    var blob_digest: [32]u8 = undefined;
    std.crypto.hash.sha3.Sha3_256.hash(f1v2, &blob_digest, .{});
    var blob_hex_buf: [64]u8 = undefined;
    const blob_hex = std.fmt.bufPrint(&blob_hex_buf, "{x}", .{&blob_digest}) catch unreachable;
    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(gpa);
    try body.print(gpa, "B {s}\nC delta\nD 2030-01-01T00:00:00\nF f1.txt {s}\nF f3.txt\nP {s}\nU u\n", .{ c1, blob_hex, c1 });
    var seal_buf: [32]u8 = undefined;
    const seal = artifact.selfChecksum(body.items, &seal_buf);
    try body.print(gpa, "Z {s}\n", .{seal});
    var name_buf: [64]u8 = undefined;
    const delta_id = artifact.artifactName(.sha3_256, body.items, &name_buf);
    {
        var file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_write });
        defer file.close(io);
        const st = try file.stat(io);
        var fs = storage.FileStorage.init(io, file, st.size);
        var log = try logstore.LogStore.open(gpa, fs.storageFile(), 0);
        defer log.deinit(gpa);
        _ = try log.put(&blob_digest, f1v2);
        var mdigest: [32]u8 = undefined;
        _ = try std.fmt.hexToBytes(&mdigest, delta_id);
        _ = try log.put(&mdigest, body.items);
        try log.commit();
    }

    // Materialize the delta child (update resolves the baseline): f3 gone,
    // f1 at its second version, f2/f4 carried.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "update", delta_id });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "f3.txt", .{}));

    // Commit a change atop the delta parent. The dialect continues: the new
    // manifest is a DELTA against c1 (the parent's own baseline — §4.7.3,
    // never a delta of a delta).
    try writeFile(io, tmp.dir, "f2.txt", "f2 changed");
    const c3 = try commitHashDup(gpa, io, tmp.dir, "child atop delta");
    defer gpa.free(c3);
    var art = try runZit(gpa, io, tmp.dir, &.{ "artifact", c3 });
    defer freeResult(gpa, &art);
    try expectExitDiag(art.term, 0, art);
    var bline_buf: [80]u8 = undefined;
    const bline = try std.fmt.bufPrint(&bline_buf, "B {s}\n", .{c1});
    try std.testing.expect(std.mem.indexOf(u8, art.stdout, bline) != null);
    // The patch names what differs from the BASELINE: f1 (parent's change,
    // carried forward), f2 (this commit), f3 as a delete marker — and NOT
    // f4, which matches the baseline byte-for-byte.
    try std.testing.expect(std.mem.indexOf(u8, art.stdout, "F f1.txt ") != null);
    try std.testing.expect(std.mem.indexOf(u8, art.stdout, "F f2.txt ") != null);
    try std.testing.expect(std.mem.indexOf(u8, art.stdout, "F f3.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, art.stdout, "F f4.txt") == null);

    // Completeness on the derived surface: every carried file is in the
    // child's effective set. Before the parent-baseline resolution landed, a
    // delta parent's raw PATCH stood in for its file set — masked on the
    // default path by whole-tree capture, but wrong as the baseline diff's
    // ground truth, which this enumeration pins.
    var ls = try runZit(gpa, io, tmp.dir, &.{ "ls", c3 });
    defer freeResult(gpa, &ls);
    try expectExitDiag(ls.term, 0, ls);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "f1.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "f2.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "f4.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "f3.txt") == null);
    try std.testing.expectEqual(@as(usize, 3), std.mem.count(u8, ls.stdout, "\n"));
}

test "cli diff: default output is unified hunks — worktree edits, rev-to-rev, added and deleted files, all through one renderer" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\ntwo\nthree\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    const c1 = try commitHashDup(gpa, io, tmp.dir, "first");
    defer gpa.free(c1);

    // Worktree hunks: one changed line, three context... file too short for
    // full context — the hunk covers what exists.
    try writeFile(io, tmp.dir, "a.txt", "one\nTWO\nthree\n");
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"diff"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(
            "Index: a.txt\n==================================================================\n--- a.txt\n+++ a.txt\n@@ -1,3 +1,3 @@\n one\n-two\n+TWO\n three\n",
            res.stdout,
        );
    }

    // Rev-to-rev hunks with an added file: /dev/null headers on the new side.
    try writeFile(io, tmp.dir, "b.txt", "born\n");
    try addManaged(gpa, io, tmp.dir, &.{"b.txt"});
    const c2 = try commitHashDup(gpa, io, tmp.dir, "second");
    defer gpa.free(c2);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--from", c1, "--to", c2 });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(
            "Index: a.txt\n==================================================================\n--- a.txt\n+++ a.txt\n@@ -1,3 +1,3 @@\n one\n-two\n+TWO\n three\n" ++
                "Index: b.txt\n==================================================================\n--- /dev/null\n+++ b.txt\n@@ -0,0 +1 @@\n+born\n",
            res.stdout,
        );
    }
    // Reversed: the added file renders as a deletion against /dev/null.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--from", c2, "--to", c1 });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "Index: b.txt\n==================================================================\n--- b.txt\n+++ /dev/null\n@@ -1 +0,0 @@\n-born\n") != null);
    }
}

test "cli diff -w: whitespace-only changes vanish from hunks and from --brief, in both modes" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "ws.txt", "alpha\nbeta\n");
    try writeFile(io, tmp.dir, "real.txt", "keep\n");
    try addManaged(gpa, io, tmp.dir, &.{ "ws.txt", "real.txt" });
    const c1 = try commitHashDup(gpa, io, tmp.dir, "first");
    defer gpa.free(c1);

    // Reindent ws.txt only: a real byte change that is entirely whitespace.
    try writeFile(io, tmp.dir, "ws.txt", "    alpha\nbeta\t\n");

    // Worktree, hunks: visible without -w, gone with it.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"diff"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "ws.txt") != null);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "-w" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("", res.stdout);
    }
    // Worktree, --brief: the file must not be listed at all under -w, and the
    // long spelling reaches the same path.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "ws.txt") != null);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief", "--ignore-whitespace" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("", res.stdout);
    }

    // A genuine edit alongside the whitespace one: -w must still report it,
    // and must report ONLY it. This is what stops -w from being "show nothing".
    try writeFile(io, tmp.dir, "real.txt", "changed\n");
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief", "-w" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "real.txt") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "ws.txt") == null);
    }

    // Rev-to-rev: same two questions across a commit boundary.
    const c2 = try commitHashDup(gpa, io, tmp.dir, "second");
    defer gpa.free(c2);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--from", c1, "--to", c2, "-w" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "real.txt") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "ws.txt") == null);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--from", c1, "--to", c2, "--brief", "-w" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("M real.txt\n", res.stdout);
    }
    // Without -w the same brief pair lists both files, so the assertions above
    // are about the flag and not about the fixture.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--from", c1, "--to", c2, "--brief" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "ws.txt") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "real.txt") != null);
    }
}

test "cli diff: --from/--to compares two check-ins by name-status, in both directions; a lone flag is a usage error" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try writeFile(io, tmp.dir, "b.txt", "gone later\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    const c1 = try commitHashDup(gpa, io, tmp.dir, "first");
    defer gpa.free(c1);

    try writeFile(io, tmp.dir, "a.txt", "two\n");
    try writeFile(io, tmp.dir, "c.txt", "new\n");
    try addManaged(gpa, io, tmp.dir, &.{"c.txt"});
    {
        var rm = try runZit(gpa, io, tmp.dir, &.{ "rm", "b.txt" });
        defer freeResult(gpa, &rm);
        try expectExit(rm.term, 0);
    }
    const c2 = try commitHashDup(gpa, io, tmp.dir, "second");
    defer gpa.free(c2);

    // Forward: modified, deleted, added — in canonical path order.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief", "--from", c1, "--to", c2 });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("M a.txt\nD b.txt\nA c.txt\n", res.stdout);
    }
    // Reversed operands: A and D swap, M stays.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief", "--from", c2, "--to", c1 });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("M a.txt\nA b.txt\nD c.txt\n", res.stdout);
    }
    // Identical operands: silence.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief", "--from", c1, "--to", c1 });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("", res.stdout);
    }
    // The pair is required together.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--from", c1 });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "--from and --to must be given together") != null);
    }
}

test "cli diff: a clean committed file is silent and arbitrary untracked paths are omitted" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "tracked.txt", "same\n");
    try addManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    try writeFile(io, tmp.dir, "untracked.txt", "ignored\n");

    var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli diff: a changed committed managed file is modified and leaves repository metadata byte-identical" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "before\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    try writeFile(io, tmp.dir, "a.txt", "after\n");

    const before_zit = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(before_zit);
    const before_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(before_checkout);
    const before_managed = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(before_managed);

    var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("M a.txt\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);

    const after_zit = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(after_zit);
    const after_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(after_checkout);
    const after_managed = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(after_managed);
    try std.testing.expectEqualSlices(u8, before_zit, after_zit);
    try std.testing.expectEqualSlices(u8, before_checkout, after_checkout);
    try std.testing.expectEqualSlices(u8, before_managed, after_managed);
}

test "cli diff: a baseline file removed from management is deleted and mixed changes are path-sorted" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "a0\n");
    try writeFile(io, tmp.dir, "b", "b0\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a", "b" });
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    try writeFile(io, tmp.dir, "a", "a1\n");
    try writeFile(io, tmp.dir, "c", "c0\n");
    try addManaged(gpa, io, tmp.dir, &.{"c"});
    var remove = try runZit(gpa, io, tmp.dir, &.{ "rm", "b" });
    defer freeResult(gpa, &remove);
    try expectExit(remove.term, 0);

    var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("M a\nD b\nA c\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try std.testing.expect(fileExists(io, tmp.dir, "b")); // `rm` forgets, it does not unlink
}

test "cli diff: a managed filename uses section 4.3 escapes" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    const filename = "a b\tc\\d";
    try writeFile(io, tmp.dir, filename, "new\n");
    try addManaged(gpa, io, tmp.dir, &.{filename});

    var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("A a\\sb\\tc\\\\d\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli diff: a lowercase SHA-1 baseline is clean unchanged and modified when altered" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    const original = "same\n";
    try writeFile(io, tmp.dir, "sha1.txt", original);
    try addManaged(gpa, io, tmp.dir, &.{"sha1.txt"});

    const blob = try appendArtifactAs(gpa, io, tmp.dir, .sha1, original);
    defer gpa.free(blob);
    const f = [_]artifact.FCard{.{
        .filename = "sha1.txt",
        .hash = blob,
        .perm = null,
        .old_name = null,
        .mode = .regular,
    }};
    var manifest: artifact.Manifest = .{
        .arena_state = std.heap.ArenaAllocator.init(gpa),
        .kind = .manifest,
        .a = null,
        .b = null,
        .c = .{ .comment = "sha1-root" },
        .d = .{ .timestamp = "2020-01-01T00:00:00" },
        .e = null,
        .g = null,
        .h = null,
        .i = null,
        .k = null,
        .l = null,
        .n = null,
        .r = null,
        .u = .{ .login = "u" },
        .z = null,
        .w = null,
        .f = &f,
        .j = &.{},
        .m = &.{},
        .p = &.{},
        .q = &.{},
        .t = &.{},
    };
    defer manifest.deinit();
    const manifest_bytes = try artifact.create(&manifest, gpa);
    defer gpa.free(manifest_bytes);
    const checkin = try appendArtifactAs(gpa, io, tmp.dir, .sha1, manifest_bytes);
    defer gpa.free(checkin);
    try std.testing.expectEqual(@as(usize, 40), checkin.len);
    for (checkin) |ch| try std.testing.expect(std.ascii.isHex(ch) and !std.ascii.isUpper(ch));
    try seedCheckout(io, tmp.dir, checkin, "trunk");

    var clean = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &clean);
    try expectExit(clean.term, 0);
    try std.testing.expectEqualStrings("", clean.stdout);
    try std.testing.expectEqualStrings("", clean.stderr);

    try writeFile(io, tmp.dir, "sha1.txt", "changed\n");
    var changed = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &changed);
    try expectExit(changed.term, 0);
    try std.testing.expectEqualStrings("M sha1.txt\n", changed.stdout);
    try std.testing.expectEqualStrings("", changed.stderr);
}

test "cli diff: a managed path replaced by a symlink is deleted without following its target" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "target", "same\n");
    try writeFile(io, tmp.dir, "link", "same\n");
    try addManaged(gpa, io, tmp.dir, &.{"link"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    try tmp.dir.deleteFile(io, "link");
    try tmp.dir.symLink(io, "target", "link", .{});

    var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("D link\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli diff: executable-bit-only change to a managed regular file is modified" {
    if (comptime !File.Permissions.has_executable_bit) return error.SkipZigTest;

    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const original = "#!/bin/zit\nsame\n";
    try writeFile(io, tmp.dir, "script", original);
    try addManaged(gpa, io, tmp.dir, &.{"script"});
    {
        var script = try tmp.dir.openFile(io, "script", .{ .mode = .read_write });
        defer script.close(io);
        try script.setPermissions(io, File.Permissions.executable_file);
    }
    const blob = try appendArtifactAs(gpa, io, tmp.dir, .sha1, original);
    defer gpa.free(blob);
    const files = [_]artifact.FCard{.{
        .filename = "script",
        .hash = blob,
        .perm = "x",
        .old_name = null,
        .mode = .executable,
    }};
    const baseline = try appendDiffManifest(gpa, io, tmp.dir, "executable-root", null, &files);
    defer gpa.free(baseline);
    var stored_baseline = try parseCheckin(gpa, io, tmp.dir, baseline);
    defer stored_baseline.deinit();
    try std.testing.expectEqual(artifact.FileMode.executable, (findF(stored_baseline.m, "script") orelse return error.MissingFCard).mode);
    try seedCheckout(io, tmp.dir, baseline, "trunk");

    var clean = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &clean);
    try expectExit(clean.term, 0);
    try std.testing.expectEqualStrings("", clean.stdout);
    try std.testing.expectEqualStrings("", clean.stderr);

    {
        var script = try tmp.dir.openFile(io, "script", .{ .mode = .read_write });
        defer script.close(io);
        try script.setPermissions(io, File.Permissions.default_file);
    }
    var changed = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &changed);
    try expectExit(changed.term, 0);
    try std.testing.expectEqualStrings("M script\n", changed.stdout);
    try std.testing.expectEqualStrings("", changed.stderr);
}

test "cli commit §4.7.6: chmod +x on a managed file is captured, recorded once, then quiesces" {
    if (comptime !File.Permissions.has_executable_bit) return error.SkipZigTest;

    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // A regular managed file, committed through zit's own capture path: §4.7.6
    // permits omitting the perm token for a regular file, so its F card is
    // mode-regular with no perm.
    try writeFile(io, tmp.dir, "script", "#!/bin/sh\necho hi\n");
    try addManaged(gpa, io, tmp.dir, &.{"script"});
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "regular" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);
    const root_hash = try gpa.dupe(u8, try checkinHash(root.stdout));
    defer gpa.free(root_hash);
    {
        var mc = try parseCheckin(gpa, io, tmp.dir, root_hash);
        defer mc.deinit();
        const f = findF(mc.m, "script") orelse return error.MissingFCard;
        try std.testing.expectEqual(artifact.FileMode.regular, f.mode);
        try std.testing.expectEqual(@as(?[]const u8, null), f.perm);
    }

    // chmod +x — the content is unchanged; only the executable bit flips.
    {
        var script = try tmp.dir.openFile(io, "script", .{ .mode = .read_write });
        defer script.close(io);
        try script.setPermissions(io, File.Permissions.executable_file);
    }

    // The drift surfaces in diff exactly once (metadata-only M, no content read).
    var drift = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &drift);
    try expectExit(drift.term, 0);
    try std.testing.expectEqualStrings("M script\n", drift.stdout);
    try std.testing.expectEqualStrings("", drift.stderr);

    // commit records the mode change: a real child whose F card carries an `x`
    // permission token (§4.7.6 — a token containing `x` marks the entry
    // executable), not a silent revert to regular.
    var rec = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "chmod +x" });
    defer freeResult(gpa, &rec);
    try expectExit(rec.term, 0);
    const child_hash = try checkinHash(rec.stdout);
    try std.testing.expect(!std.mem.eql(u8, child_hash, root_hash));
    {
        var mc = try parseCheckin(gpa, io, tmp.dir, child_hash);
        defer mc.deinit();
        const f = findF(mc.m, "script") orelse return error.MissingFCard;
        try std.testing.expectEqual(artifact.FileMode.executable, f.mode);
        try std.testing.expectEqualStrings("x", f.perm.?);
    }

    // The tree quiesces: diff is clean and a re-commit is a no-op (B-commit-9),
    // where before the fix the drift re-appeared on every diff forever.
    var clean = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &clean);
    try expectExit(clean.term, 0);
    try std.testing.expectEqualStrings("", clean.stdout);
    try std.testing.expectEqualStrings("", clean.stderr);

    var noop = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "again" });
    defer freeResult(gpa, &noop);
    try expectExit(noop.term, 1);
    try std.testing.expectEqualStrings("", noop.stdout);
    try std.testing.expectEqualStrings("zit commit: nothing to commit (no changes)\n", noop.stderr);
}

test "cli diff: a delta checkout resolves inherited managed files through its B-card" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const original = "same\n";
    try writeFile(io, tmp.dir, "inherited.txt", original);
    try addManaged(gpa, io, tmp.dir, &.{"inherited.txt"});
    const blob = try appendArtifactAs(gpa, io, tmp.dir, .sha1, original);
    defer gpa.free(blob);
    const files = [_]artifact.FCard{.{
        .filename = "inherited.txt",
        .hash = blob,
        .perm = null,
        .old_name = null,
        .mode = .regular,
    }};
    const baseline = try appendDiffManifest(gpa, io, tmp.dir, "full-root", null, &files);
    defer gpa.free(baseline);
    const delta = try appendDiffManifest(gpa, io, tmp.dir, "delta", baseline, &.{});
    defer gpa.free(delta);
    var stored_delta = try parseCheckin(gpa, io, tmp.dir, delta);
    defer stored_delta.deinit();
    try std.testing.expectEqualStrings(baseline, stored_delta.m.b.?.hash);
    try std.testing.expectEqual(@as(usize, 0), stored_delta.m.f.len);
    try seedCheckout(io, tmp.dir, delta, "trunk");

    var clean = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &clean);
    try expectExit(clean.term, 0);
    try std.testing.expectEqualStrings("", clean.stdout);
    try std.testing.expectEqualStrings("", clean.stderr);

    try writeFile(io, tmp.dir, "inherited.txt", "changed\n");
    var changed = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &changed);
    try expectExit(changed.term, 0);
    try std.testing.expectEqualStrings("M inherited.txt\n", changed.stdout);
    try std.testing.expectEqualStrings("", changed.stderr);
}

test "cli diff: outside a repository fails with the shared discovery diagnostic" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit: no zit checkout here (looked for .zit-checkout in this directory and its parents)\n" ++
            "if this is a bare repository, name it with -R, or run 'zit open' to create a working tree\n",
        res.stderr,
    );
}

test "cli diff: a corrupt .zit-checkout fails without a partial diff" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    {
        var f = try tmp.dir.createFile(io, checkout.filename, .{});
        defer f.close(io);
        try f.writePositionalAll(io, &.{ 0xff, 0xff, 0xaa }, 0);
    }

    var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit diff: cannot read checkout state: .zit-checkout is corrupt\n",
        res.stderr,
    );
}

test "cli diff: a corrupt .zit-managed fails without a partial diff" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try corruptSection(io, tmp.dir, .managed);

    var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit diff: cannot read tracked files: .zit-checkout is corrupt\n",
        res.stderr,
    );
}

test "cli diff: a current delta checkout with an absent B-card baseline fails without a partial diff" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    const absent_baseline = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
    const delta = try appendDiffManifest(gpa, io, tmp.dir, "missing-baseline", absent_baseline, &.{});
    defer gpa.free(delta);
    try seedCheckout(io, tmp.dir, delta, "trunk");

    var res = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit diff: cannot resolve baseline\n", res.stderr);
}

test "cli diff: help is the sole option and invalid arguments are usage errors" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    inline for (.{ "--help", "-h" }) |flag| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var help = try runZit(gpa, io, tmp.dir, &.{ "diff", flag });
        defer freeResult(gpa, &help);
        try expectExit(help.term, 0);
        try std.testing.expect(std.mem.startsWith(u8, help.stdout, "usage: zit diff [--brief|--numstat|--by] [-w|-Z] [-c N] [--invert] [-N]\n                [--from CHECKIN --to CHECKIN | --checkin V | --branch B]\n"));
        try std.testing.expect(std.mem.indexOf(u8, help.stdout, "--exit-code") == null);
        try std.testing.expectEqualStrings("", help.stderr);
    }

    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var unknown = try runZit(gpa, io, tmp.dir, &.{ "diff", "-x" });
        defer freeResult(gpa, &unknown);
        try expectExit(unknown.term, 2);
        try std.testing.expectEqualStrings("", unknown.stdout);
        try std.testing.expectEqualStrings(
            "zit diff: unknown option '-x'\ntry 'zit diff --help'\n",
            unknown.stderr,
        );
    }

    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var positional = try runZit(gpa, io, tmp.dir, &.{ "diff", "--from" }); // a value-less flag; bare words are PATH operands now
        defer freeResult(gpa, &positional);
        try expectExit(positional.term, 2);
        try std.testing.expectEqualStrings("", positional.stdout);
        try std.testing.expectEqualStrings(
            "zit diff: unknown option '--from'\ntry 'zit diff --help'\n",
            positional.stderr,
        );
    }
}

fn appendDiffManifest(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    label: []const u8,
    baseline: ?[]const u8,
    files: []const artifact.FCard,
) ![]u8 {
    var manifest: artifact.Manifest = .{
        .arena_state = std.heap.ArenaAllocator.init(gpa),
        .kind = .manifest,
        .a = null,
        .b = if (baseline) |hash| .{ .hash = hash } else null,
        .c = .{ .comment = label },
        .d = .{ .timestamp = "2020-01-01T00:00:00" },
        .e = null,
        .g = null,
        .h = null,
        .i = null,
        .k = null,
        .l = null,
        .n = null,
        .r = null,
        .u = .{ .login = "u" },
        .z = null,
        .w = null,
        .f = files,
        .j = &.{},
        .m = &.{},
        .p = &.{},
        .q = &.{},
        .t = &.{},
    };
    defer manifest.deinit();
    const bytes = try artifact.create(&manifest, gpa);
    defer gpa.free(bytes);
    return appendArtifact(gpa, io, dir, bytes);
}
/// Writes a legal non-leaf control artifact that mutates the named target's
/// tag state (a single T card). Synthetic control artifacts are necessary
/// because this public behavior cannot be produced by an existing CLI command.
fn appendTagControl(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    timestamp: []const u8,
    sign: artifact.TSign,
    name: []const u8,
    target: []const u8,
    value: ?[]const u8,
) ![]u8 {
    const cards = [_]artifact.TCard{.{
        .sign = sign,
        .name = name,
        .target = target,
        .value = value,
    }};
    var control: artifact.Manifest = .{
        .arena_state = std.heap.ArenaAllocator.init(gpa),
        .kind = .control,
        .a = null,
        .b = null,
        .c = null,
        .d = .{ .timestamp = timestamp },
        .e = null,
        .g = null,
        .h = null,
        .i = null,
        .k = null,
        .l = null,
        .n = null,
        .r = null,
        .u = .{ .login = "u" },
        .z = null,
        .w = null,
        .f = &.{},
        .j = &.{},
        .m = &.{},
        .p = &.{},
        .q = &.{},
        .t = &cards,
    };
    defer control.deinit();
    const bytes = try artifact.create(&control, gpa);
    defer gpa.free(bytes);
    return appendArtifact(gpa, io, dir, bytes);
}

/// Writes a control artifact that flips `target`'s branch tag — the branch-only
/// wrapper the update tests already rely on.
fn appendBranchControl(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    timestamp: []const u8,
    sign: artifact.TSign,
    target: []const u8,
    value: ?[]const u8,
) ![]u8 {
    return appendTagControl(gpa, io, dir, timestamp, sign, "branch", target, value);
}

/// Writes a control artifact that applies an active `closed` tag to `target`
/// (repo-state §7.4). No CLI verb closes a leaf yet, so the closed state is
/// synthesized directly for the `leaves` enumeration tests.
fn appendClosedControl(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    timestamp: []const u8,
    target: []const u8,
) ![]u8 {
    return appendTagControl(gpa, io, dir, timestamp, .add, "closed", target, null);
}

// ==========================================================================
// `zit commit` end-to-end (behavior ids B-commit-1..20). Each test
// spawns the REAL binary in an isolated tmp dir with a pinned environment
// (TZ=UTC, isolated HOME, and — for reproducibility — a fixed
// SOURCE_DATE_EPOCH and USER), asserts exit code + stdout/stderr
// (structurally around the hash/timestamp), and reads the resulting `.zit` /
// `.zit-checkout` state back THROUGH THE LIBRARY. Teardown is unconditional.
// ==========================================================================

/// A fixed clock for reproducible check-ins: 1234567890 == 2009-02-13T23:31:30Z.
const commit_epoch = "1234567890";
const commit_ts = "2009-02-13T23:31:30.000";

/// Optional environment overrides for a `commit` spawn. Defaults pin a
/// reproducible clock and a known author; a test sets a field to null to
/// exercise its absence (e.g. `.{ .user = null }` for the author-missing path).
const CommitEnv = struct {
    epoch: ?[]const u8 = commit_epoch,
    user: ?[]const u8 = "e2e-user",
    /// `ZIT_PGP_COMMAND` for `commit --sign`. A stub signer keeps the
    /// suite deterministic and key-less: no gpg, no keyring, no expiry.
    pgp: ?[]const u8 = null,
    /// `$ZIT_CONFIG`, the operator-scope settings file. Set it to a path
    /// inside the test's own tmp tree: the global tier is located through the
    /// environment, and the suite must never read or write the real one.
    zit_config: ?[]const u8 = null,
};

/// Spawns `zit <tail...>` with the pinned base environment plus the commit
/// overrides. Caller owns `result.stdout`/`result.stderr`.
/// `runZit` with stdin fed from `stdin_file` (a path under `tmp_dir`):
/// `std.process.run` pins child stdin to `.ignore`, so the redirect goes
/// through `sh -c` with the zit binary quoted absolute. The wiki body and
/// `cone test -`'s path list are the consumers.
fn runZitStdin(
    gpa: std.mem.Allocator,
    io: std.Io,
    tmp_dir: std.Io.Dir,
    stdin_file: []const u8,
    tail: []const []const u8,
) !std.process.RunResult {
    var env = std.process.Environ.Map.init(gpa);
    defer env.deinit();
    try env.put("TZ", "UTC");
    try env.put("HOME", "/zit-e2e-isolated-home-does-not-exist");
    try env.put("PATH", "/usr/bin:/bin");

    const cwd_abs = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd_abs);
    const abs_exe = try std.fs.path.resolve(gpa, &.{ cwd_abs, zit_exe });
    defer gpa.free(abs_exe);

    var cmd: std.ArrayList(u8) = .empty;
    defer cmd.deinit(gpa);
    try cmd.appendSlice(gpa, "exec '");
    try cmd.appendSlice(gpa, abs_exe);
    try cmd.appendSlice(gpa, "'");
    for (tail) |t| {
        try cmd.appendSlice(gpa, " '");
        try cmd.appendSlice(gpa, t);
        try cmd.appendSlice(gpa, "'");
    }
    try cmd.appendSlice(gpa, " < '");
    try cmd.appendSlice(gpa, stdin_file);
    try cmd.appendSlice(gpa, "'");

    return std.process.run(gpa, io, .{
        .argv = &.{ "/bin/sh", "-c", cmd.items },
        .cwd = .{ .dir = tmp_dir },
        .environ_map = &env,
    });
}

fn runCommitEnv(
    gpa: std.mem.Allocator,
    io: std.Io,
    tmp_dir: std.Io.Dir,
    env_opts: CommitEnv,
    tail: []const []const u8,
) !std.process.RunResult {
    var env = std.process.Environ.Map.init(gpa);
    defer env.deinit();
    try env.put("TZ", "UTC");
    try env.put("HOME", "/zit-e2e-isolated-home-does-not-exist");
    try env.put("PATH", "/usr/bin:/bin");
    if (env_opts.epoch) |e| try env.put("SOURCE_DATE_EPOCH", e);
    if (env_opts.user) |u| try env.put("USER", u);
    if (env_opts.pgp) |p| try env.put("ZIT_PGP_COMMAND", p);
    if (env_opts.zit_config) |c| try env.put("ZIT_CONFIG", c);

    const cwd_abs = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd_abs);
    const abs_exe = try std.fs.path.resolve(gpa, &.{ cwd_abs, zit_exe });
    defer gpa.free(abs_exe);

    var argv_buf: [16][]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 = .{ .dir = tmp_dir },
        .environ_map = &env,
    });
}

/// Writes `content` to `name` under `dir` (truncating any existing file), so a
/// second commit sees the edited bytes.
fn writeFile(io: std.Io, dir: std.Io.Dir, name: []const u8, content: []const u8) !void {
    var f = try dir.createFile(io, name, .{});
    defer f.close(io);
    try f.writePositionalAll(io, content, 0);
}

/// Reads `.zit-checkout` through the library and returns whether `version`
/// equals `expected` (empty string ⇒ unborn).
fn expectCheckoutVersion(io: std.Io, dir: std.Io.Dir, expected: []const u8) !void {
    var ckbuf: [checkout.max_ck_payload + 1]u8 = undefined;
    const ck = (try checkout.read(io, dir, &ckbuf)).?;
    try std.testing.expectEqualStrings(expected, ck.version);
}

/// Returns the checkout's current `version` copied onto `gpa` (caller frees).
fn checkoutVersionAlloc(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]u8 {
    var ckbuf: [checkout.max_ck_payload + 1]u8 = undefined;
    const ck = (try checkout.read(io, dir, &ckbuf)).?;
    return gpa.dupe(u8, ck.version);
}

/// A parsed check-in plus the raw manifest bytes it borrows. `Manifest`'s `hash`
/// (F card) and `parents` (P card) fields are RAW slices into these bytes, so the
/// bytes must outlive the manifest — `deinit` frees both, in order.
const Checkin = struct {
    m: artifact.Manifest,
    bytes: []u8,
    gpa: std.mem.Allocator,
    fn deinit(self: *Checkin) void {
        self.m.deinit();
        self.gpa.free(self.bytes);
    }
};

/// Opens `.zit` read-only through the library and parses the artifact named
/// `hash` into a `Checkin` (caller `deinit`s). The e2e state oracle for a minted
/// check-in — never reads Fossil, only zit's own reader.
fn parseCheckin(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, hash: []const u8) !Checkin {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_only });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);
    // The log keys frames by the 32-byte binary digest; the check-in `hash` is
    // its 64-hex name, so decode before lookup (the same bridge `commit` uses).
    var digest: [32]u8 = undefined;
    if (hash.len != 64) return error.CheckinNotFound;
    _ = try std.fmt.hexToBytes(&digest, hash);
    const rid = log.lookup(&digest) orelse return error.CheckinNotFound;
    const bytes = (try log.content(gpa, rid)) orelse return error.CheckinNotFound;
    errdefer gpa.free(bytes);
    const m = try artifact.parse(gpa, bytes);
    return .{ .m = m, .bytes = bytes, .gpa = gpa };
}

/// Finds the F card named `name`, or null.
fn findF(m: artifact.Manifest, name: []const u8) ?artifact.FCard {
    for (m.f) |f| if (std.mem.eql(u8, f.filename, name)) return f;
    return null;
}

/// Extracts the 64-hex artifact hash from a `check-in <hash> on <branch>\n`
/// success line, asserting the fixed structure (never a wall clock).
fn checkinHash(stdout: []const u8) ![]const u8 {
    const prefix = "check-in ";
    try std.testing.expect(std.mem.startsWith(u8, stdout, prefix));
    try std.testing.expect(std.mem.endsWith(u8, stdout, "\n"));
    const rest = stdout[prefix.len .. stdout.len - 1];
    const sp = std.mem.indexOfScalar(u8, rest, ' ') orelse return error.MalformedSuccessLine;
    const hash = rest[0..sp];
    try std.testing.expectEqual(@as(usize, 64), hash.len);
    for (hash) |ch| try std.testing.expect(std.ascii.isHex(ch) and !std.ascii.isUpper(ch));
    return hash;
}

/// An independent transcription of the B-commit-20 §Help block (must match
/// `main.zig`'s `commit_help` byte-for-byte; drift fails the test).
const commit_help_expected =
    \\usage: zit commit [-m <message> | -M <file> | --editor <prog>] [-n|--dry-run]
    \\                  [--allow-empty] [--user <login>] [--branch <name>] [--close]
    \\                  [--allow-fork] [--no-sync] [--sign] [-q|--quiet]
    \\                  [--no-checks|--no-warnings] [--no-submanifest-advice]
    \\                  [--] [<path>...]
    \\
    \\Record a check-in: save the current contents of your tracked files as a
    \\new, permanent point in history on the current branch.
    \\
    \\zit has no staging area. `commit` reads each tracked file straight from
    \\the working tree right now — there is nothing to stage first. With no
    \\paths, every tracked file is captured; name one or more paths to commit
    \\just those (the rest of the tree is carried forward unchanged).
    \\
    \\The first commit in a new repository creates the root check-in and starts
    \\the default branch 'trunk'. Each check-in is named by its full artifact
    \\hash (SHA3-256).
    \\
    \\--branch <name> starts a new branch at this check-in: the check-in moves
    \\onto <name> and later commits inherit it. <name> must not already name an
    \\open branch, and there must be a parent to fork from. --close marks this
    \\check-in a closed leaf, so it no longer shows as an open head of its branch.
    \\
    \\History is immutable: a check-in is never rewritten. There is no amend and
    \\no --force; to undo a file, change it and commit again. To stop tracking a
    \\file (and record its deletion), use `zit rm` then commit.
    \\
    \\The comment comes from -m, from -M <file>, or from an editor: with no
    \\-m on a terminal, commit opens $VISUAL (else $EDITOR, else --editor
    \\<prog>) on a template whose '#' lines name the branch, the parent and
    \\every file this commit captures; those lines are stripped, and an
    \\empty comment aborts. The template lives in .zit-ci-comment in the
    \\checkout root and stays there when the commit fails after the editor,
    \\so 'commit -M .zit-ci-comment' retries without retyping. Off a
    \\terminal, a missing -m is still a usage error. -n reports what would
    \\be recorded and writes nothing. Only regular files are captured.
    \\
    \\On a repository with sub-manifests enabled and pinned, a commit that
    \\drifts far enough from the analyzed optimum (draft-fossil-submanifests-00
    \\§6.4) prints one advisory line and, interactively, offers to retune.
    \\"Far enough" is zit's §6.4 relative margin m: a re-tune is advised only
    \\when it would improve the analyzed read cost by more than 15% of the
    \\pinned value's own cost. --no-submanifest-advice suppresses the advisory
    \\entirely. Never affects whether the commit itself succeeds.
    \\
    \\options:
    \\  -m, --message <message>   the check-in comment
    \\  -M, --message-file <file> read the comment from <file>
    \\      --editor <prog>       compose the comment in <prog> (default:
    \\                            $VISUAL, then $EDITOR, on a terminal)
    \\  -n, --dry-run             list what would be recorded; write nothing
    \\      --allow-empty         record a check-in with no file changes
    \\      --user <login>        author to record (default: $USER)
    \\      --branch <name>       start a new branch at this check-in
    \\      --close               mark this check-in a closed leaf
    \\      --allow-fork          commit even though the current check-in is no
    \\                            longer the tip of its branch — a deliberate
    \\                            fork (refused otherwise, naming the newer tip)
    \\      --no-sync             skip autosync (the pull before and push
    \\                            after) for this one commit
    \\      --no-checks           waive the binary-glob/crlf-glob/encoding-glob
    \\                            content gate for this one commit
    \\                            (--no-warnings is Fossil's spelling of the
    \\                            same waiver)
    \\      --sign                clearsign the manifest before storing it.
    \\                            The signed text IS the stored artifact, so
    \\                            every reader strips the wrapper to reach
    \\                            the cards and the signature travels with
    \\                            the check-in wherever it syncs.
    \\                            The signer is $ZIT_PGP_COMMAND, fossil's
    \\                            pgp-command contract: default
    \\                            'gpg --clearsign -o ', invoked as
    \\                            COMMAND OUTFILE INFILE; a command whose
    \\                            basename starts with 'ssh' is invoked as
    \\                            COMMAND INFILE, writes INFILE.sig, and its
    \\                            armor is assembled here. 'off' disables
    \\                            signing. Refused — never committed
    \\                            unsigned — if the signer fails or alters
    \\                            the manifest it was given.
    \\  -q, --quiet               do not print the check-in line
    \\      --no-submanifest-advice
    \\                            suppress the sub-manifest drift advisory
    \\  -h, --help                show this help and exit
    \\
    \\exit status:
    \\  0  the check-in was created
    \\  1  nothing to commit, a tracked file is missing, an empty message, an
    \\     existing branch name, or this is not a zit repository
    \\  2  usage error (no message, or an unknown option)
    \\
    \\aliases: ci, checkin
    \\
;

test "cli commit B-commit-18a: --help and -h print the help to stdout, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var r1 = try runCommitEnv(gpa, io, tmp.dir, .{}, &.{ "commit", "--help" });
    defer freeResult(gpa, &r1);
    try expectExit(r1.term, 0);
    try std.testing.expectEqualStrings(commit_help_expected, r1.stdout);
    try std.testing.expectEqualStrings("", r1.stderr);

    // -h wins even mixed with other tokens (no repo needed).
    var r2 = try runCommitEnv(gpa, io, tmp.dir, .{}, &.{ "commit", "-m", "x", "-h", "-z" });
    defer freeResult(gpa, &r2);
    try expectExit(r2.term, 0);
    try std.testing.expectEqualStrings(commit_help_expected, r2.stdout);
}

// --- commit --branch / --close (repostate §6.2, artifact §4.7) --------------

/// True iff `m` carries a T card with the given sign and (sign-stripped) name.
fn hasTag(m: artifact.Manifest, sign: artifact.TSign, name: []const u8) bool {
    for (m.t) |t| {
        if (t.sign == sign and std.mem.eql(u8, t.name, name)) return true;
    }
    return false;
}

test "cli commit --branch repostate §6.1/§6.2: a new check-in starts NAME and becomes its tip" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Root check-in on trunk.
    try writeFile(io, tmp.dir, "f", "v1\n");
    try addManaged(gpa, io, tmp.dir, &.{"f"});
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);
    const root_hash = try gpa.dupe(u8, try checkinHash(root.stdout));
    defer gpa.free(root_hash);

    // A child that forks onto branch 'feature'.
    try writeFile(io, tmp.dir, "f", "v2\n");
    var child = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "--branch", "feature", "-m", "start feature" });
    defer freeResult(gpa, &child);
    try expectExit(child.term, 0);
    // The success line names the branch it landed on (§6.1).
    try std.testing.expect(std.mem.endsWith(u8, child.stdout, " on feature\n"));
    const child_hash = try gpa.dupe(u8, try checkinHash(child.stdout));
    defer gpa.free(child_hash);
    try std.testing.expect(!std.mem.eql(u8, child_hash, root_hash));

    // §6.2 branch-creation cards: the propagating branch/sym tags plus the
    // sym-trunk cancellation, byte-checked in the minted manifest.
    {
        var mc = try parseCheckin(gpa, io, tmp.dir, child_hash);
        defer mc.deinit();
        try std.testing.expect(hasTag(mc.m, .propagate, "branch"));
        try std.testing.expect(hasTag(mc.m, .propagate, "sym-feature"));
        try std.testing.expect(hasTag(mc.m, .cancel, "sym-trunk"));
        // The branch card carries NAME as its value.
        for (mc.m.t) |t| {
            if (t.sign == .propagate and std.mem.eql(u8, t.name, "branch"))
                try std.testing.expectEqualStrings("feature", t.value.?);
        }
    }

    // Derived view (§6.3/§6.4): both branches exist, feature is current tip.
    var br = try runZit(gpa, io, tmp.dir, &.{"branch"});
    defer freeResult(gpa, &br);
    try expectExit(br.term, 0);
    try std.testing.expectEqualStrings("* feature\n  trunk\n", br.stdout);

    // The branch name resolves to the child check-in (`ls feature` lists it).
    var lf = try runZit(gpa, io, tmp.dir, &.{ "ls", "feature" });
    defer freeResult(gpa, &lf);
    try expectExit(lf.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, lf.stdout, "f\n") != null);
}

test "cli commit --branch repostate §6.2: rejected (exit 1) when NAME already names an open branch" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "f", "v1\n");
    try addManaged(gpa, io, tmp.dir, &.{"f"});
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);

    // Create branch 'feature' (now an open branch).
    try writeFile(io, tmp.dir, "f", "v2\n");
    var first = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "--branch", "feature", "-m", "start" });
    defer freeResult(gpa, &first);
    try expectExit(first.term, 0);

    // A second `--branch feature` must not re-create the existing open branch.
    try writeFile(io, tmp.dir, "f", "v3\n");
    var second = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "--branch", "feature", "-m", "again" });
    defer freeResult(gpa, &second);
    try expectExit(second.term, 1);
    try std.testing.expectEqualStrings("", second.stdout);
    try std.testing.expect(std.mem.indexOf(u8, second.stderr, "already exists") != null);
}

test "cli commit --branch repostate §6.2: rejected (exit 1) on an unborn checkout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // No root yet: there is no parent to fork a branch from (§6.2).
    var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "--branch", "feature", "-m", "x" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "needs a parent") != null);
}

test "cli commit --branch --close repostate §7.4: the check-in is a closed leaf on NAME" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "f", "v1\n");
    try addManaged(gpa, io, tmp.dir, &.{"f"});
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);
    const root_hash = try gpa.dupe(u8, try checkinHash(root.stdout));
    defer gpa.free(root_hash);

    // Fork onto feature AND close the new leaf in one commit.
    try writeFile(io, tmp.dir, "f", "v2\n");
    var child = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "--branch", "feature", "--close", "-m", "closed feature" });
    defer freeResult(gpa, &child);
    try expectExit(child.term, 0);
    const child_hash = try gpa.dupe(u8, try checkinHash(child.stdout));
    defer gpa.free(child_hash);

    // §4.7 +closed is present alongside the branch cards.
    {
        var mc = try parseCheckin(gpa, io, tmp.dir, child_hash);
        defer mc.deinit();
        try std.testing.expect(hasTag(mc.m, .add, "closed"));
        try std.testing.expect(hasTag(mc.m, .propagate, "branch"));
    }

    // §7.4: the closed check-in is not an open leaf, so `leaves` omits it; the
    // still-open trunk root remains.
    var lv = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &lv);
    try expectExit(lv.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, lv.stdout, cli.shortHash(child_hash)) == null);
    try std.testing.expect(std.mem.indexOf(u8, lv.stdout, cli.shortHash(root_hash)) != null);

    // The branch view marks feature current and closed.
    var br = try runZit(gpa, io, tmp.dir, &.{"branch"});
    defer freeResult(gpa, &br);
    try expectExit(br.term, 0);
    try std.testing.expectEqualStrings("* feature  (closed)\n  trunk\n", br.stdout);
}

// --- revert (artifact §8.4 effective file + §7.2 verified blob) -------------

test "cli revert artifact §8.4/§7.2: a modified managed file is restored to its check-in blob byte-for-byte" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const original = "line one\nline two\n";
    try writeFile(io, tmp.dir, "doc.txt", original);
    try addManaged(gpa, io, tmp.dir, &.{"doc.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    // A working edit that revert must discard.
    try writeFile(io, tmp.dir, "doc.txt", "line one\nCHANGED\nline three\n");

    var res = try runZit(gpa, io, tmp.dir, &.{"revert"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("REVERT   doc.txt\n", res.stdout);

    const restored = try readFile(gpa, io, tmp.dir, "doc.txt");
    defer gpa.free(restored);
    try std.testing.expectEqualStrings(original, restored);

    // The tree quiesces: after revert, diff is clean.
    var diff = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &diff);
    try expectExit(diff.term, 0);
    try std.testing.expectEqualStrings("", diff.stdout);
}

test "cli revert §4.7.6: a PATH-scoped revert restores only the named file, leaving other edits" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "a", "a-original\n");
    try writeFile(io, tmp.dir, "b", "b-original\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a", "b" });
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    // Edit both; revert only 'a'.
    try writeFile(io, tmp.dir, "a", "a-edited\n");
    try writeFile(io, tmp.dir, "b", "b-edited\n");
    var res = try runZit(gpa, io, tmp.dir, &.{ "revert", "a" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("REVERT   a\n", res.stdout);

    const a_after = try readFile(gpa, io, tmp.dir, "a");
    defer gpa.free(a_after);
    const b_after = try readFile(gpa, io, tmp.dir, "b");
    defer gpa.free(b_after);
    try std.testing.expectEqualStrings("a-original\n", a_after); // restored
    try std.testing.expectEqualStrings("b-edited\n", b_after); // untouched
}

test "cli revert §4.7.6: a managed file absent from the effective set is reported and skipped, never fabricated" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "committed", "c\n");
    try addManaged(gpa, io, tmp.dir, &.{"committed"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    // 'staged' is managed but never committed — it is not in the check-in's
    // effective set, so revert cannot restore it: reported, left as-is.
    try writeFile(io, tmp.dir, "staged", "s\n");
    try addManaged(gpa, io, tmp.dir, &.{"staged"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "revert", "staged" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0); // a skip is informational, not a hard error
    try std.testing.expectEqualStrings("", res.stdout); // nothing reverted
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not in the current check-in") != null);

    // The working file is left exactly as it was — never fabricated/removed.
    const kept = try readFile(gpa, io, tmp.dir, "staged");
    defer gpa.free(kept);
    try std.testing.expectEqualStrings("s\n", kept);
}

test "cli revert artifact §4.7.6-m5: a deleted executable file is restored with its exec bit" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "script", "#!/bin/sh\necho hi\n");
    try addManaged(gpa, io, tmp.dir, &.{"script"});
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);

    // chmod +x and record the executable mode in a child check-in.
    {
        var f = try tmp.dir.openFile(io, "script", .{ .mode = .read_write });
        defer f.close(io);
        try f.setPermissions(io, File.Permissions.executable_file);
    }
    var exe = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "chmod +x" });
    defer freeResult(gpa, &exe);
    try expectExit(exe.term, 0);

    // Delete the file, then revert recreates it from the check-in.
    try tmp.dir.deleteFile(io, "script");
    var res = try runZit(gpa, io, tmp.dir, &.{ "revert", "script" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("REVERT   script\n", res.stdout);

    // Content restored, and — the §4.7.6-m5 point — the exec bit is set again,
    // so diff is clean (a content- or mode-only drift would surface as M).
    const restored = try readFile(gpa, io, tmp.dir, "script");
    defer gpa.free(restored);
    try std.testing.expectEqualStrings("#!/bin/sh\necho hi\n", restored);
    if (comptime File.Permissions.has_executable_bit) {
        const st = try tmp.dir.statFile(io, "script", .{ .follow_symlinks = false });
        try std.testing.expect(st.permissions.toMode() & 0o111 != 0);
    }
    var diff = try runZit(gpa, io, tmp.dir, &.{ "diff", "--brief" });
    defer freeResult(gpa, &diff);
    try expectExit(diff.term, 0);
    try std.testing.expectEqualStrings("", diff.stdout);
}

test "cli revert artifact §4.7.6-m5: a symlink entry materializes its target" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Track a regular file so the managed set names 'link'. Revert must replace
    // this placeholder with the symlink specified by the check-in.
    try writeFile(io, tmp.dir, "link", "placeholder\n");
    try addManaged(gpa, io, tmp.dir, &.{"link"});

    // Inject a check-in whose F card for 'link' is a symlink. `zit commit`
    // never mints a symlink card, so this directly exercises §4.7.6-m5
    // materialization from the reader's wire representation.
    const blob = try appendArtifact(gpa, io, tmp.dir, "target/path\n");
    defer gpa.free(blob);
    // The wire mode is the F-card perm token ('l' ⇒ symlink, §4.7.6-m5), so it
    // is set explicitly — `mode` alone is not serialized.
    const files = [_]artifact.FCard{.{
        .filename = "link",
        .hash = blob,
        .perm = "l",
        .old_name = null,
        .mode = .symlink,
    }};
    const manifest_hash = try appendDiffManifest(gpa, io, tmp.dir, "symlink check-in", null, &files);
    defer gpa.free(manifest_hash);
    try checkout.write(io, tmp.dir, .{ .version = manifest_hash, .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "revert", "link" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("REVERT   link\n", res.stdout);
    try std.testing.expectEqualStrings(" \"zit undo\" is available to undo changes to the working checkout.\n", res.stderr);

    // readLink verifies the entry is native symlink materialization, rather
    // than a regular file merely containing the target's bytes.
    var target: [std.fs.max_path_bytes]u8 = undefined;
    const target_len = try tmp.dir.readLink(io, "link", &target);
    try std.testing.expectEqualStrings("target/path\n", target[0..target_len]);
}

test "cli revert/undo: -r restores another check-in's content, -n lists without writing, undo -n names the files" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c1" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    try writeFile(io, tmp.dir, "a.txt", "two\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c2" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    try writeFile(io, tmp.dir, "a.txt", "edit\n");

    // -n names the file and writes nothing (no undo journal either).
    var dry = try runZit(gpa, io, tmp.dir, &.{ "revert", "-n" });
    defer freeResult(gpa, &dry);
    try expectExitDiag(dry.term, 0, dry);
    try std.testing.expectEqualStrings("would revert a.txt\n", dry.stdout);
    try expectFileContent(gpa, io, tmp.dir, "a.txt", "edit\n");
    var no_undo = try runZit(gpa, io, tmp.dir, &.{ "undo", "-n" });
    defer freeResult(gpa, &no_undo);
    try expectExit(no_undo.term, 1);

    // -r prev restores c1's bytes without moving the checkout.
    var older = try runZit(gpa, io, tmp.dir, &.{ "revert", "-r", "prev", "a.txt" });
    defer freeResult(gpa, &older);
    try expectExitDiag(older.term, 0, older);
    try expectFileContent(gpa, io, tmp.dir, "a.txt", "one\n");
    var st = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &st);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "EDITED     a.txt") != null);

    // undo -n names what the undo would touch; the tree is unchanged until
    // the real undo, which brings the discarded edit back.
    var peek = try runZit(gpa, io, tmp.dir, &.{ "undo", "-n" });
    defer freeResult(gpa, &peek);
    try expectExitDiag(peek.term, 0, peek);
    try std.testing.expect(std.mem.indexOf(u8, peek.stdout, "undo would restore a.txt") != null);
    try expectFileContent(gpa, io, tmp.dir, "a.txt", "one\n");
    var un = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &un);
    try expectExitDiag(un.term, 0, un);
    try expectFileContent(gpa, io, tmp.dir, "a.txt", "edit\n");

    var bad = try runZit(gpa, io, tmp.dir, &.{ "revert", "-r", "nope" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, bad.stderr, "check-in 'nope' not found") != null);
}

test "cli revert: fails exit 1 on an unborn checkout (no check-in to restore)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"revert"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "unborn") != null);
}

// ==========================================================================
// `zit undo` / `zit redo` — single-level undo/redo (contracts/undo.md). The
// real binary is driven end to end; state is read back through the library.
// ==========================================================================

const undo_help_text =
    \\usage: zit undo [-n|--dry-run] [-h|--help]
    \\
    \\Undo the last working-file change made by 'zit update' or 'zit revert',
    \\restoring every affected file and the checkout record to their state before
    \\that change. Single level: only the most recent change can be undone; a second
    \\'zit undo' in a row has nothing left to undo.
    \\
    \\Use 'zit redo' to reapply a change you just undid. A new update or revert
    \\clears the redo slot.
    \\
    \\options:
    \\  -n, --dry-run name the files (and the checkout record) a undo would
    \\                restore; change nothing
    \\  -h, --help    show this help and exit
    \\
    \\exit status:
    \\  0  the change was undone
    \\  1  nothing to undo, the journal is damaged, or this is not a zit repository
    \\  2  usage error (an unknown option or an unexpected argument)
    \\
;

const redo_help_text =
    \\usage: zit redo [-n|--dry-run] [-h|--help]
    \\
    \\Reapply the change you just undid with 'zit undo', restoring every affected
    \\file and the checkout record to their state after that change. Single level:
    \\redo is available only immediately after an undo; a new update or revert
    \\clears it.
    \\
    \\options:
    \\  -n, --dry-run name the files (and the checkout record) a redo would
    \\                restore; change nothing
    \\  -h, --help    show this help and exit
    \\
    \\exit status:
    \\  0  the change was reapplied
    \\  1  nothing to redo, the journal is damaged, or this is not a zit repository
    \\  2  usage error (an unknown option or an unexpected argument)
    \\
;

/// Commits `content` for `name` and returns the resulting check-in hash borrowed
/// from `commit_out` (kept alive by the caller). Two calls build a root→child
/// history the undo tests reverse.
fn commitFile(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    name: []const u8,
    content: []const u8,
    message: []const u8,
    commit_out: *std.process.RunResult,
) ![]const u8 {
    try writeFile(io, dir, name, content);
    try addManaged(gpa, io, dir, &.{name});
    commit_out.* = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", message });
    try expectExit(commit_out.term, 0);
    return checkinHash(commit_out.stdout);
}

test "cli undo B-undo-1: update then undo restores working files and .zit-checkout byte-for-byte" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    const root = try commitFile(gpa, io, tmp.dir, "tracked.txt", "earlier\n", "earlier", &root_c);
    var child_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &child_c);
    _ = try commitFile(gpa, io, tmp.dir, "tracked.txt", "later\n", "later", &child_c);

    // Pre-update state (the child checkout): the exact bytes undo must
    // restore. The checkout RECORD, not the container file — the container
    // legitimately differs afterwards (its undo section then holds the redo
    // image).
    const before_content = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(before_content);
    const before_checkout = (try checkout.readSectionAlloc(gpa, io, tmp.dir, .checkout)).?;
    defer gpa.free(before_checkout);

    var upd = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &upd);
    try expectExit(upd.term, 0);
    try expectCheckoutVersion(io, tmp.dir, root);

    var un = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &un);
    try expectExit(un.term, 0);
    try std.testing.expect(std.mem.endsWith(u8, un.stdout, "undo: working tree restored\n"));
    try std.testing.expectEqualStrings("", un.stderr);

    const after_content = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(after_content);
    const after_checkout = (try checkout.readSectionAlloc(gpa, io, tmp.dir, .checkout)).?;
    defer gpa.free(after_checkout);
    try std.testing.expectEqualStrings("later\n", after_content);
    try std.testing.expectEqualSlices(u8, before_content, after_content);
    try std.testing.expectEqualSlices(u8, before_checkout, after_checkout);
}

test "cli undo B-undo-2: revert then undo recovers the discarded working bytes" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var c: std.process.RunResult = undefined;
    defer freeResult(gpa, &c);
    _ = try commitFile(gpa, io, tmp.dir, "f.txt", "v1\n", "v1", &c);

    // A dirty working edit revert will discard.
    try writeFile(io, tmp.dir, "f.txt", "edited\n");

    var rev = try runZit(gpa, io, tmp.dir, &.{"revert"});
    defer freeResult(gpa, &rev);
    try expectExit(rev.term, 0);
    {
        const restored = try readFile(gpa, io, tmp.dir, "f.txt");
        defer gpa.free(restored);
        try std.testing.expectEqualStrings("v1\n", restored); // revert restored the blob
    }

    var un = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &un);
    try expectExit(un.term, 0);
    try std.testing.expect(std.mem.endsWith(u8, un.stdout, "undo: working tree restored\n"));
    const recovered = try readFile(gpa, io, tmp.dir, "f.txt");
    defer gpa.free(recovered);
    try std.testing.expectEqualStrings("edited\n", recovered); // discarded edit recovered
}

test "cli redo B-undo-4/5: update then undo then redo returns to the post-update state" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    const root = try commitFile(gpa, io, tmp.dir, "tracked.txt", "earlier\n", "earlier", &root_c);
    var child_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &child_c);
    _ = try commitFile(gpa, io, tmp.dir, "tracked.txt", "later\n", "later", &child_c);

    var upd = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &upd);
    try expectExit(upd.term, 0);
    // Post-update state: tracked.txt == "earlier\n", checkout == root.
    const post_content = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(post_content);
    const post_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(post_checkout);

    var un = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &un);
    try expectExit(un.term, 0);
    try expectCheckoutVersion(io, tmp.dir, try checkinHash(child_c.stdout));

    var re = try runZit(gpa, io, tmp.dir, &.{"redo"});
    defer freeResult(gpa, &re);
    try expectExit(re.term, 0);
    try std.testing.expect(std.mem.endsWith(u8, re.stdout, "redo: working tree restored\n"));
    try std.testing.expectEqualStrings("", re.stderr);

    const back_content = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(back_content);
    const back_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(back_checkout);
    try std.testing.expectEqualStrings("earlier\n", back_content);
    try std.testing.expectEqualSlices(u8, post_content, back_content);
    try std.testing.expectEqualSlices(u8, post_checkout, back_checkout);
}

test "cli undo B-undo-6: a second consecutive undo is exit 1, mutating nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    const root = try commitFile(gpa, io, tmp.dir, "tracked.txt", "earlier\n", "earlier", &root_c);
    var child_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &child_c);
    _ = try commitFile(gpa, io, tmp.dir, "tracked.txt", "later\n", "later", &child_c);

    var upd = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &upd);
    try expectExit(upd.term, 0);

    var un1 = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &un1);
    try expectExit(un1.term, 0);
    const after_first = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(after_first);

    var un2 = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &un2);
    try expectExit(un2.term, 1);
    try std.testing.expectEqualStrings("", un2.stdout);
    try std.testing.expectEqualStrings("zit undo: nothing to undo\n", un2.stderr);
    const after_second = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(after_second);
    try std.testing.expectEqualSlices(u8, after_first, after_second); // no mutation
}

test "cli redo B-undo-7: a new update after undo invalidates redo (exit 1)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    const root = try commitFile(gpa, io, tmp.dir, "tracked.txt", "earlier\n", "earlier", &root_c);
    var child_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &child_c);
    const child = try commitFile(gpa, io, tmp.dir, "tracked.txt", "later\n", "later", &child_c);

    var upd_a = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &upd_a);
    try expectExit(upd_a.term, 0);
    var un = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &un);
    try expectExit(un.term, 0);
    // A second sheltered mutation clears the redo slot the undo populated.
    var upd_b = try runZit(gpa, io, tmp.dir, &.{ "update", child });
    defer freeResult(gpa, &upd_b);
    try expectExit(upd_b.term, 0);

    var re = try runZit(gpa, io, tmp.dir, &.{"redo"});
    defer freeResult(gpa, &re);
    try expectExit(re.term, 1);
    try std.testing.expectEqualStrings("", re.stdout);
    try std.testing.expectEqualStrings("zit redo: nothing to redo\n", re.stderr);
}

test "cli undo B-undo-8: with two updates, undo restores the last one's pre-image, not the earlier" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    const root = try commitFile(gpa, io, tmp.dir, "tracked.txt", "earlier\n", "earlier", &root_c);
    var child_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &child_c);
    const child = try commitFile(gpa, io, tmp.dir, "tracked.txt", "later\n", "later", &child_c);

    // update A (-> root/"earlier"), then update B (-> child/"later").
    var ua = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &ua);
    try expectExit(ua.term, 0);
    var ub = try runZit(gpa, io, tmp.dir, &.{ "update", child });
    defer freeResult(gpa, &ub);
    try expectExit(ub.term, 0);

    // undo restores B's pre-image (the post-A tree: "earlier"/root), NOT A's.
    var un = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &un);
    try expectExit(un.term, 0);
    const content = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(content);
    try std.testing.expectEqualStrings("earlier\n", content);
    try expectCheckoutVersion(io, tmp.dir, root);
}

test "cli undo B-undo-9: an empty journal is exit 1, stdout empty, no mutation" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit undo: nothing to undo\n", res.stderr);
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .undo));
}

test "cli redo B-undo-10: no redo image is exit 1, stdout empty, no mutation" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"redo"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit redo: nothing to redo\n", res.stderr);
}

test "cli undo B-undo-11: a corrupt .zit-undo is exit 1 with no partial restore" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    const root = try commitFile(gpa, io, tmp.dir, "tracked.txt", "earlier\n", "earlier", &root_c);
    var child_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &child_c);
    _ = try commitFile(gpa, io, tmp.dir, "tracked.txt", "later\n", "later", &child_c);

    var upd = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &upd);
    try expectExit(upd.term, 0);
    const before = try readFile(gpa, io, tmp.dir, "tracked.txt"); // "earlier\n"
    defer gpa.free(before);

    // Overwrite the journal with garbage.
    try corruptSection(io, tmp.dir, .undo);

    var un = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &un);
    try expectExit(un.term, 1);
    try std.testing.expectEqualStrings("", un.stdout);
    try std.testing.expectEqualStrings("zit undo: the undo journal is damaged\n", un.stderr);
    const after = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after); // byte-identical, no partial restore
}

test "cli undo B-undo-13: the journal's home '.zit-checkout' is refused; the retired name '.zit-undo' is ordinary content" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // The journal lives in the container now; that name stays untrackable.
    var res = try runZit(gpa, io, tmp.dir, &.{ "add", ".zit-checkout" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit add: '.zit-checkout' is a zit control file and cannot be tracked\n",
        res.stderr,
    );

    // The legacy sibling name lost its reservation with the legacy tier: a
    // user file spelled '.zit-undo' is admissible working-tree content (the
    // container's fold never reads it once a container exists).
    try writeFile(io, tmp.dir, ".zit-undo", "just a file\n");
    var add = try runZit(gpa, io, tmp.dir, &.{ "add", ".zit-undo" });
    defer freeResult(gpa, &add);
    try expectExit(add.term, 0);
    try expectManaged(gpa, io, tmp.dir, &.{".zit-undo"});
}

test "cli undo B-undo-14: update leaves its output/exit unchanged and the journal is invisible in its output" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    const root = try commitFile(gpa, io, tmp.dir, "tracked.txt", "earlier\n", "earlier", &root_c);
    var child_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &child_c);
    _ = try commitFile(gpa, io, tmp.dir, "tracked.txt", "later\n", "later", &child_c);

    var upd = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &upd);
    try expectExit(upd.term, 0);
    try std.testing.expectEqualStrings("", upd.stdout); // update's happy path is silent, as today
    try expectNoDiagnostic(upd.stderr); // the receipt lives on stderr
    // The one new observable: the journal exists, and update never named it.
    try std.testing.expect(sectionExists(gpa, io, tmp.dir, .undo));
    try std.testing.expect(std.mem.indexOf(u8, upd.stdout, ".zit-undo") == null);
    try std.testing.expect(std.mem.indexOf(u8, upd.stderr, ".zit-undo") == null);
}

test "cli undo B-undo-15: update with no undo invoked reproduces today's tree and checkout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    const root = try commitFile(gpa, io, tmp.dir, "tracked.txt", "earlier\n", "earlier", &root_c);
    var child_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &child_c);
    _ = try commitFile(gpa, io, tmp.dir, "tracked.txt", "later\n", "later", &child_c);

    var upd = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &upd);
    try expectExit(upd.term, 0);
    // Exactly today's observable result (matches the pre-journal update e2e).
    const content = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(content);
    try std.testing.expectEqualStrings("earlier\n", content);
    try expectCheckoutVersion(io, tmp.dir, root);
    try expectManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
}

test "cli undo B-undo-16: undo/redo -h/--help print the verbatim help to stdout, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const cases = [_]struct { verb: []const u8, text: []const u8 }{
        .{ .verb = "undo", .text = undo_help_text },
        .{ .verb = "redo", .text = redo_help_text },
    };
    for (cases) |case| {
        for ([_][]const u8{ "--help", "-h" }) |flag| {
            var res = try runZit(gpa, io, tmp.dir, &.{ case.verb, flag });
            defer freeResult(gpa, &res);
            try expectExit(res.term, 0);
            try std.testing.expectEqualStrings(case.text, res.stdout);
            try std.testing.expectEqualStrings("", res.stderr);
        }
        // `-h` wins over an otherwise-invalid trailing token.
        var win = try runZit(gpa, io, tmp.dir, &.{ case.verb, "-h", "bogus" });
        defer freeResult(gpa, &win);
        try expectExit(win.term, 0);
        try std.testing.expectEqualStrings(case.text, win.stdout);
        // `zit help <verb>` teaches identical text (DX D4).
        var routed = try runZit(gpa, io, tmp.dir, &.{ "help", case.verb });
        defer freeResult(gpa, &routed);
        try expectExit(routed.term, 0);
        try std.testing.expectEqualStrings(case.text, routed.stdout);
    }
}

test "cli undo B-undo-17: an unknown option is a usage error (exit 2, stderr try-help, no journal read)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const cases = [_]struct { verb: []const u8, arg: []const u8 }{
        .{ .verb = "undo", .arg = "--force" },
        .{ .verb = "redo", .arg = "-x" },
    };
    for (cases) |case| {
        var res = try runZit(gpa, io, tmp.dir, &.{ case.verb, case.arg });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        var buf: [128]u8 = undefined;
        const expected = try std.fmt.bufPrint(&buf, "zit {s}: unknown option '{s}'\ntry 'zit {s} --help'\n", .{ case.verb, case.arg, case.verb });
        try std.testing.expectEqualStrings(expected, res.stderr);
    }
}

test "cli undo B-undo-18: a positional or token after '--' is a usage error (exit 2)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // A bare positional, and a token after `--`, both reject with the try-help line.
    const cases = [_]struct { verb: []const u8, args: []const []const u8, tok: []const u8 }{
        .{ .verb = "undo", .args = &.{ "undo", "foo" }, .tok = "foo" },
        .{ .verb = "redo", .args = &.{ "redo", "--", "3" }, .tok = "3" },
    };
    for (cases) |case| {
        var res = try runZit(gpa, io, tmp.dir, case.args);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        var buf: [128]u8 = undefined;
        const expected = try std.fmt.bufPrint(&buf, "zit {s}: unexpected argument '{s}'\ntry 'zit {s} --help'\n", .{ case.verb, case.tok, case.verb });
        try std.testing.expectEqualStrings(expected, res.stderr);
    }
}

test "cli undo B-undo-19: outside a repository is exit 1, stdout empty" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    for ([_][]const u8{ "undo", "redo" }) |verb| {
        var res = try runZit(gpa, io, tmp.dir, &.{verb});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no zit checkout here") != null);
    }
}

test "cli undo B-undo-20: a redirected undo keeps stdout and stderr unmixed" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    const root = try commitFile(gpa, io, tmp.dir, "tracked.txt", "earlier\n", "earlier", &root_c);
    var child_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &child_c);
    _ = try commitFile(gpa, io, tmp.dir, "tracked.txt", "later\n", "later", &child_c);
    var upd = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &upd);
    try expectExit(upd.term, 0);

    // Success row to stdout only; the failed second undo's diagnostic to stderr
    // only — the two files never cross-contaminate.
    var sh = try runShell(gpa, io, tmp.dir, "\"$ZIT\" undo >out.txt 2>err.txt; \"$ZIT\" undo >out2.txt 2>err2.txt");
    defer freeResult(gpa, &sh);

    const out = try readFile(gpa, io, tmp.dir, "out.txt");
    defer gpa.free(out);
    const err = try readFile(gpa, io, tmp.dir, "err.txt");
    defer gpa.free(err);
    try std.testing.expect(std.mem.endsWith(u8, out, "undo: working tree restored\n"));
    try std.testing.expectEqualStrings("", err);

    const out2 = try readFile(gpa, io, tmp.dir, "out2.txt");
    defer gpa.free(out2);
    const err2 = try readFile(gpa, io, tmp.dir, "err2.txt");
    defer gpa.free(err2);
    try std.testing.expectEqualStrings("", out2);
    try std.testing.expectEqualStrings("zit undo: nothing to undo\n", err2);
}

test "cli undo B-undo-21: undo and redo are listed in the top-level help" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "\n  undo       undo the last update or revert\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "\n  redo       reapply an undone change\n") != null);
}

test "cli commit B-commit-5: a missing message is a usage error (exit 2, empty stdout)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runCommitEnv(gpa, io, tmp.dir, .{}, &.{"commit"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit commit: a check-in message is required\n" ++
            "  give one with -m <message>,\n" ++
            "  run from a terminal to compose it in $EDITOR,\n" ++
            "  or set an editor: zit settings editor <command>\n" ++
            "try 'zit commit --help'\n",
        res.stderr,
    );
}

test "cli commit B-commit-18b: an unknown option is a usage error (exit 2)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runCommitEnv(gpa, io, tmp.dir, .{}, &.{ "commit", "-m", "x", "-z" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit commit: unknown option '-z'\ntry 'zit commit --help'\n",
        res.stderr,
    );
}

test "cli commit B-commit-20 (DX): --amend/--force teach the immutable-history model, exit 2, stderr only" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    // The reflex flag is rejected right after parse, before any repo discovery,
    // so no repo is needed; the message must teach that history is immutable
    // (check-ins are never rewritten) rather than print a bare "unknown option"
    // that reads as a typo. Still a usage error: exit 2, stderr only, try-help kept.
    inline for (.{
        .{ &[_][]const u8{ "commit", "-m", "x", "--amend" }, "--amend" },
        .{ &[_][]const u8{ "commit", "-m", "x", "--force" }, "--force" },
        .{ &[_][]const u8{ "commit", "-m", "x", "-f" }, "-f" },
    }) |case| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try runZit(gpa, io, tmp.dir, case[0]);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings(
            "zit commit: '" ++ case[1] ++ "' is not supported. zit history is immutable — " ++
                "check-ins are never rewritten; record a new check-in that supersedes the old one.\n" ++
                "try 'zit commit --help'\n",
            res.stderr,
        );
    }
}

test "cli commit B-commit-14: outside a repository is an expected failure (exit 1)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{}); // no init
    defer tmp.cleanup();

    var res = try runCommitEnv(gpa, io, tmp.dir, .{}, &.{ "commit", "-m", "x" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit: no zit checkout here (looked for .zit-checkout in this directory and its parents)\n" ++
            "if this is a bare repository, name it with -R, or run 'zit open' to create a working tree\n",
        res.stderr,
    );
}

test "cli commit B-commit-4: an empty or whitespace-only message is rejected (exit 1), nothing written" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a.txt");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    inline for (.{ "", "   " }) |msg| {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{}, &.{ "commit", "-m", msg });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings(
            "zit commit: aborting — empty check-in message\n",
            res.stderr,
        );
    }
    // Unborn trunk untouched: nothing was committed.
    try expectCheckoutVersion(io, tmp.dir, "");
}

test "cli commit B-commit-9: a root with no tracked files has nothing to commit (exit 1)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runCommitEnv(gpa, io, tmp.dir, .{}, &.{ "commit", "-m", "x" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit commit: nothing to commit (no tracked files) — use 'zit add <file>'\n",
        res.stderr,
    );
    try expectCheckoutVersion(io, tmp.dir, "");
}

test "cli commit B-commit-2/13/16/17: the root check-in mints trunk, decodes back through the library" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "hello\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice" }, &.{ "commit", "-m", "first" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("a.txt added\n", res.stderr);
    // Structural success line: `check-in <64-hex> on trunk` (B-commit-2/16/19).
    const hash = try checkinHash(res.stdout);
    try std.testing.expect(std.mem.endsWith(u8, res.stdout, " on trunk\n"));

    // The checkout advanced to the new hash (B-commit-15).
    try expectCheckoutVersion(io, tmp.dir, hash);

    // Decode the manifest back through the library (B-commit-17).
    var mc = try parseCheckin(gpa, io, tmp.dir, hash);
    defer mc.deinit();
    const m = mc.m;
    try std.testing.expectEqual(artifact.ArtifactType.manifest, m.kind);
    try std.testing.expect(m.c != null and std.mem.eql(u8, m.c.?.comment, "first"));
    try std.testing.expect(m.u != null and std.mem.eql(u8, m.u.?.login, "alice"));
    // Root: no P card, an F card for a.txt, and the two trunk tags (B-commit-13).
    try std.testing.expectEqual(@as(usize, 0), m.p.len);
    const fa = findF(m, "a.txt") orelse return error.MissingFCard;
    try std.testing.expectEqual(@as(usize, 64), fa.hash.?.len); // SHA3-256 blob name
    try std.testing.expect(fa.mode == .regular);
    try std.testing.expectEqual(@as(usize, 2), m.t.len);
    try std.testing.expect(m.t[0].sign == .propagate and std.mem.eql(u8, m.t[0].name, "branch"));
    try std.testing.expect(m.t[0].value != null and std.mem.eql(u8, m.t[0].value.?, "trunk"));
    try std.testing.expect(m.t[1].sign == .propagate and std.mem.eql(u8, m.t[1].name, "sym-trunk"));
    try std.testing.expect(m.t[1].value == null);
}

test "cli commit B-commit-7: the D card is the pinned UTC writer timestamp; commits are reproducible" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var hashes: [2][]u8 = undefined;
    inline for (0..2) |i| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a.txt", "same bytes\n");
        try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "bob" }, &.{ "commit", "-m", "msg" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        const hash = try checkinHash(res.stdout);
        var mc = try parseCheckin(gpa, io, tmp.dir, hash);
        defer mc.deinit();
        const m = mc.m;
        try std.testing.expect(m.d != null);
        try std.testing.expectEqualStrings(commit_ts, m.d.?.timestamp);
        hashes[i] = try gpa.dupe(u8, hash);
    }
    defer gpa.free(hashes[0]);
    defer gpa.free(hashes[1]);
    // Identical inputs + pinned clock/author ⇒ identical manifest hash.
    try std.testing.expectEqualStrings(hashes[0], hashes[1]);
}

test "cli commit B-commit-6: author is --user, else $USER, else an expected failure" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // (a) --user overrides the environment author.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a.txt", "x\n");
        try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "env-user" }, &.{ "commit", "-m", "m", "--user", "flag-user" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        const hash = try checkinHash(res.stdout);
        var mc = try parseCheckin(gpa, io, tmp.dir, hash);
        defer mc.deinit();
        const m = mc.m;
        try std.testing.expectEqualStrings("flag-user", m.u.?.login);
    }
    // (b) no --user ⇒ the $USER environment default.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a.txt", "x\n");
        try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "env-user" }, &.{ "commit", "-m", "m" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        const hash = try checkinHash(res.stdout);
        var mc = try parseCheckin(gpa, io, tmp.dir, hash);
        defer mc.deinit();
        const m = mc.m;
        try std.testing.expectEqualStrings("env-user", m.u.?.login);
    }
    // (c) neither ⇒ author-missing, exit 1, nothing written.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a.txt", "x\n");
        try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = null }, &.{ "commit", "-m", "m" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings(
            "zit commit: cannot determine the check-in author; pass --user <login>\n",
            res.stderr,
        );
        try expectCheckoutVersion(io, tmp.dir, "");
    }
}

test "cli commit B-commit-3/9: a child check-in carries a P card; an unchanged re-commit is a no-op" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "v1\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var r1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "first" });
    defer freeResult(gpa, &r1);
    try expectExit(r1.term, 0);
    const root_hash = try gpa.dupe(u8, try checkinHash(r1.stdout));
    defer gpa.free(root_hash);

    // No change ⇒ B-commit-9 child no-op, checkout unchanged.
    var rnoop = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "again" });
    defer freeResult(gpa, &rnoop);
    try expectExit(rnoop.term, 1);
    try std.testing.expectEqualStrings("", rnoop.stdout);
    try std.testing.expectEqualStrings("zit commit: nothing to commit (no changes)\n", rnoop.stderr);
    try expectCheckoutVersion(io, tmp.dir, root_hash);

    // Edit ⇒ a real child: new hash, one P card == root, no T card, checkout advanced.
    try writeFile(io, tmp.dir, "a.txt", "v2\n");
    var r2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "second" });
    defer freeResult(gpa, &r2);
    try expectExit(r2.term, 0);
    const child_hash = try checkinHash(r2.stdout);
    try std.testing.expect(!std.mem.eql(u8, child_hash, root_hash));
    try expectCheckoutVersion(io, tmp.dir, child_hash);

    var mc = try parseCheckin(gpa, io, tmp.dir, child_hash);
    defer mc.deinit();
    const m = mc.m;
    try std.testing.expectEqual(@as(usize, 1), m.p.len);
    try std.testing.expect(m.p[0].parents != null and std.mem.eql(u8, m.p[0].parents.?, root_hash));
    try std.testing.expectEqual(@as(usize, 0), m.t.len); // child inherits trunk by propagation
}

test "cli commit B-commit-10: a managed file missing/non-regular is refused, nothing written" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a.txt");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    // Remove the working-tree file (but keep it managed): an all-files commit
    // must refuse, teaching `zit rm` for a genuine deletion.
    try tmp.dir.deleteFile(io, "a.txt");

    var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "m" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit commit: cannot commit 'a.txt': missing from the working tree (use 'zit rm' to stop tracking it)\n",
        res.stderr,
    );
    try expectCheckoutVersion(io, tmp.dir, "");
}

test "cli commit B-commit-11: a partial commit re-hashes named files and carries the rest forward" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "a1\n");
    try writeFile(io, tmp.dir, "b.txt", "b1\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });

    var r1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "first" });
    defer freeResult(gpa, &r1);
    try expectExit(r1.term, 0);
    const root_hash = try gpa.dupe(u8, try checkinHash(r1.stdout));
    defer gpa.free(root_hash);
    var root_mc = try parseCheckin(gpa, io, tmp.dir, root_hash);
    defer root_mc.deinit();
    const root_m = root_mc.m;
    const b_orig_hash = try gpa.dupe(u8, (findF(root_m, "b.txt") orelse return error.MissingFCard).hash.?);
    defer gpa.free(b_orig_hash);

    // Edit both, but commit only a.txt: b.txt must be carried at its parent hash.
    try writeFile(io, tmp.dir, "a.txt", "a2\n");
    try writeFile(io, tmp.dir, "b.txt", "b2-uncommitted\n");
    var r2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "partial", "a.txt" });
    defer freeResult(gpa, &r2);
    try expectExit(r2.term, 0);
    const child_hash = try checkinHash(r2.stdout);

    var mc = try parseCheckin(gpa, io, tmp.dir, child_hash);
    defer mc.deinit();
    const m = mc.m;
    const fa = findF(m, "a.txt") orelse return error.MissingFCard;
    const fb = findF(m, "b.txt") orelse return error.MissingFCard;
    // a.txt re-hashed (differs from its root card); b.txt carried unchanged.
    const fa_root = findF(root_m, "a.txt").?;
    try std.testing.expect(!std.mem.eql(u8, fa.hash.?, fa_root.hash.?));
    try std.testing.expectEqualStrings(b_orig_hash, fb.hash.?);
}

test "cli commit B-commit-11: a named unmanaged path is not tracked (exit 1)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    try writeFile(io, tmp.dir, "b.txt", "b\n"); // exists but never `zit add`ed

    var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "m", "b.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit commit: 'b.txt' is not tracked (use 'zit add' first)\n",
        res.stderr,
    );
    try expectCheckoutVersion(io, tmp.dir, "");
}

test "cli commit B-commit-12: after `zit rm`, an all-files commit records the deletion" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try writeFile(io, tmp.dir, "b.txt", "b\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    var r1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "first" });
    defer freeResult(gpa, &r1);
    try expectExit(r1.term, 0);

    // Forget b.txt, then commit: the new manifest omits b's F card (deletion).
    var rrm = try runZit(gpa, io, tmp.dir, &.{ "rm", "b.txt" });
    defer freeResult(gpa, &rrm);
    try expectExit(rrm.term, 0);

    var r2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "drop b", "--user", "e2e" });
    defer freeResult(gpa, &r2);
    try expectExit(r2.term, 0);
    const child_hash = try checkinHash(r2.stdout);
    var mc = try parseCheckin(gpa, io, tmp.dir, child_hash);
    defer mc.deinit();
    const m = mc.m;
    try std.testing.expect(findF(m, "a.txt") != null);
    try std.testing.expect(findF(m, "b.txt") == null); // deletion recorded
}

test "cli commit B-commit-19: -q suppresses the success line but still advances the checkout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-q", "-m", "quiet" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout); // no success line
    try std.testing.expectEqualStrings("", res.stderr);

    // The commit still happened: the checkout advanced off the unborn state.
    const version = try checkoutVersionAlloc(gpa, io, tmp.dir);
    defer gpa.free(version);
    try std.testing.expectEqual(@as(usize, 64), version.len);
}

test "cli commit: a stale parent refuses naming the tip; --allow-fork forks deliberately" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    const root = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(root);
    try writeFile(io, tmp.dir, "a.txt", "two\n");
    const tip = try commitHashDup(gpa, io, tmp.dir, "tip");
    defer gpa.free(tip);

    // Move BEHIND the tip and edit: this parent now has a same-branch child.
    {
        var up = try runZit(gpa, io, tmp.dir, &.{ "update", root });
        defer freeResult(gpa, &up);
        try expectExit(up.term, 0);
    }
    try writeFile(io, tmp.dir, "a.txt", "divergent\n");

    // Refused, naming the newer tip and both remedies; nothing recorded.
    {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "would-fork" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, tip) != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "--allow-fork") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "zit update") != null);
    }
    try expectCheckoutVersion(io, tmp.dir, root);

    // --allow-fork proceeds; the branch now has two open leaves.
    {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "forked", "--allow-fork" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    {
        var lv = try runZit(gpa, io, tmp.dir, &.{"leaves"});
        defer freeResult(gpa, &lv);
        try expectExit(lv.term, 0);
        var count: usize = 0;
        var lines = std.mem.splitScalar(u8, lv.stdout, '\n');
        while (lines.next()) |line| {
            if (line.len != 0) count += 1;
        }
        try std.testing.expectEqual(@as(usize, 2), count);
    }
}

test "cli autosync: pull informs the fork guard, push publishes, failure degrades, off silences" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A — the peer. autosync OFF here, which doubles as the config-leak
    // probe: the clone below must come up with the DEFAULT (on), because the
    // setting is local and never crosses the wire.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"a.txt"});
    const c1 = try commitHashDup(gpa, io, tmp_a.dir, "c1");
    defer gpa.free(c1);
    {
        var so = try runZit(gpa, io, tmp_a.dir, &.{ "settings", "autosync", "off" });
        defer freeResult(gpa, &so);
        try expectExit(so.term, 0);
        try std.testing.expectEqualStrings("autosync off\n", so.stdout);
    }

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    var server_alive = true;
    defer if (server_alive) server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    {
        var op = try runZit(gpa, io, b_dir, &.{"open"});
        defer freeResult(gpa, &op);
        try expectExit(op.term, 0);
    }
    // A's off did not cross the wire: B reads the default.
    {
        var sv = try runZit(gpa, io, b_dir, &.{"settings"});
        defer freeResult(gpa, &sv);
        try expectExit(sv.term, 0);
        // The bare listing reports the value in EFFECT for every setting, which
        // for an untouched repository is each setting's default -- including
        // pgp-command's, whose default is fossil's own command line.
        try std.testing.expectEqualStrings(
            "autosync on\nignore-glob \nkeep-glob \nmanifest \n" ++
                "clearsign off\npgp-command gpg --clearsign -o \nbefore-commit \n" ++
                "binary-glob \ncrlf-glob \ncrnl-glob \nencoding-glob \n" ++
                "clean-glob \nempty-dirs \ndotfiles \nmimetypes \nattachment-size-limit \neditor \nmain-branch trunk\n",
            sv.stdout,
        );
    }

    // A moves ahead while B is at c1.
    try writeFile(io, tmp_a.dir, "a.txt", "two\n");
    const c2 = try commitHashDup(gpa, io, tmp_a.dir, "c2");
    defer gpa.free(c2);

    // B commits without updating: the autosync pull fetches c2, so the fork
    // guard refuses NAMING it — divergence caught before it exists.
    try writeFile(io, b_dir, "a.txt", "b-edit\n");
    {
        var res = try runCommitEnv(gpa, io, b_dir, .{ .user = "b" }, &.{ "commit", "-m", "stale" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, c2) != null);
    }

    // Join the tip (keeping B's edit is not the point here — revert, update,
    // re-edit), then commit: success, and the push lands the check-in in A.
    {
        var rv = try runZit(gpa, io, b_dir, &.{"revert"});
        defer freeResult(gpa, &rv);
        try expectExit(rv.term, 0);
        var up = try runZit(gpa, io, b_dir, &.{ "update", "trunk" });
        defer freeResult(gpa, &up);
        try expectExit(up.term, 0);
    }
    try expectCheckoutVersion(io, b_dir, c2);
    try writeFile(io, b_dir, "a.txt", "three\n");
    {
        var res = try runCommitEnv(gpa, io, b_dir, .{ .user = "b" }, &.{ "commit", "-m", "b-third" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    {
        var tl = try runZit(gpa, io, tmp_a.dir, &.{ "timeline", "-n", "1" });
        defer freeResult(gpa, &tl);
        try expectExit(tl.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "b-third") != null);
    }

    // Dead peer: the commit still succeeds, warning once. (`server.kill` stops
    // the listener; the port stays free of a second binder.)
    server.kill(io);
    server_alive = false;
    try writeFile(io, b_dir, "a.txt", "four\n");
    {
        var res = try runCommitEnv(gpa, io, b_dir, .{ .user = "b" }, &.{ "commit", "-m", "offline" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "continuing locally") != null);
    }

    // autosync off: the same dead peer draws no warning at all — inert, not
    // degraded. --no-sync would do the same for one invocation.
    {
        var so = try runZit(gpa, io, b_dir, &.{ "settings", "autosync", "off" });
        defer freeResult(gpa, &so);
        try expectExit(so.term, 0);
    }
    try writeFile(io, b_dir, "a.txt", "five\n");
    {
        var res = try runCommitEnv(gpa, io, b_dir, .{ .user = "b" }, &.{ "commit", "-m", "quiet-offline" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("a.txt edited\n", res.stderr);
    }
}

// ==========================================================================
// `zit update CHECKIN` end-to-end. These tests exercise the public command
// against real check-ins and synthetic delta fixtures without reaching into
// the update implementation.
// ==========================================================================

test "cli update: a clean canonical target restores earlier content and checkout ID" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "tracked.txt", "earlier\n");
    try addManaged(gpa, io, tmp.dir, &.{"tracked.txt"});

    var root_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "earlier" });
    defer freeResult(gpa, &root_commit);
    try expectExit(root_commit.term, 0);
    const root = try checkinHash(root_commit.stdout);

    try writeFile(io, tmp.dir, "tracked.txt", "later\n");
    var child_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "later" });
    defer freeResult(gpa, &child_commit);
    try expectExit(child_commit.term, 0);

    var update = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 0);
    try expectNoDiagnostic(update.stderr); // the receipt lives on stderr

    const restored = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(restored);
    try std.testing.expectEqualStrings("earlier\n", restored);
    try expectCheckoutVersion(io, tmp.dir, root);
    try expectManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
}

test "cli update: a non-manifest target leaves the clean checkout unchanged" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "tracked.txt", "kept\n");
    try addManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "kept" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const current = try checkinHash(commit.stdout);

    const non_manifest = try appendArtifact(gpa, io, tmp.dir, "not a manifest\n");
    defer gpa.free(non_manifest);
    const before_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(before_checkout);
    const before_managed = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(before_managed);
    const before_content = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(before_content);

    var update = try runZit(gpa, io, tmp.dir, &.{ "update", non_manifest });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 1);

    const after_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(after_checkout);
    const after_managed = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(after_managed);
    const after_content = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(after_content);
    try std.testing.expectEqualSlices(u8, before_checkout, after_checkout);
    try std.testing.expectEqualSlices(u8, before_managed, after_managed);
    try std.testing.expectEqualSlices(u8, before_content, after_content);
    try expectCheckoutVersion(io, tmp.dir, current);
    try expectManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
}

// The shape that made `zit update` accuse an intact fossil clone of corruption:
// a store that also holds an anonymous wiki edit (cards D L W Z, no U). The
// artifact parses and is well-framed, but §5.2 requires a U card on a WIKI
// artifact, so classification declines with MissingRequiredCard; its bytes
// still hash to the name it is stored under, so the repository is undamaged and
// the artifact is opaque to state. `update` derives state over the whole log
// and so meets that artifact, exactly as `timeline`/`ls`/`info`/`branch` do.
//
// This pins the general form of the defect (Repository State erratum
// `repo-state-00-s4-unclassifiable-artifact`): no read verb may succeed on a
// store where `update` fails for a reason the reader tolerated. Pre-fix, update
// exited 1 while every read verb read the same store fine; post-fix, all agree.
test "cli update: an unclassifiable artifact in the store is tolerated, and update agrees with the read verbs" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Two real check-ins: `root` (earlier) is the update target, and a later
    // commit is the current tip, so `update root` is a genuine backward update.
    try writeFile(io, tmp.dir, "tracked.txt", "earlier\n");
    try addManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
    var root_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "earlier" });
    defer freeResult(gpa, &root_commit);
    try expectExit(root_commit.term, 0);
    const root = try checkinHash(root_commit.stdout);

    try writeFile(io, tmp.dir, "tracked.txt", "later\n");
    var child_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "later" });
    defer freeResult(gpa, &child_commit);
    try expectExit(child_commit.term, 0);

    // Inject the anonymous wiki artifact under its true hash (appendArtifact
    // names it by content), so it is unclassifiable yet undamaged.
    const wiki_body = "D 2008-11-24T02:18:26\nL RockShoxBugs\nW 12\nhello, world\n";
    var sum_buf: [32]u8 = undefined;
    const sum = artifact.selfChecksum(wiki_body, &sum_buf);
    const wiki_bytes = try std.fmt.allocPrint(gpa, "{s}Z {s}\n", .{ wiki_body, sum });
    defer gpa.free(wiki_bytes);
    const wiki_name = try appendArtifact(gpa, io, tmp.dir, wiki_bytes);
    defer gpa.free(wiki_name);

    // The fix: update tolerates the verified-but-unclassifiable artifact and
    // restores the target. Pre-fix this exited 1 (Corrupt).
    var update_res = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &update_res);
    try expectExit(update_res.term, 0);
    const restored = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(restored);
    try std.testing.expectEqualStrings("earlier\n", restored);
    try expectCheckoutVersion(io, tmp.dir, root);

    // Read/assembly agreement: every read verb reads the same store — the one
    // update just tolerated — and must also exit 0.
    const read_verbs = [_][]const []const u8{
        &.{"timeline"},
        &.{"branch"},
        &.{ "info", root },
        &.{ "ls", root },
    };
    for (read_verbs) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
}

test "cli update: a moved tip under uncommitted work joins without forking" {
    // The issue's own reproduction, end to end. Every exit used to be closed:
    // `commit` refused with "run 'zit update' to join it", `update` refused
    // because the tree was dirty, `merge` refused for the same reason, and
    // `stash pop` cannot replay onto a check-in other than the one it was taken
    // from. The only way to keep the work was `commit --allow-fork` — so the
    // guard that exists to keep forks deliberate was manufacturing them.
    //
    // The fork guard is NOT relaxed here; it still refuses a non-leaf parent.
    // What changed is that its advice is now followable.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "base\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});
    var first = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "base" });
    defer freeResult(gpa, &first);
    try expectExit(first.term, 0);
    const older = try checkinHash(first.stdout);

    try writeFile(io, tmp.dir, "f.txt", "base\ntheirs\n");
    var second = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "second" });
    defer freeResult(gpa, &second);
    try expectExit(second.term, 0);
    const tip = try checkinHash(second.stdout);

    // Go back to the older check-in and start uncommitted work there — the
    // state an operator lands in when a shared branch moves under them.
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", older });
    defer freeResult(gpa, &back);
    try expectExit(back.term, 0);
    try writeFile(io, tmp.dir, "f.txt", "mine\nbase\n");

    // `commit` still refuses, and still says to update. That is the guard doing
    // its job — the fork must stay deliberate.
    var blocked = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "blocked" });
    defer freeResult(gpa, &blocked);
    try expectExit(blocked.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, blocked.stderr, "run 'zit update' to join it") != null);

    // Following that advice now works: the edits are merged onto the tip rather
    // than the update being refused. Disjoint regions merge without markers.
    var joined = try runZit(gpa, io, tmp.dir, &.{ "update", tip });
    defer freeResult(gpa, &joined);
    try expectExit(joined.term, 0);
    try expectCheckoutVersion(io, tmp.dir, tip);
    const merged = try readFile(gpa, io, tmp.dir, "f.txt");
    defer gpa.free(merged);
    try std.testing.expectEqualStrings("mine\nbase\ntheirs\n", merged);

    // And the commit that follows is an ordinary child of the tip: one leaf,
    // no `--allow-fork`, no repair check-in.
    var done = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "joined" });
    defer freeResult(gpa, &done);
    try expectExit(done.term, 0);
    const head = try checkinHash(done.stdout);

    var leaves = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &leaves);
    try expectExit(leaves.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, leaves.stdout, cli.shortHash(head)) != null);
    // Exactly one leaf line: a fork would show two.
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, leaves.stdout, "\n"));
}

test "cli update: a dirty checkout moves and three-way merges, marking conflicts" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "tracked.txt", "earlier\n");
    try addManaged(gpa, io, tmp.dir, &.{"tracked.txt"});

    var root_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "earlier" });
    defer freeResult(gpa, &root_commit);
    try expectExit(root_commit.term, 0);
    const root = try checkinHash(root_commit.stdout);

    try writeFile(io, tmp.dir, "tracked.txt", "committed later\n");
    var child_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "later" });
    defer freeResult(gpa, &child_commit);
    try expectExit(child_commit.term, 0);

    try writeFile(io, tmp.dir, "tracked.txt", "uncommitted change\n");

    // All three sides differ, so the merge cannot decide the file and writes an
    // envelope. That is a conflict (exit 1), NOT a refusal: this used to refuse
    // outright, which left an operator whose branch tip had moved under
    // uncommitted work with no way to join it at all.
    var update = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, update.stderr, "conflict in tracked.txt") != null);

    // The checkout MOVED — that is the whole point. A dirty tree is no longer a
    // reason to stay behind the tip.
    try expectCheckoutVersion(io, tmp.dir, root);
    try expectManaged(gpa, io, tmp.dir, &.{"tracked.txt"});

    // Both sides survive in the working file: the operator's uncommitted text
    // and the check-in being joined. Neither is silently discarded.
    const after_content = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(after_content);
    try std.testing.expect(std.mem.indexOf(u8, after_content, "uncommitted change") != null);
    try std.testing.expect(std.mem.indexOf(u8, after_content, "earlier") != null);
    try std.testing.expect(std.mem.indexOf(u8, after_content, "<<<<<<< current") != null);
}

test "cli update: a delta target materializes content inherited from its baseline" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "inherited.txt", "current content\n");
    try addManaged(gpa, io, tmp.dir, &.{"inherited.txt"});
    var current_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "current" });
    defer freeResult(gpa, &current_commit);
    try expectExit(current_commit.term, 0);

    const inherited = "inherited baseline content\n";
    const blob = try appendArtifact(gpa, io, tmp.dir, inherited);
    defer gpa.free(blob);
    const files = [_]artifact.FCard{.{
        .filename = "inherited.txt",
        .hash = blob,
        .perm = null,
        .old_name = null,
        .mode = .regular,
    }};
    const baseline = try appendDiffManifest(gpa, io, tmp.dir, "baseline", null, &files);
    defer gpa.free(baseline);
    const delta = try appendDiffManifest(gpa, io, tmp.dir, "delta", baseline, &.{});
    defer gpa.free(delta);

    var update = try runZit(gpa, io, tmp.dir, &.{ "update", delta });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 0);
    try expectNoDiagnostic(update.stderr); // the receipt lives on stderr

    const materialized = try readFile(gpa, io, tmp.dir, "inherited.txt");
    defer gpa.free(materialized);
    try std.testing.expectEqualStrings(inherited, materialized);
    try expectCheckoutVersion(io, tmp.dir, delta);
    try expectManaged(gpa, io, tmp.dir, &.{"inherited.txt"});
}

test "cli update: non-ancestor control branch records set then cancel the target branch" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "tracked.txt", "content\n");
    try addManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "target" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const target = try checkinHash(commit.stdout);

    try writeFile(io, tmp.dir, "tracked.txt", "child content\n");
    var child_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "child" });
    defer freeResult(gpa, &child_commit);
    try expectExit(child_commit.term, 0);
    const child = try checkinHash(child_commit.stdout);

    const set_control_id = try appendBranchControl(
        gpa,
        io,
        tmp.dir,
        "2020-01-01T00:00:00",
        .propagate,
        target,
        "feature",
    );
    defer gpa.free(set_control_id);

    var set_update = try runZit(gpa, io, tmp.dir, &.{ "update", target });
    defer freeResult(gpa, &set_update);
    try expectExit(set_update.term, 0);
    try expectNoDiagnostic(set_update.stderr); // the receipt lives on stderr
    {
        var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined;
        const current = (try checkout.read(io, tmp.dir, &checkout_buf)).?;
        try std.testing.expectEqualStrings(target, current.version);
        try std.testing.expectEqualStrings("feature", current.branch);
    }

    var reset = try runZit(gpa, io, tmp.dir, &.{ "update", child });
    defer freeResult(gpa, &reset);
    try expectExit(reset.term, 0);
    try expectNoDiagnostic(reset.stderr); // the receipt lives on stderr
    try expectCheckoutVersion(io, tmp.dir, child);

    const cancel_control_id = try appendBranchControl(
        gpa,
        io,
        tmp.dir,
        "2020-01-02T00:00:00",
        .cancel,
        target,
        null,
    );
    defer gpa.free(cancel_control_id);

    var cancel_update = try runZit(gpa, io, tmp.dir, &.{ "update", target });
    defer freeResult(gpa, &cancel_update);
    try expectExit(cancel_update.term, 0);
    try expectNoDiagnostic(cancel_update.stderr); // the receipt lives on stderr
    {
        var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined;
        const current = (try checkout.read(io, tmp.dir, &checkout_buf)).?;
        try std.testing.expectEqualStrings(target, current.version);
        try std.testing.expectEqualStrings("trunk", current.branch);
    }
}

test "cli update: unsafe and root-control target paths leave the checkout and outside sentinel unchanged" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    inline for (.{ ".zit", ".zit-checkout", ".zit-cache", "../outside/sentinel" }) |target_name| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try tmp.dir.createDir(io, "repo", .default_dir);
        try tmp.dir.createDir(io, "outside", .default_dir);
        var repo = try tmp.dir.openDir(io, "repo", .{});
        defer repo.close(io);
        var outside = try tmp.dir.openDir(io, "outside", .{});
        defer outside.close(io);

        try initRepo(gpa, io, repo);
        try writeFile(io, repo, "tracked.txt", "current\n");
        try addManaged(gpa, io, repo, &.{"tracked.txt"});
        var commit = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "current" });
        defer freeResult(gpa, &commit);
        try expectExit(commit.term, 0);
        const current = try checkinHash(commit.stdout);
        try writeFile(io, outside, "sentinel", "outside must survive\n");

        const payload = try appendArtifact(gpa, io, repo, "target content\n");
        defer gpa.free(payload);
        const files = [_]artifact.FCard{.{
            .filename = target_name,
            .hash = payload,
            .perm = null,
            .old_name = null,
            .mode = .regular,
        }};
        const target = try appendDiffManifest(gpa, io, repo, "unsafe target", null, &files);
        defer gpa.free(target);

        const before_log = try readFile(gpa, io, repo, ".zit");
        defer gpa.free(before_log);
        const before_checkout = try readFile(gpa, io, repo, ".zit-checkout");
        defer gpa.free(before_checkout);
        const before_managed = try readManagedImage(gpa, io, repo);
        defer gpa.free(before_managed);
        const before_tracked = try readFile(gpa, io, repo, "tracked.txt");
        defer gpa.free(before_tracked);
        const before_sentinel = try readFile(gpa, io, outside, "sentinel");
        defer gpa.free(before_sentinel);

        var update = try runZit(gpa, io, repo, &.{ "update", target });
        defer freeResult(gpa, &update);
        try expectExit(update.term, 1);
        try std.testing.expectEqualStrings("", update.stdout);

        const after_log = try readFile(gpa, io, repo, ".zit");
        defer gpa.free(after_log);
        const after_checkout = try readFile(gpa, io, repo, ".zit-checkout");
        defer gpa.free(after_checkout);
        const after_managed = try readManagedImage(gpa, io, repo);
        defer gpa.free(after_managed);
        const after_tracked = try readFile(gpa, io, repo, "tracked.txt");
        defer gpa.free(after_tracked);
        const after_sentinel = try readFile(gpa, io, outside, "sentinel");
        defer gpa.free(after_sentinel);
        try std.testing.expectEqualSlices(u8, before_log, after_log);
        try std.testing.expectEqualSlices(u8, before_checkout, after_checkout);
        try std.testing.expectEqualSlices(u8, before_managed, after_managed);
        try std.testing.expectEqualSlices(u8, before_tracked, after_tracked);
        try std.testing.expectEqualSlices(u8, before_sentinel, after_sentinel);
        try expectCheckoutVersion(io, repo, current);
        try expectManaged(gpa, io, repo, &.{"tracked.txt"});
    }
}

test "cli update: a target F-card cannot replace a symlink with an outside target" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    try tmp.dir.createDir(io, "outside", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    var outside = try tmp.dir.openDir(io, "outside", .{});
    defer outside.close(io);

    try initRepo(gpa, io, repo);
    try writeFile(io, repo, "tracked.txt", "current\n");
    try addManaged(gpa, io, repo, &.{"tracked.txt"});
    var commit = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "current" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const current = try checkinHash(commit.stdout);
    try writeFile(io, outside, "sentinel", "outside must survive\n");
    try repo.symLink(io, "../outside/sentinel", "link", .{});

    const payload = try appendArtifact(gpa, io, repo, "target content\n");
    defer gpa.free(payload);
    const files = [_]artifact.FCard{.{
        .filename = "link",
        .hash = payload,
        .perm = null,
        .old_name = null,
        .mode = .regular,
    }};
    const target = try appendDiffManifest(gpa, io, repo, "symlink target", null, &files);
    defer gpa.free(target);

    const before_checkout = try readFile(gpa, io, repo, ".zit-checkout");
    defer gpa.free(before_checkout);
    const before_managed = try readManagedImage(gpa, io, repo);
    defer gpa.free(before_managed);
    const before_tracked = try readFile(gpa, io, repo, "tracked.txt");
    defer gpa.free(before_tracked);
    const before_sentinel = try readFile(gpa, io, outside, "sentinel");
    defer gpa.free(before_sentinel);

    var update = try runZit(gpa, io, repo, &.{ "update", target });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 1);
    try std.testing.expectEqualStrings("", update.stdout);

    const after_checkout = try readFile(gpa, io, repo, ".zit-checkout");
    defer gpa.free(after_checkout);
    const after_managed = try readManagedImage(gpa, io, repo);
    defer gpa.free(after_managed);
    const after_tracked = try readFile(gpa, io, repo, "tracked.txt");
    defer gpa.free(after_tracked);
    const after_link = try readFile(gpa, io, repo, "link");
    defer gpa.free(after_link);
    const after_sentinel = try readFile(gpa, io, outside, "sentinel");
    defer gpa.free(after_sentinel);
    try std.testing.expectEqualSlices(u8, before_checkout, after_checkout);
    try std.testing.expectEqualSlices(u8, before_managed, after_managed);
    try std.testing.expectEqualSlices(u8, before_tracked, after_tracked);
    try std.testing.expectEqualSlices(u8, before_sentinel, after_link);
    try std.testing.expectEqualSlices(u8, before_sentinel, after_sentinel);
    try expectCheckoutVersion(io, repo, current);
    try expectManaged(gpa, io, repo, &.{"tracked.txt"});
}

test "cli update: a symlink ancestor is rejected without following it outside the repository" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    try tmp.dir.createDir(io, "outside", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    var outside = try tmp.dir.openDir(io, "outside", .{});
    defer outside.close(io);

    try initRepo(gpa, io, repo);
    try writeFile(io, repo, "tracked.txt", "current\n");
    try addManaged(gpa, io, repo, &.{"tracked.txt"});
    var commit = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "current" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const current = try checkinHash(commit.stdout);
    try writeFile(io, outside, "sentinel", "outside must survive\n");
    try repo.symLink(io, "../outside", "link", .{});

    const payload = try appendArtifact(gpa, io, repo, "target content\n");
    defer gpa.free(payload);
    const files = [_]artifact.FCard{.{
        .filename = "link/sentinel",
        .hash = payload,
        .perm = null,
        .old_name = null,
        .mode = .regular,
    }};
    const target = try appendDiffManifest(gpa, io, repo, "symlink ancestor", null, &files);
    defer gpa.free(target);

    const before_checkout = try readFile(gpa, io, repo, ".zit-checkout");
    defer gpa.free(before_checkout);
    const before_managed = try readManagedImage(gpa, io, repo);
    defer gpa.free(before_managed);
    const before_tracked = try readFile(gpa, io, repo, "tracked.txt");
    defer gpa.free(before_tracked);
    const before_sentinel = try readFile(gpa, io, outside, "sentinel");
    defer gpa.free(before_sentinel);

    var update = try runZit(gpa, io, repo, &.{ "update", target });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 1);
    try std.testing.expectEqualStrings("", update.stdout);

    const after_checkout = try readFile(gpa, io, repo, ".zit-checkout");
    defer gpa.free(after_checkout);
    const after_managed = try readManagedImage(gpa, io, repo);
    defer gpa.free(after_managed);
    const after_tracked = try readFile(gpa, io, repo, "tracked.txt");
    defer gpa.free(after_tracked);
    const after_sentinel = try readFile(gpa, io, outside, "sentinel");
    defer gpa.free(after_sentinel);
    try std.testing.expectEqualSlices(u8, before_checkout, after_checkout);
    try std.testing.expectEqualSlices(u8, before_managed, after_managed);
    try std.testing.expectEqualSlices(u8, before_tracked, after_tracked);
    try std.testing.expectEqualSlices(u8, before_sentinel, after_sentinel);
    try expectCheckoutVersion(io, repo, current);
    try expectManaged(gpa, io, repo, &.{"tracked.txt"});
}

test "cli update: file-to-directory transition round-trips through undo and redo" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "node", "old file\n");
    try addManaged(gpa, io, tmp.dir, &.{"node"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "file" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const current = try checkinHash(commit.stdout);

    const child_content = "new child\n";
    const blob = try appendArtifact(gpa, io, tmp.dir, child_content);
    defer gpa.free(blob);
    const files = [_]artifact.FCard{.{
        .filename = "node/child.txt",
        .hash = blob,
        .perm = null,
        .old_name = null,
        .mode = .regular,
    }};
    const target = try appendDiffManifest(gpa, io, tmp.dir, "file to directory", null, &files);
    defer gpa.free(target);

    var update = try runZit(gpa, io, tmp.dir, &.{ "update", target });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 0);
    try expectNoDiagnostic(update.stderr); // the receipt lives on stderr
    const child = try readFile(gpa, io, tmp.dir, "node/child.txt");
    defer gpa.free(child);
    try std.testing.expectEqualStrings(child_content, child);
    try expectCheckoutVersion(io, tmp.dir, target);
    try expectManaged(gpa, io, tmp.dir, &.{"node/child.txt"});

    var undo_res = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &undo_res);
    try expectExit(undo_res.term, 0);
    try expectNoDiagnostic(undo_res.stderr); // the receipt lives on stderr
    const restored = try readFile(gpa, io, tmp.dir, "node");
    defer gpa.free(restored);
    try std.testing.expectEqualStrings("old file\n", restored);
    try std.testing.expect(!fileExists(io, tmp.dir, "node/child.txt"));
    try expectCheckoutVersion(io, tmp.dir, current);
    try expectManaged(gpa, io, tmp.dir, &.{"node"});

    var redo_res = try runZit(gpa, io, tmp.dir, &.{"redo"});
    defer freeResult(gpa, &redo_res);
    try expectExit(redo_res.term, 0);
    try expectNoDiagnostic(redo_res.stderr); // the receipt lives on stderr
    const redone = try readFile(gpa, io, tmp.dir, "node/child.txt");
    defer gpa.free(redone);
    try std.testing.expectEqualStrings(child_content, redone);
    try expectCheckoutVersion(io, tmp.dir, target);
    try expectManaged(gpa, io, tmp.dir, &.{"node/child.txt"});
}

test "cli update: a clean managed directory can become a file" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, "node", .default_dir);
    try writeFile(io, tmp.dir, "node/child.txt", "old child\n");
    try addManaged(gpa, io, tmp.dir, &.{"node/child.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "directory" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    const file_content = "new file\n";
    const blob = try appendArtifact(gpa, io, tmp.dir, file_content);
    defer gpa.free(blob);
    const files = [_]artifact.FCard{.{
        .filename = "node",
        .hash = blob,
        .perm = null,
        .old_name = null,
        .mode = .regular,
    }};
    const target = try appendDiffManifest(gpa, io, tmp.dir, "directory to file", null, &files);
    defer gpa.free(target);

    var update = try runZit(gpa, io, tmp.dir, &.{ "update", target });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 0);
    try expectNoDiagnostic(update.stderr); // the receipt lives on stderr
    const node = try readFile(gpa, io, tmp.dir, "node");
    defer gpa.free(node);
    try std.testing.expectEqualStrings(file_content, node);
    try std.testing.expect(!fileExists(io, tmp.dir, "node/child.txt"));
    try expectCheckoutVersion(io, tmp.dir, target);
    try expectManaged(gpa, io, tmp.dir, &.{"node"});
}

test "cli update: directory-to-file transition undo/redo treats descendants below restored file as absent" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, "a", .default_dir);
    try writeFile(io, tmp.dir, "a/b", "old child\n");
    try addManaged(gpa, io, tmp.dir, &.{"a/b"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "directory" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const current = try checkinHash(commit.stdout);

    const file_content = "new file\n";
    const blob = try appendArtifact(gpa, io, tmp.dir, file_content);
    defer gpa.free(blob);
    const files = [_]artifact.FCard{.{
        .filename = "a",
        .hash = blob,
        .perm = null,
        .old_name = null,
        .mode = .regular,
    }};
    const target = try appendDiffManifest(gpa, io, tmp.dir, "directory to file", null, &files);
    defer gpa.free(target);

    var update = try runZit(gpa, io, tmp.dir, &.{ "update", target });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 0);
    try expectNoDiagnostic(update.stderr); // the receipt lives on stderr
    try std.testing.expect(sectionExists(gpa, io, tmp.dir, .undo));

    var undo_res = try runZit(gpa, io, tmp.dir, &.{"undo"});
    defer freeResult(gpa, &undo_res);
    try expectExit(undo_res.term, 0);
    try std.testing.expect(std.mem.endsWith(u8, undo_res.stdout, "undo: working tree restored\n"));
    try expectNoDiagnostic(undo_res.stderr); // the receipt lives on stderr
    const restored = try readFile(gpa, io, tmp.dir, "a/b");
    defer gpa.free(restored);
    try std.testing.expectEqualStrings("old child\n", restored);
    try expectCheckoutVersion(io, tmp.dir, current);
    try expectManaged(gpa, io, tmp.dir, &.{"a/b"});

    var redo_res = try runZit(gpa, io, tmp.dir, &.{"redo"});
    defer freeResult(gpa, &redo_res);
    try expectExit(redo_res.term, 0);
    try std.testing.expect(std.mem.endsWith(u8, redo_res.stdout, "redo: working tree restored\n"));
    try expectNoDiagnostic(redo_res.stderr); // the receipt lives on stderr
    const redone = try readFile(gpa, io, tmp.dir, "a");
    defer gpa.free(redone);
    try std.testing.expectEqualStrings(file_content, redone);
    try std.testing.expect(!fileExists(io, tmp.dir, "a/b"));
    try expectCheckoutVersion(io, tmp.dir, target);
    try expectManaged(gpa, io, tmp.dir, &.{"a"});
    try std.testing.expect(sectionExists(gpa, io, tmp.dir, .undo));

    var second_redo = try runZit(gpa, io, tmp.dir, &.{"redo"});
    defer freeResult(gpa, &second_redo);
    try expectExit(second_redo.term, 1);
    try std.testing.expectEqualStrings("", second_redo.stdout);
    try std.testing.expectEqualStrings("zit redo: nothing to redo\n", second_redo.stderr);
    const final_content = try readFile(gpa, io, tmp.dir, "a");
    defer gpa.free(final_content);
    try std.testing.expectEqualSlices(u8, redone, final_content);
}

test "cli update: an unmanaged descendant blocks a directory-to-file transition without mutation" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, "node", .default_dir);
    try writeFile(io, tmp.dir, "node/tracked.txt", "managed child\n");
    try addManaged(gpa, io, tmp.dir, &.{"node/tracked.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "directory" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const current = try checkinHash(commit.stdout);
    try writeFile(io, tmp.dir, "node/unmanaged.txt", "must survive\n");

    const blob = try appendArtifact(gpa, io, tmp.dir, "would replace directory\n");
    defer gpa.free(blob);
    const files = [_]artifact.FCard{.{
        .filename = "node",
        .hash = blob,
        .perm = null,
        .old_name = null,
        .mode = .regular,
    }};
    const target = try appendDiffManifest(gpa, io, tmp.dir, "blocked directory to file", null, &files);
    defer gpa.free(target);

    const before_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(before_checkout);
    const before_managed = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(before_managed);
    const before_tracked = try readFile(gpa, io, tmp.dir, "node/tracked.txt");
    defer gpa.free(before_tracked);
    const before_unmanaged = try readFile(gpa, io, tmp.dir, "node/unmanaged.txt");
    defer gpa.free(before_unmanaged);

    var update = try runZit(gpa, io, tmp.dir, &.{ "update", target });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 1);
    try std.testing.expectEqualStrings("", update.stdout);

    const after_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(after_checkout);
    const after_managed = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(after_managed);
    const after_tracked = try readFile(gpa, io, tmp.dir, "node/tracked.txt");
    defer gpa.free(after_tracked);
    const after_unmanaged = try readFile(gpa, io, tmp.dir, "node/unmanaged.txt");
    defer gpa.free(after_unmanaged);
    try std.testing.expectEqualSlices(u8, before_checkout, after_checkout);
    try std.testing.expectEqualSlices(u8, before_managed, after_managed);
    try std.testing.expectEqualSlices(u8, before_tracked, after_tracked);
    try std.testing.expectEqualSlices(u8, before_unmanaged, after_unmanaged);
    try expectCheckoutVersion(io, tmp.dir, current);
    try expectManaged(gpa, io, tmp.dir, &.{"node/tracked.txt"});
}

test "cli update: an executable F-card materializes an executable regular file" {
    if (comptime !File.Permissions.has_executable_bit) return error.SkipZigTest;

    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "script", "old\n");
    try addManaged(gpa, io, tmp.dir, &.{"script"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "regular" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    const script_content = "#!/bin/sh\necho updated\n";
    const blob = try appendArtifact(gpa, io, tmp.dir, script_content);
    defer gpa.free(blob);
    const files = [_]artifact.FCard{.{
        .filename = "script",
        .hash = blob,
        .perm = "x",
        .old_name = null,
        .mode = .executable,
    }};
    const target = try appendDiffManifest(gpa, io, tmp.dir, "executable target", null, &files);
    defer gpa.free(target);

    var update = try runZit(gpa, io, tmp.dir, &.{ "update", target });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 0);
    try expectNoDiagnostic(update.stderr); // the receipt lives on stderr
    const script = try readFile(gpa, io, tmp.dir, "script");
    defer gpa.free(script);
    try std.testing.expectEqualStrings(script_content, script);
    var script_file = try tmp.dir.openFile(io, "script", .{ .mode = .read_only });
    defer script_file.close(io);
    try std.testing.expect((try script_file.stat(io)).permissions.toMode() & 0o111 != 0);
    try expectCheckoutVersion(io, tmp.dir, target);
    try expectManaged(gpa, io, tmp.dir, &.{"script"});
}

test "cli update: a locally added file rides across the move" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "tracked.txt", "earlier\n");
    try addManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
    var root_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "earlier" });
    defer freeResult(gpa, &root_commit);
    try expectExit(root_commit.term, 0);
    const root = try checkinHash(root_commit.stdout);
    try writeFile(io, tmp.dir, "tracked.txt", "later\n");
    var child_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "later" });
    defer freeResult(gpa, &child_commit);
    try expectExit(child_commit.term, 0);
    try writeFile(io, tmp.dir, "added.txt", "new membership\n");
    try addManaged(gpa, io, tmp.dir, &.{"added.txt"});

    // `added.txt` exists on neither check-in, so no side of the merge has an
    // opinion about it and it simply stays — staged, with its content intact.
    // Membership drift used to be a flat refusal.
    var update = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, update.stdout, "carried 1 local change") != null);

    try expectCheckoutVersion(io, tmp.dir, root);
    const after_added = try readFile(gpa, io, tmp.dir, "added.txt");
    defer gpa.free(after_added);
    try std.testing.expectEqualStrings("new membership\n", after_added);
    // The clean file took the target's content, exactly as a clean update.
    const after_tracked = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(after_tracked);
    try std.testing.expectEqualStrings("earlier\n", after_tracked);
    try expectManaged(gpa, io, tmp.dir, &.{ "added.txt", "tracked.txt" });
}

test "cli info: with no NAME a bare info describes the check-in the tree is on; revert names only the files it changes" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "a\n");
    try writeFile(io, tmp.dir, "b", "b\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a", "b" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c1" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    const c1_hash = try checkinHash(c1.stdout);
    var info = try runZit(gpa, io, tmp.dir, &.{"info"});
    defer freeResult(gpa, &info);
    try expectExitDiag(info.term, 0, info);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, cli.shortHash(c1_hash)) != null);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "check-ins:    1\n") != null);
    try writeFile(io, tmp.dir, "a", "edited\n");
    var rv = try runZit(gpa, io, tmp.dir, &.{"revert"});
    defer freeResult(gpa, &rv);
    try expectExitDiag(rv.term, 0, rv);
    try std.testing.expectEqualStrings("REVERT   a\n", rv.stdout); // b was already clean
}

test "cli update: with no CHECKIN a bare update moves to the current branch's newest check-in, as Fossil's does" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c1" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    const c1_hash = try checkinHash(c1.stdout);
    try writeFile(io, tmp.dir, "a", "two\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c2" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    const c2_hash = try checkinHash(c2.stdout);
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", c1_hash });
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);
    try expectFileContent(gpa, io, tmp.dir, "a", "one\n");
    var bare = try runZit(gpa, io, tmp.dir, &.{"update"});
    defer freeResult(gpa, &bare);
    try expectExitDiag(bare.term, 0, bare);
    try expectFileContent(gpa, io, tmp.dir, "a", "two\n");
    var st = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &st);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, cli.shortHash(c2_hash)) != null);
}

test "cli update: a missing managed file preserves the current checkout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "tracked.txt", "earlier\n");
    try addManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
    var root_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "earlier" });
    defer freeResult(gpa, &root_commit);
    try expectExit(root_commit.term, 0);
    const root = try checkinHash(root_commit.stdout);
    try writeFile(io, tmp.dir, "tracked.txt", "later\n");
    var child_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "later" });
    defer freeResult(gpa, &child_commit);
    try expectExit(child_commit.term, 0);
    const child = try checkinHash(child_commit.stdout);
    try tmp.dir.deleteFile(io, "tracked.txt");

    const before_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(before_checkout);
    const before_managed = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(before_managed);

    var update = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 1);
    try std.testing.expectEqualStrings("", update.stdout);

    const after_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(after_checkout);
    const after_managed = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(after_managed);
    try std.testing.expectEqualSlices(u8, before_checkout, after_checkout);
    try std.testing.expectEqualSlices(u8, before_managed, after_managed);
    try std.testing.expect(!fileExists(io, tmp.dir, "tracked.txt"));
    try expectCheckoutVersion(io, tmp.dir, child);
    try expectManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
}

test "cli update: executable-mode drift is carried onto the new content" {
    if (comptime !File.Permissions.has_executable_bit) return error.SkipZigTest;

    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "script", "earlier\n");
    try addManaged(gpa, io, tmp.dir, &.{"script"});
    var root_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "earlier" });
    defer freeResult(gpa, &root_commit);
    try expectExit(root_commit.term, 0);
    const root = try checkinHash(root_commit.stdout);
    try writeFile(io, tmp.dir, "script", "later\n");
    var child_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "later" });
    defer freeResult(gpa, &child_commit);
    try expectExit(child_commit.term, 0);
    {
        var script = try tmp.dir.openFile(io, "script", .{ .mode = .read_write });
        defer script.close(io);
        try script.setPermissions(io, File.Permissions.executable_file);
    }

    // The bytes are unchanged locally, so the target's content wins; the local
    // MODE is the local change, and B-merge-23 keeps the working file's side of
    // it. Mode drift used to refuse the update outright.
    var update = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 0);

    try expectCheckoutVersion(io, tmp.dir, root);
    const after_script = try readFile(gpa, io, tmp.dir, "script");
    defer gpa.free(after_script);
    try std.testing.expectEqualStrings("earlier\n", after_script);
    var script = try tmp.dir.openFile(io, "script", .{ .mode = .read_only });
    defer script.close(io);
    try std.testing.expect((try script.stat(io)).permissions.toMode() & 0o111 != 0);
    try expectManaged(gpa, io, tmp.dir, &.{"script"});
}

test "cli update: managed type drift preserves the current checkout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "tracked", "earlier\n");
    try addManaged(gpa, io, tmp.dir, &.{"tracked"});
    var root_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "earlier" });
    defer freeResult(gpa, &root_commit);
    try expectExit(root_commit.term, 0);
    const root = try checkinHash(root_commit.stdout);
    try writeFile(io, tmp.dir, "tracked", "later\n");
    var child_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "later" });
    defer freeResult(gpa, &child_commit);
    try expectExit(child_commit.term, 0);
    const child = try checkinHash(child_commit.stdout);
    try tmp.dir.deleteFile(io, "tracked");
    try tmp.dir.createDir(io, "tracked", .default_dir);
    try writeFile(io, tmp.dir, "tracked/nested.txt", "type drift\n");

    const before_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(before_checkout);
    const before_managed = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(before_managed);
    const before_nested = try readFile(gpa, io, tmp.dir, "tracked/nested.txt");
    defer gpa.free(before_nested);

    var update = try runZit(gpa, io, tmp.dir, &.{ "update", root });
    defer freeResult(gpa, &update);
    try expectExit(update.term, 1);
    try std.testing.expectEqualStrings("", update.stdout);

    const after_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(after_checkout);
    const after_managed = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(after_managed);
    const after_nested = try readFile(gpa, io, tmp.dir, "tracked/nested.txt");
    defer gpa.free(after_nested);
    try std.testing.expectEqualSlices(u8, before_checkout, after_checkout);
    try std.testing.expectEqualSlices(u8, before_managed, after_managed);
    try std.testing.expectEqualSlices(u8, before_nested, after_nested);
    try expectCheckoutVersion(io, tmp.dir, child);
    try expectManaged(gpa, io, tmp.dir, &.{"tracked"});
}

test "cli update: unresolvable check-in names do not mutate a repository" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    const Case = struct {
        id: []const u8,
        diagnostic: []const u8,
    };
    const cases = [_]Case{
        // CHANGED EXPECTATION (was "invalid check-in ID"): SF1 made `update`'s
        // argument a check-in NAME resolved through the shared §8.3 procedure,
        // so a non-hex string is no longer malformed by construction — it is a
        // symbolic name that resolves to nothing. The property this test exists
        // for is unchanged and still asserted below: exit 1, a `zit update: `
        // diagnostic, and a byte-identical repository.
        .{ .id = "not-an-id", .diagnostic = "not found" },
        .{
            .id = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
            .diagnostic = "not found",
        },
    };

    inline for (cases) |case| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "tracked.txt", "current\n");
        try addManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
        var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "current" });
        defer freeResult(gpa, &commit);
        try expectExit(commit.term, 0);
        const current = try checkinHash(commit.stdout);

        const before_log = try readFile(gpa, io, tmp.dir, ".zit");
        defer gpa.free(before_log);
        const before_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
        defer gpa.free(before_checkout);
        const before_managed = try readManagedImage(gpa, io, tmp.dir);
        defer gpa.free(before_managed);
        const before_tracked = try readFile(gpa, io, tmp.dir, "tracked.txt");
        defer gpa.free(before_tracked);

        var update = try runZit(gpa, io, tmp.dir, &.{ "update", case.id });
        defer freeResult(gpa, &update);
        try expectExit(update.term, 1);
        try std.testing.expectEqualStrings("", update.stdout);
        try std.testing.expect(std.mem.startsWith(u8, update.stderr, "zit update: "));
        try std.testing.expect(std.mem.indexOf(u8, update.stderr, case.diagnostic) != null);
        try std.testing.expect(std.mem.endsWith(u8, update.stderr, "\n"));

        const after_log = try readFile(gpa, io, tmp.dir, ".zit");
        defer gpa.free(after_log);
        const after_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
        defer gpa.free(after_checkout);
        const after_managed = try readManagedImage(gpa, io, tmp.dir);
        defer gpa.free(after_managed);
        const after_tracked = try readFile(gpa, io, tmp.dir, "tracked.txt");
        defer gpa.free(after_tracked);
        try std.testing.expectEqualSlices(u8, before_log, after_log);
        try std.testing.expectEqualSlices(u8, before_checkout, after_checkout);
        try std.testing.expectEqualSlices(u8, before_managed, after_managed);
        try std.testing.expectEqualSlices(u8, before_tracked, after_tracked);
        try expectCheckoutVersion(io, tmp.dir, current);
        try expectManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
    }
}

test "cli update: direct and routed help agree, and missing, extra, and invalid arguments are usage errors" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var direct = try runZit(gpa, io, tmp.dir, &.{ "update", "--help" });
        defer freeResult(gpa, &direct);
        try expectExit(direct.term, 0);
        try std.testing.expect(std.mem.startsWith(u8, direct.stdout, "usage: zit update "));
        try std.testing.expectEqualStrings("", direct.stderr);

        var routed = try runZit(gpa, io, tmp.dir, &.{ "help", "update" });
        defer freeResult(gpa, &routed);
        try expectExit(routed.term, 0);
        try std.testing.expectEqualSlices(u8, direct.stdout, routed.stdout);
        try std.testing.expectEqualStrings("", routed.stderr);
    }

    const Case = struct {
        argv: []const []const u8,
        stderr: []const u8,
    };
    const cases = [_]Case{
        .{
            .argv = &.{ "update", "a", "extra" },
            .stderr = "zit update: unexpected argument 'extra'\ntry 'zit update --help'\n",
        },
        .{
            .argv = &.{ "update", "-x" },
            .stderr = "zit update: unknown option '-x'\ntry 'zit update --help'\n",
        },
    };
    inline for (cases) |case| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try runZit(gpa, io, tmp.dir, case.argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings(case.stderr, res.stderr);
    }
}

// ==========================================================================
// `zit log CHECKIN` end-to-end. These tests spawn the real binary to observe
// log output. Synthetic fixtures write artifacts directly when a graph or
// corruption state cannot be constructed through the public CLI.
// ==========================================================================

test "cli log: a root check-in emits exactly its full ID" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const root = try checkinHash(commit.stdout);

    var res = try runZit(gpa, io, tmp.dir, &.{ "log", root });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var want: [80]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "checkin {s}\n", .{root});
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli log: a child emits itself then its parent" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    const root = try checkinHash(c1.stdout);

    try writeFile(io, tmp.dir, "a.txt", "two\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "child" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    const child = try checkinHash(c2.stdout);

    var res = try runZit(gpa, io, tmp.dir, &.{ "log", child });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var want: [160]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "checkin {s}\ncheckin {s}\n", .{ child, root });
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}
test "cli log: an uppercase P-card parent ID remains a reachable invalid target" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const parent = try appendSyntheticCheckin(gpa, io, tmp.dir, "parent", null);
    defer gpa.free(parent);
    var uppercase_parent: [64]u8 = undefined;
    for (parent, &uppercase_parent) |byte, *destination| {
        destination.* = std.ascii.toUpper(byte);
    }
    try std.testing.expect(std.mem.indexOfAny(u8, uppercase_parent[0..], "ABCDEF") != null);
    const child = try appendRawCheckinWithUppercaseParent(gpa, io, tmp.dir, uppercase_parent[0..]);
    defer gpa.free(child);

    var stored_child = try parseCheckin(gpa, io, tmp.dir, child);
    defer stored_child.deinit();
    var expected_parent_card_buf: [67]u8 = undefined;
    const expected_parent_card = try std.fmt.bufPrint(
        &expected_parent_card_buf,
        "P {s}\n",
        .{uppercase_parent},
    );
    try std.testing.expect(std.mem.indexOf(u8, stored_child.bytes, expected_parent_card) != null);
    try std.testing.expectEqual(@as(usize, 1), stored_child.m.p.len);
    try std.testing.expectEqualStrings(uppercase_parent[0..], stored_child.m.p[0].parents.?);

    // The default walk reads the derived adjacency rather than re-parsing every
    // reachable manifest, so an uppercase P-card target is no longer diagnosed
    // HERE: the derive never crosslinked a check-in under that name, so the
    // edge leads to a rid with no event row and the walk names it a phantom,
    // rendered with the P card's own bytes -- the identity space keeps the
    // token as written, which is why the uppercase form comes back out.
    // Fossil draws the line the same way -- `plink` is written once at
    // crosslink and every later traversal trusts it -- and the damage is still
    // reported, by the verb whose job that is. See the `verify` assertion below.
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", child });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var want: [160]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "checkin {s}\nphantom {s}\n", .{ child, uppercase_parent[0..] });
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

fn appendArtifactAs(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    algo: artifact.HashAlgo,
    bytes: []const u8,
) ![]u8 {
    var name_buf: [64]u8 = undefined;
    const name = artifact.artifactName(algo, bytes, &name_buf);
    var digest: [32]u8 = undefined;
    const decoded = digest[0 .. name.len / 2];
    _ = try std.fmt.hexToBytes(decoded, name);

    var file = try dir.openFile(io, ".zit", .{ .mode = .read_write });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.open(gpa, fs.storageFile(), 0);
    defer log.deinit(gpa);
    _ = try log.put(decoded, bytes);
    try log.commit();
    return gpa.dupe(u8, name);
}

fn appendArtifact(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, bytes: []const u8) ![]u8 {
    return appendArtifactAs(gpa, io, dir, .sha3_256, bytes);
}

fn appendRawCheckinWithUppercaseParent(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    uppercase_parent: []const u8,
) ![]u8 {
    std.debug.assert(uppercase_parent.len == 64);

    var body_buf: [256]u8 = undefined;
    const body = try std.fmt.bufPrint(
        &body_buf,
        "C child\n" ++
            "D 2020-01-01T00:00:00\n" ++
            "F f 0000000000000000000000000000000000000000000000000000000000000000\n" ++
            "P {s}\n" ++
            "U u\n",
        .{uppercase_parent},
    );
    var raw_buf: [256]u8 = undefined;
    @memcpy(raw_buf[0..body.len], body);
    var checksum: [32]u8 = undefined;
    const digest = artifact.selfChecksum(body, &checksum);
    raw_buf[body.len] = 'Z';
    raw_buf[body.len + 1] = ' ';
    @memcpy(raw_buf[body.len + 2 ..][0..digest.len], digest);
    raw_buf[body.len + 34] = '\n';

    return appendArtifact(gpa, io, dir, raw_buf[0 .. body.len + 35]);
}

fn appendSyntheticCheckinAs(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    algo: artifact.HashAlgo,
    label: []const u8,
    parents: ?[]const u8,
) ![]u8 {
    const f = [_]artifact.FCard{.{
        .filename = "f",
        .hash = "0000000000000000000000000000000000000000000000000000000000000000",
        .perm = null,
        .old_name = null,
        .mode = .regular,
    }};
    const p = [_]artifact.PCard{.{
        .parents = parents,
        .count = if (parents) |wire| std.mem.countScalar(u8, wire, ' ') + 1 else 0,
    }};
    var manifest: artifact.Manifest = .{
        .arena_state = std.heap.ArenaAllocator.init(gpa),
        .kind = .manifest,
        .a = null,
        .b = null,
        .c = .{ .comment = label },
        .d = .{ .timestamp = "2020-01-01T00:00:00" },
        .e = null,
        .g = null,
        .h = null,
        .i = null,
        .k = null,
        .l = null,
        .n = null,
        .r = null,
        .u = .{ .login = "u" },
        .z = null,
        .w = null,
        .f = &f,
        .j = &.{},
        .m = &.{},
        .p = if (parents == null) &.{} else &p,
        .q = &.{},
        .t = &.{},
    };
    defer manifest.deinit();
    const bytes = try artifact.create(&manifest, gpa);
    defer gpa.free(bytes);
    return appendArtifactAs(gpa, io, dir, algo, bytes);
}

fn appendSyntheticCheckin(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    label: []const u8,
    parents: ?[]const u8,
) ![]u8 {
    return appendSyntheticCheckinAs(gpa, io, dir, .sha3_256, label, parents);
}

test "cli log: merge parents are traversed depth-first in P-card order" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const a = try appendSyntheticCheckin(gpa, io, tmp.dir, "a", null);
    defer gpa.free(a);
    const b = try appendSyntheticCheckin(gpa, io, tmp.dir, "b", null);
    defer gpa.free(b);
    const c = try appendSyntheticCheckin(gpa, io, tmp.dir, "c", a);
    defer gpa.free(c);
    var parents_buf: [129]u8 = undefined;
    const parents = try std.fmt.bufPrint(&parents_buf, "{s} {s}", .{ c, b });
    const merge = try appendSyntheticCheckin(gpa, io, tmp.dir, "merge", parents);
    defer gpa.free(merge);

    var res = try runZit(gpa, io, tmp.dir, &.{ "log", merge });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var want: [320]u8 = undefined;
    const expected = try std.fmt.bufPrint(
        &want,
        "checkin {s}\ncheckin {s}\ncheckin {s}\ncheckin {s}\n",
        .{ merge, c, a, b },
    );
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli log: first visit wins across a diamond" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const base = try appendSyntheticCheckin(gpa, io, tmp.dir, "base", null);
    defer gpa.free(base);
    const left = try appendSyntheticCheckin(gpa, io, tmp.dir, "left", base);
    defer gpa.free(left);
    const right = try appendSyntheticCheckin(gpa, io, tmp.dir, "right", base);
    defer gpa.free(right);
    var parents_buf: [129]u8 = undefined;
    const parents = try std.fmt.bufPrint(&parents_buf, "{s} {s}", .{ left, right });
    const top = try appendSyntheticCheckin(gpa, io, tmp.dir, "top", parents);
    defer gpa.free(top);

    var res = try runZit(gpa, io, tmp.dir, &.{ "log", top });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var want: [320]u8 = undefined;
    const expected = try std.fmt.bufPrint(
        &want,
        "checkin {s}\ncheckin {s}\ncheckin {s}\ncheckin {s}\n",
        .{ top, left, base, right },
    );
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli log: a valid missing reachable parent is printed once as a phantom" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const missing = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
    var parents_buf: [129]u8 = undefined;
    const parents = try std.fmt.bufPrint(&parents_buf, "{s} {s}", .{ missing, missing });
    const top = try appendSyntheticCheckin(gpa, io, tmp.dir, "phantom-parent", parents);
    defer gpa.free(top);

    var res = try runZit(gpa, io, tmp.dir, &.{ "log", top });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var want: [160]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "checkin {s}\nphantom {s}\n", .{ top, missing });
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli log: a missing root check-in fails without output" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const missing = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", missing });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    var want: [160]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "zit log: check-in '{s}' not found\n", .{missing});
    try std.testing.expectEqualStrings(expected, res.stderr);
}

test "cli log: a malformed stored root is rejected as an invalid manifest" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const bad = try appendArtifact(gpa, io, tmp.dir, "not a manifest");
    defer gpa.free(bad);
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", bad });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    var want: [192]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "zit log: check-in '{s}' is not a valid manifest\n", .{bad});
    try std.testing.expectEqualStrings(expected, res.stderr);
}

test "cli log: a malformed reachable target fails after its valid ancestor" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const bad = try appendArtifact(gpa, io, tmp.dir, "not a manifest");
    defer gpa.free(bad);
    const top = try appendSyntheticCheckin(gpa, io, tmp.dir, "bad-parent", bad);
    defer gpa.free(top);
    // The default walk trusts the derived adjacency instead of re-parsing every
    // reachable manifest, so a target the derive never crosslinked is a phantom
    // here rather than a diagnosis. Fossil splits it the same way: `plink` is
    // written once at crosslink and every later traversal reads it, with
    // `test-integrity` as the verb that re-checks the repository. `zit verify`
    // is that verb here, and the assertion below holds it to the damage.
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", top });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var want: [160]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "checkin {s}\nphantom {s}\n", .{ top, bad });
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

fn appendSyntheticCluster(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]u8 {
    const members = [_]artifact.MCard{.{
        .hash = "1111111111111111111111111111111111111111111111111111111111111111",
    }};
    var manifest: artifact.Manifest = .{
        .arena_state = std.heap.ArenaAllocator.init(gpa),
        .kind = .cluster,
        .a = null,
        .b = null,
        .c = null,
        .d = null,
        .e = null,
        .g = null,
        .h = null,
        .i = null,
        .k = null,
        .l = null,
        .n = null,
        .r = null,
        .u = null,
        .z = null,
        .w = null,
        .f = &.{},
        .j = &.{},
        .m = &members,
        .p = &.{},
        .q = &.{},
        .t = &.{},
    };
    defer manifest.deinit();
    const bytes = try artifact.create(&manifest, gpa);
    defer gpa.free(bytes);
    return appendArtifact(gpa, io, dir, bytes);
}

test "cli log: a reachable non-check-in artifact is rejected" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const cluster = try appendSyntheticCluster(gpa, io, tmp.dir);
    defer gpa.free(cluster);
    const top = try appendSyntheticCheckin(gpa, io, tmp.dir, "cluster-parent", cluster);
    defer gpa.free(top);
    // The default walk trusts the derived adjacency instead of re-parsing every
    // reachable manifest, so a target the derive never crosslinked is a phantom
    // here rather than a diagnosis. Fossil splits it the same way: `plink` is
    // written once at crosslink and every later traversal reads it, with
    // `test-integrity` as the verb that re-checks the repository. `zit verify`
    // is that verb here, and the assertion below holds it to the damage.
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", top });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var want: [160]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "checkin {s}\nphantom {s}\n", .{ top, cluster });
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli log: a torn tail is rejected without truncating the repository" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const root = try appendSyntheticCheckin(gpa, io, tmp.dir, "root", null);
    defer gpa.free(root);
    var file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_write });
    const before = (try file.stat(io)).size;
    try file.writePositionalAll(io, "\x00", before);
    const torn_size = (try file.stat(io)).size;
    file.close(io);

    var res = try runZit(gpa, io, tmp.dir, &.{ "log", root });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit log: cannot read repository: repository is corrupt\n",
        res.stderr,
    );
    var after_file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_only });
    defer after_file.close(io);
    try std.testing.expectEqual(torn_size, (try after_file.stat(io)).size);
}

test "cli log: a stored SHA-1 parent is traversed by its 40-hex P-card ID" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const parent = try appendSyntheticCheckinAs(gpa, io, tmp.dir, .sha1, "sha1-parent", null);
    defer gpa.free(parent);
    try std.testing.expectEqual(@as(usize, 40), parent.len);
    const child = try appendSyntheticCheckinAs(gpa, io, tmp.dir, .sha3_256, "sha3-child", parent);
    defer gpa.free(child);

    var res = try runZit(gpa, io, tmp.dir, &.{ "log", child });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var want: [128]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "checkin {s}\ncheckin {s}\n", .{ child, parent });
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli log: a stored SHA-1 root is addressed by its 40-hex full ID" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const root = try appendSyntheticCheckinAs(gpa, io, tmp.dir, .sha1, "sha1-root", null);
    defer gpa.free(root);
    try std.testing.expectEqual(@as(usize, 40), root.len);
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", root });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var want: [64]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "checkin {s}\n", .{root});
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli log: content stored under the wrong full ID fails integrity" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const real = try appendSyntheticCheckin(gpa, io, tmp.dir, "real", null);
    defer gpa.free(real);
    var checkin = try parseCheckin(gpa, io, tmp.dir, real);
    defer checkin.deinit();
    const wrong = "2222222222222222222222222222222222222222222222222222222222222222";
    var wrong_digest: [32]u8 = undefined;
    _ = try std.fmt.hexToBytes(&wrong_digest, wrong);
    var file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_write });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.open(gpa, fs.storageFile(), 0);
    defer log.deinit(gpa);
    _ = try log.put(&wrong_digest, checkin.bytes);
    try log.commit();

    // WHERE THIS INTEGRITY CHECK LIVES NOW. The default walk reads the derived
    // adjacency and never re-hashes a reachable artifact, so `log` no longer
    // notices that these bytes are stored under a name they do not hash to.
    // That is fossil's split -- a manifest is validated once at crosslink, and
    // `test-integrity` is what re-checks a repository afterwards -- and paying
    // a full-history re-verification on every history listing was the anomaly.
    //
    // The guarantee MOVED; it was not dropped. `zit verify` reconstructs every
    // stored artifact through its delta chain and re-hashes it against its own
    // name, which is exactly this damage, and the assertion below is what keeps
    // that true: if verify ever stopped catching it, this test fails and the
    // coverage lost from `log` would not silently go missing everywhere.
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", wrong });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    var ver = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &ver);
    try expectNonzeroExit(ver.term);
    try std.testing.expect(std.mem.indexOf(u8, ver.stdout, wrong) != null or
        std.mem.indexOf(u8, ver.stderr, wrong) != null);
}

test "cli log: a corrupt complete frame is rejected without truncation" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var initial = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_only });
    const frame_offset = (try initial.stat(io)).size;
    initial.close(io);
    const root = try appendSyntheticCheckin(gpa, io, tmp.dir, "root", null);
    defer gpa.free(root);

    var file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_write });
    const stable_size = (try file.stat(io)).size;
    var byte: [1]u8 = undefined;
    try std.testing.expectEqual(@as(usize, 1), try file.readPositionalAll(io, &byte, frame_offset + 10));
    byte[0] ^= 0xff;
    try file.writePositionalAll(io, &byte, frame_offset + 10);
    file.close(io);

    var res = try runZit(gpa, io, tmp.dir, &.{ "log", root });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit log: cannot read repository: repository is corrupt\n",
        res.stderr,
    );
    var after = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_only });
    defer after.close(io);
    try std.testing.expectEqual(stable_size, (try after.stat(io)).size);
}

test "cli log: help documents only the signed one-verb surface" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const expected =
        \\usage: zit log [--long] [-n N] [-R <path>] CHECKIN [PATH]
        \\
        \\Show CHECKIN followed by every reachable parent check-in in depth-first
        \\P-card order. CHECKIN accepts every form 'zit help names' lists —
        \\IDs, prefixes, branches, tags, reserved forms, and timestamps. Each
        \\check-in is shown once; a referenced parent missing from this
        \\repository is shown once as a phantom.
        \\
        \\With PATH, show only the reachable check-ins that change PATH — where
        \\PATH's effective content differs from its primary parent's, including
        \\where PATH is added or deleted. PATH is an exact repo-relative path, not
        \\a pattern; a path that never changed yields no output.
        \\
        \\--long adds each check-in's own facts after its id:
        \\
        \\  checkin <id> <timestamp> <user> <comment>
        \\
        \\The comment is last because it is the only field that may contain
        \\spaces; a check-in with no comment prints an empty trailing field, so
        \\the column count never varies. Absent timestamp or user read '-'.
        \\
        \\This record grammar is a contract: consumers parse it positionally, and
        \\no display option, present or future, will change it. Presentation
        \\extras (the timeline's --weekday, for one) belong to the verbs whose
        \\output is for eyes, not parsers.
        \\
        \\options:
        \\      --long    add timestamp, user and comment to each entry
        \\  -n, --limit N show only the N most recent entries; 0 means all
        \\  -R, --repo <path>
        \\                read this repository instead of the one found from the
        \\                current directory; <path> is the directory holding '.zit'
        \\                (or the '.zit' file itself). No working tree is required.
        \\  -h, --help    show this help and exit
        \\
        \\exit status:
        \\  0  success
        \\  1  operational failure (not a repository, unreadable state, a refused
        \\     action) — the diagnostic on stderr names it
        \\  2  usage error (an unknown option or unexpected argument)
        \\
    ;

    var direct = try runZit(gpa, io, tmp.dir, &.{ "log", "--help", "ignored" });
    defer freeResult(gpa, &direct);
    try expectExit(direct.term, 0);
    try std.testing.expectEqualStrings(expected, direct.stdout);
    try std.testing.expectEqualStrings("", direct.stderr);

    var topic = try runZit(gpa, io, tmp.dir, &.{ "help", "log" });
    defer freeResult(gpa, &topic);
    try expectExit(topic.term, 0);
    try std.testing.expectEqualStrings(expected, topic.stdout);
    try std.testing.expectEqualStrings("", topic.stderr);
}

test "cli log: usage rejects missing, extra, flags, and non-canonical IDs" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var missing = try runZit(gpa, io, tmp.dir, &.{"log"});
    defer freeResult(gpa, &missing);
    try expectExit(missing.term, 2);
    try std.testing.expectEqualStrings("", missing.stdout);
    try std.testing.expectEqualStrings(
        "zit log: no check-in given\ntry 'zit log --help'\n",
        missing.stderr,
    );

    var flag = try runZit(gpa, io, tmp.dir, &.{ "log", "--all" });
    defer freeResult(gpa, &flag);
    try expectExit(flag.term, 2);
    try std.testing.expectEqualStrings("", flag.stdout);
    try std.testing.expectEqualStrings(
        "zit log: unknown option '--all'\ntry 'zit log --help'\n",
        flag.stderr,
    );

    // `--` ends the options, so the token after it is the check-in name and
    // the failure is resolution, not usage. Before this it was rejected as an
    // unknown option, which left a name beginning with a dash unreachable.
    var terminator = try runZit(gpa, io, tmp.dir, &.{ "log", "--", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" });
    defer freeResult(gpa, &terminator);
    // Not a usage error any more: the marker is consumed and the token after
    // it is the check-in name, so this fails later (no checkout here) rather
    // than at the parser. Asserted as "not exit 2, never `unknown option`"
    // instead of pinning whichever operational message comes first.
    try expectExit(terminator.term, 1);
    try std.testing.expectEqualStrings("", terminator.stdout);
    try std.testing.expect(std.mem.indexOf(u8, terminator.stderr, "unknown option") == null);

    // The second positional now binds PATH; a THIRD positional is the extra
    // argument (B-log-8).
    var extra = try runZit(gpa, io, tmp.dir, &.{ "log", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "path", "third" });
    defer freeResult(gpa, &extra);
    try expectExit(extra.term, 2);
    try std.testing.expectEqualStrings("", extra.stdout);
    try std.testing.expectEqualStrings(
        "zit log: unexpected argument 'third'\ntry 'zit log --help'\n",
        extra.stderr,
    );

    // A non-hex CHECKIN is no longer a syntax error: it is a name for the
    // resolver, and resolution needs a repository first.
    const uppercase = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
    var invalid = try runZit(gpa, io, tmp.dir, &.{ "log", uppercase });
    defer freeResult(gpa, &invalid);
    try expectExit(invalid.term, 1);
    try std.testing.expectEqualStrings("", invalid.stdout);
    try std.testing.expectEqualStrings(
        "zit: no zit checkout here (looked for .zit-checkout in this directory and its parents)\n" ++
            "if this is a bare repository, name it with -R, or run 'zit open' to create a working tree\n",
        invalid.stderr,
    );
}

test "cli log: CHECKIN resolves like info — branch, prefix, and an unknown name" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    var add = try runZit(gpa, io, tmp.dir, &.{ "add", "a.txt" });
    defer freeResult(gpa, &add);
    try expectExit(add.term, 0);
    var c1 = try runZit(gpa, io, tmp.dir, &.{ "commit", "-m", "root", "--user", "u" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    try writeFile(io, tmp.dir, "a.txt", "two\n");
    var c2 = try runZit(gpa, io, tmp.dir, &.{ "commit", "-m", "second", "--user", "u" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);

    var info = try runZit(gpa, io, tmp.dir, &.{ "info", "trunk" });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    const tip_full = info.stdout[0 .. std.mem.indexOfScalar(u8, info.stdout, '\n') orelse info.stdout.len];

    // The full-ID walk is the baseline every symbolic form must reproduce.
    var by_full = try runZit(gpa, io, tmp.dir, &.{ "log", tip_full });
    defer freeResult(gpa, &by_full);
    try expectExit(by_full.term, 0);

    var by_branch = try runZit(gpa, io, tmp.dir, &.{ "log", "trunk" });
    defer freeResult(gpa, &by_branch);
    try expectExit(by_branch.term, 0);
    try std.testing.expectEqualStrings(by_full.stdout, by_branch.stdout);

    var by_prefix = try runZit(gpa, io, tmp.dir, &.{ "log", tip_full[0..12] });
    defer freeResult(gpa, &by_prefix);
    try expectExit(by_prefix.term, 0);
    try std.testing.expectEqualStrings(by_full.stdout, by_prefix.stdout);

    var unknown = try runZit(gpa, io, tmp.dir, &.{ "log", "no-such-name" });
    defer freeResult(gpa, &unknown);
    try expectExit(unknown.term, 1);
    try std.testing.expectEqualStrings("", unknown.stdout);
    try std.testing.expectEqualStrings(
        "zit log: check-in 'no-such-name' not found\n",
        unknown.stderr,
    );
}

// ==========================================================================
// `zit log CHECKIN PATH` end-to-end — the finfo fold (contract B-log-2..9).
// Each test builds its DAG through real commits (add/modify/`rm`/`mv`/`merge`)
// and asserts the filtered walk names exactly the check-ins that change PATH,
// in the unfiltered DFS order (B-log-1's layout preserved). Teardown is
// unconditional.
// ==========================================================================

test "cli log B-log-2/-4/-1: PATH lists only the check-ins that change it; no-PATH is unchanged" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // root: a.txt is added (a root's presence alone is a change).
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    const root = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(root);

    // c2: a.txt modified (content hash differs from root's).
    try writeFile(io, tmp.dir, "a.txt", "two\n");
    const c2 = try commitHashDup(gpa, io, tmp.dir, "modify-a");
    defer gpa.free(c2);

    // c3: b.txt added; a.txt untouched — c3 must NOT appear in an a.txt log.
    try writeFile(io, tmp.dir, "b.txt", "b\n");
    try addManaged(gpa, io, tmp.dir, &.{"b.txt"});
    const c3 = try commitHashDup(gpa, io, tmp.dir, "add-b");
    defer gpa.free(c3);

    // c4: a.txt modified again.
    try writeFile(io, tmp.dir, "a.txt", "three\n");
    const c4 = try commitHashDup(gpa, io, tmp.dir, "modify-a-again");
    defer gpa.free(c4);

    // B-log-2/-4: filtered by a.txt = exactly {c4 modify, c2 modify, root add},
    // in DFS order; c3 (b.txt only) is walked but not named.
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", c4, "a.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var wbuf: [256]u8 = undefined;
    const want = try std.fmt.bufPrint(&wbuf, "checkin {s}\ncheckin {s}\ncheckin {s}\n", .{ c4, c2, root });
    try std.testing.expectEqualStrings(want, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);

    // B-log-1: the no-PATH walk is unchanged — every reachable check-in, DFS.
    var full = try runZit(gpa, io, tmp.dir, &.{ "log", c4 });
    defer freeResult(gpa, &full);
    try expectExit(full.term, 0);
    var fbuf: [320]u8 = undefined;
    const fwant = try std.fmt.bufPrint(&fbuf, "checkin {s}\ncheckin {s}\ncheckin {s}\ncheckin {s}\n", .{ c4, c3, c2, root });
    try std.testing.expectEqualStrings(fwant, full.stdout);
    try std.testing.expectEqualStrings("", full.stderr);

    // B-log-6: a path that never existed in the span yields empty stdout, exit 0.
    var none = try runZit(gpa, io, tmp.dir, &.{ "log", c4, "never.txt" });
    defer freeResult(gpa, &none);
    try expectExit(none.term, 0);
    try std.testing.expectEqualStrings("", none.stdout);
    try std.testing.expectEqualStrings("", none.stderr);

    // B-log-7: PATH is a path, not a pattern — a glob matches no entry (empty).
    var pat = try runZit(gpa, io, tmp.dir, &.{ "log", c4, "a.*" });
    defer freeResult(gpa, &pat);
    try expectExit(pat.term, 0);
    try std.testing.expectEqualStrings("", pat.stdout);
    try std.testing.expectEqualStrings("", pat.stderr);
}

test "cli log B-log-4: adds, deletes, and renames are changes at the affected paths" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // r: x.txt and old.txt both added.
    try writeFile(io, tmp.dir, "x.txt", "x\n");
    try writeFile(io, tmp.dir, "old.txt", "old\n");
    try addManaged(gpa, io, tmp.dir, &.{ "x.txt", "old.txt" });
    const r = try commitHashDup(gpa, io, tmp.dir, "add-both");
    defer gpa.free(r);

    // d: x.txt deleted (old.txt untouched).
    var rm = try runZit(gpa, io, tmp.dir, &.{ "rm", "x.txt" });
    defer freeResult(gpa, &rm);
    try expectExit(rm.term, 0);
    const d = try commitHashDup(gpa, io, tmp.dir, "delete-x");
    defer gpa.free(d);

    // m: old.txt renamed to new.txt (a delete+add at the two names).
    var mv = try runZit(gpa, io, tmp.dir, &.{ "mv", "old.txt", "new.txt" });
    defer freeResult(gpa, &mv);
    try expectExit(mv.term, 0);
    const m = try commitHashDup(gpa, io, tmp.dir, "rename-old-to-new");
    defer gpa.free(m);

    // x.txt changed at its add (r) and its delete (d); m does not touch it.
    var lx = try runZit(gpa, io, tmp.dir, &.{ "log", m, "x.txt" });
    defer freeResult(gpa, &lx);
    try expectExit(lx.term, 0);
    var xb: [256]u8 = undefined;
    try std.testing.expectEqualStrings(
        try std.fmt.bufPrint(&xb, "checkin {s}\ncheckin {s}\n", .{ d, r }),
        lx.stdout,
    );
    try std.testing.expectEqualStrings("", lx.stderr);

    // old.txt: added at r, departs at the rename m; unchanged at d.
    var lo = try runZit(gpa, io, tmp.dir, &.{ "log", m, "old.txt" });
    defer freeResult(gpa, &lo);
    try expectExit(lo.term, 0);
    var ob: [256]u8 = undefined;
    try std.testing.expectEqualStrings(
        try std.fmt.bufPrint(&ob, "checkin {s}\ncheckin {s}\n", .{ m, r }),
        lo.stdout,
    );
    try std.testing.expectEqualStrings("", lo.stderr);

    // new.txt: arrives only at the rename m (no old-name following).
    var ln = try runZit(gpa, io, tmp.dir, &.{ "log", m, "new.txt" });
    defer freeResult(gpa, &ln);
    try expectExit(ln.term, 0);
    var nb: [128]u8 = undefined;
    try std.testing.expectEqualStrings(
        try std.fmt.bufPrint(&nb, "checkin {s}\n", .{m}),
        ln.stdout,
    );
    try std.testing.expectEqualStrings("", ln.stderr);
}

test "cli log B-log-3: the PATH filter sees full ancestry across a merge parent" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // base: poem added.
    try writeFile(io, tmp.dir, "poem", "base\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const base = try commitHashDup(gpa, io, tmp.dir, "base");
    defer gpa.free(base);

    // other: poem changed on a side line reachable only via the merge parent.
    try writeFile(io, tmp.dir, "poem", "otherchange\n");
    const other = try commitHashDup(gpa, io, tmp.dir, "other");
    defer gpa.free(other);

    // current: back at base, add side.txt; poem is left at base content.
    var up = try runZit(gpa, io, tmp.dir, &.{ "update", base });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);
    try writeFile(io, tmp.dir, "side.txt", "s\n");
    try addManaged(gpa, io, tmp.dir, &.{"side.txt"});
    const current = try commitForkDup(gpa, io, tmp.dir, "current");
    defer gpa.free(current);

    // Merge other in (poem changed only on the other side → clean), then record
    // the merge as a check-in whose primary parent is current, merge parent other.
    var mg = try runZit(gpa, io, tmp.dir, &.{ "merge", other });
    defer freeResult(gpa, &mg);
    try expectExit(mg.term, 0);
    const merge_ci = try commitHashDup(gpa, io, tmp.dir, "merge");
    defer gpa.free(merge_ci);

    // Filtered by poem: merge_ci (adopts other's content vs current), base (add),
    // and other (the merged-in change) — current is walked but unchanged for poem.
    // `other` is reachable ONLY through the merge parent, so its presence proves
    // the walk spans full ancestry (a mainline-only walk would omit it).
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", merge_ci, "poem" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var wbuf: [320]u8 = undefined;
    const want = try std.fmt.bufPrint(&wbuf, "checkin {s}\ncheckin {s}\ncheckin {s}\n", .{ merge_ci, base, other });
    try std.testing.expectEqualStrings(want, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli log B-log-9: PATH adds no new refusal surface — an unresolvable CHECKIN still fails cleanly" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const missing = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", missing, "a.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    var want: [160]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "zit log: check-in '{s}' not found\n", .{missing});
    try std.testing.expectEqualStrings(expected, res.stderr);
}

// ==========================================================================
// `zit leaves` end-to-end (repo-state §7.1/§7.4/§7.5/§7.7 + CLI discipline).
// Each test spawns the REAL binary in an isolated tmp dir, asserts exit code
// and stdout/stderr, and builds its DAG through real commits (a fork via
// `commit` → `update` → `commit`) plus, where a closed leaf is needed, a
// synthesized `closed` control artifact (no CLI verb closes a leaf yet).
// Teardown is unconditional.
// ==========================================================================

/// Counts newline-terminated lines in `s` (the leaf listing is one line per
/// leaf, so this is the count a `| wc -l` pipe would report).
fn lineCount(s: []const u8) usize {
    return std.mem.count(u8, s, "\n");
}

test "cli leaves B-leaves-8: 'leaves' is listed in the command help and suggested for a near-miss" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // The top-level command list names the verb (discoverability).
    var help = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &help);
    try expectExit(help.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "leaves") != null);

    // A near-miss typo suggests the real verb (did-you-mean over known_verbs).
    var typo = try runZit(gpa, io, tmp.dir, &.{"leavez"});
    defer freeResult(gpa, &typo);
    try expectExit(typo.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, typo.stderr, "did you mean 'leaves'?") != null);
}

test "cli leaves B-leaves-9: --help and 'help leaves' print the verbatim help to stdout, exit 0, empty stderr" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const expected =
        \\usage: zit leaves [--all] [--no-cache] [-h|--help]
        \\
        \\List the open leaves of the check-in DAG — check-ins with no child on the
        \\same branch that carry no active closed tag. Each line is Fossil's:
        \\`(N) time [hash] comment (user: … tags: …)`, with the weekday and a
        \\twelve-character hash; the checkout's leaf carries *CURRENT*, as in
        \\timeline. Leaves are ordered most-recent first.
        \\
        \\options:
        \\      --all       also list closed leaves, tagged "closed"
        \\      --no-cache  bypass every derived sidecar (the cache AND the rebuild
        \\                  checkpoint) and re-derive from the full log, re-verifying
        \\                  whole-log integrity (the cache is refreshed)
        \\  -R, --repo <path>
        \\                read this repository instead of the one found from the
        \\                current directory; <path> is the directory holding '.zit'
        \\                (or the '.zit' file itself). No working tree is required.
        \\  -h, --help      show this help and exit
        \\
        \\exit status:
        \\  0  success
        \\  1  operational failure (not a repository, unreadable state, a refused
        \\     action) — the diagnostic on stderr names it
        \\  2  usage error (an unknown option or unexpected argument)
        \\
    ;
    for ([_][]const u8{ "--help", "-h" }) |flag| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "leaves", flag });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
    // `zit help leaves` teaches the identical text (DX D4).
    var routed = try runZit(gpa, io, tmp.dir, &.{ "help", "leaves" });
    defer freeResult(gpa, &routed);
    try expectExit(routed.term, 0);
    try std.testing.expectEqualStrings(expected, routed.stdout);
}

test "cli leaves B-leaves-10: an unknown option is a usage error (exit 2, stderr, empty stdout)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{ "leaves", "-x" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit leaves: unknown option '-x'\ntry 'zit leaves --help'\n",
        res.stderr,
    );
}

test "cli leaves B-leaves-11: outside a repository fails exit 1 on stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no zit checkout here") != null);
}

test "cli leaves B-leaves-12: an unborn repository lists no leaves (empty stdout, exit 0)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try std.testing.expectEqual(@as(usize, 0), lineCount(res.stdout));
}

test "cli leaves §7.1: a single-commit repo lists its lone check-in as the open leaf, marked '*'" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const root = try checkinHash(commit.stdout);

    var res = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // Data on stdout, diagnostics on stderr — the streams never mix (cli-stream).
    try std.testing.expectEqualStrings("", res.stderr);
    var want: [96]u8 = undefined;
    // Fossil's leaf line: one line, the root's short name, *CURRENT*.
    _ = &want;
    try std.testing.expectEqual(@as(usize, 1), lineCount(res.stdout));
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, cli.shortHash(root)) != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "*CURRENT*") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "tags: trunk") != null);
    try std.testing.expectEqual(@as(usize, 1), lineCount(res.stdout));
}

/// Builds a two-open-leaf trunk fork with distinct modification times: a root,
/// then `child1` (older), then — after `update` back to the root — `child2`
/// (newer), which becomes the current checkout. Returns both leaf IDs (caller
/// frees each).
fn buildFork(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !struct { child1: []const u8, child2: []const u8 } {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "a.txt", "root\n");
    try addManaged(gpa, io, dir, &.{"a.txt"});

    var c0 = try runCommitEnv(gpa, io, dir, .{ .user = "u", .epoch = "900000000" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &c0);
    try expectExit(c0.term, 0);
    const root = try gpa.dupe(u8, try checkinHash(c0.stdout));
    defer gpa.free(root);

    try writeFile(io, dir, "a.txt", "one\n");
    var c1 = try runCommitEnv(gpa, io, dir, .{ .user = "u", .epoch = "1000000000" }, &.{ "commit", "-m", "child1" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    const child1 = try gpa.dupe(u8, try checkinHash(c1.stdout));
    errdefer gpa.free(child1);

    var up = try runZit(gpa, io, dir, &.{ "update", root });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);

    try writeFile(io, dir, "a.txt", "two\n");
    var c2 = try runCommitEnv(gpa, io, dir, .{ .user = "u", .epoch = "2000000000" }, &.{ "commit", "-m", "child2", "--allow-fork" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    const child2 = try gpa.dupe(u8, try checkinHash(c2.stdout));

    return .{ .child1 = child1, .child2 = child2 };
}

test "cli leaves §7.5: two open leaves are ordered mtime-descending and the current checkout is marked '*'" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const fork = try buildFork(gpa, io, tmp.dir);
    defer gpa.free(fork.child1);
    defer gpa.free(fork.child2);

    var res = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr);
    // §7.5: newest first (child2, epoch 2e9), then child1 (epoch 1e9); child2 is
    // the current checkout, so it is marked '*'.
    var want: [200]u8 = undefined;
    // mtime-descending: child2's line first, and *CURRENT* on it (Fossil's shape).
    _ = &want;
    try std.testing.expectEqual(@as(usize, 2), lineCount(res.stdout));
    const at2 = std.mem.indexOf(u8, res.stdout, cli.shortHash(fork.child2)) orelse return error.MissingLeaf;
    const at1 = std.mem.indexOf(u8, res.stdout, cli.shortHash(fork.child1)) orelse return error.MissingLeaf;
    try std.testing.expect(at2 < at1);
    const cur_at = std.mem.indexOf(u8, res.stdout, "*CURRENT*") orelse return error.MissingCurrent;
    try std.testing.expect(cur_at < at1);
    try std.testing.expectEqual(@as(usize, 2), lineCount(res.stdout));
}

test "cli leaves --no-cache forces a full derive and yields output identical to the cached path" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // A two-open-leaf fork gives a non-trivial derived view to compare across
    // the cache-hit path and the forced full re-derive.
    const fork = try buildFork(gpa, io, tmp.dir);
    defer gpa.free(fork.child1);
    defer gpa.free(fork.child2);

    // The first read misses, derives from the full log, and writes the memo.
    var warm = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &warm);
    try expectExit(warm.term, 0);
    try std.testing.expect(fileExists(io, tmp.dir, ".zit-cache"));

    // With the cache present, the default read replays it (the O(1) fast path).
    var cached = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &cached);
    try expectExit(cached.term, 0);

    // --no-cache bypasses `tryLoad` and re-scans the whole log. It is the same
    // derivation, so the output is byte-identical — the flag changes only which
    // path produced it, never what it produces.
    var bypass = try runZit(gpa, io, tmp.dir, &.{ "leaves", "--no-cache" });
    defer freeResult(gpa, &bypass);
    try expectExit(bypass.term, 0);
    try std.testing.expectEqualStrings("", bypass.stderr);
    try std.testing.expectEqualStrings(cached.stdout, bypass.stdout);
    // Non-empty ⇒ the equality is a real comparison, not two empty listings.
    try std.testing.expectEqual(@as(usize, 2), lineCount(bypass.stdout));
    // The bypass path just validated the whole log, so it refreshes the memo
    // rather than leaving a stale one — the next reader gets a trustworthy cache.
    try std.testing.expect(fileExists(io, tmp.dir, ".zit-cache"));
}

test "cli leaves §7.4/§7.7: --all lists a closed leaf marked (closed) that the default listing omits" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const fork = try buildFork(gpa, io, tmp.dir);
    defer gpa.free(fork.child1);
    defer gpa.free(fork.child2);

    // Close the older leaf (child1) with a synthesized control artifact (§7.4).
    const ctl = try appendClosedControl(gpa, io, tmp.dir, "2020-01-01T00:00:00", fork.child1);
    defer gpa.free(ctl);

    // Default listing: §7.4 open predicate excludes the closed leaf — only the
    // open child2 remains (enumeration filters by leaf-table membership + the
    // open test, never by name resolution — §7.7).
    var def = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &def);
    try expectExit(def.term, 0);
    try std.testing.expectEqualStrings("", def.stderr);
    var want_def: [96]u8 = undefined;
    _ = &want_def;
    try std.testing.expectEqual(@as(usize, 1), lineCount(def.stdout));
    try std.testing.expect(std.mem.indexOf(u8, def.stdout, cli.shortHash(fork.child2)) != null);
    try std.testing.expect(std.mem.indexOf(u8, def.stdout, cli.shortHash(fork.child1)) == null);

    // --all: the closed leaf stays a leaf-table member and is listed, marked
    // "(closed)"; still mtime-descending, still the checkout marked '*'.
    var all = try runZit(gpa, io, tmp.dir, &.{ "leaves", "--all" });
    defer freeResult(gpa, &all);
    try expectExit(all.term, 0);
    try std.testing.expectEqualStrings("", all.stderr);
    var want_all: [220]u8 = undefined;
    _ = &want_all;
    try std.testing.expectEqual(@as(usize, 2), lineCount(all.stdout));
    const a2 = std.mem.indexOf(u8, all.stdout, cli.shortHash(fork.child2)) orelse return error.MissingLeaf;
    const a1 = std.mem.indexOf(u8, all.stdout, cli.shortHash(fork.child1)) orelse return error.MissingLeaf;
    try std.testing.expect(a2 < a1);
    const closed_at = std.mem.indexOf(u8, all.stdout, ", closed") orelse return error.MissingClosed;
    try std.testing.expect(closed_at > a1);
}

// ==========================================================================
// `zit timeline` end-to-end (repo-state §2/§3.2/§6.1/§6.4/§8.3 + CLI
// discipline). The global chronological history: every check-in, event.mtime
// descending, ties by full artifact hash. Each test spawns the REAL binary in
// an isolated tmp dir. Behavior ids cite contracts/timeline.md B-timeline-*.
// ==========================================================================

// ISO §4.6.4 writer forms of the injected commit epochs (whole Unix seconds,
// verified with `date -u`): the timeline date column derives from the stored
// event.mtime, which is the injected epoch carried in milliseconds.
const iso_epoch_900e6 = "1998-07-09T16:00:00"; // SOURCE_DATE_EPOCH=900000000
const iso_epoch_1e9 = "2001-09-09T01:46:40"; // SOURCE_DATE_EPOCH=1000000000
const iso_epoch_2e9 = "2033-05-18T03:33:20"; // SOURCE_DATE_EPOCH=2000000000

/// Writes `content` to `name`, optionally stages it (`stage` on the first
/// commit of a path), then commits at `epoch` with `msg`. Returns the new
/// check-in's 64-hex hash (caller frees) — the setup for the timeline ordering
/// tests, which need distinct, injected mtimes.
fn timelineCommit(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    name: []const u8,
    content: []const u8,
    stage: bool,
    epoch: []const u8,
    msg: []const u8,
) ![]u8 {
    try writeFile(io, dir, name, content);
    if (stage) try addManaged(gpa, io, dir, &.{name});
    var c = try runCommitEnv(gpa, io, dir, .{ .user = "u", .epoch = epoch }, &.{ "commit", "-m", msg });
    defer freeResult(gpa, &c);
    try expectExit(c.term, 0);
    return gpa.dupe(u8, try checkinHash(c.stdout));
}

test "cli timeline B-timeline-1: full listing, one line per check-in, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const root = try timelineCommit(gpa, io, tmp.dir, "a.txt", "root\n", true, "900000000", "root");
    defer gpa.free(root);
    const mid = try timelineCommit(gpa, io, tmp.dir, "a.txt", "mid\n", false, "1000000000", "mid");
    defer gpa.free(mid);
    const tip = try timelineCommit(gpa, io, tmp.dir, "a.txt", "tip\n", false, "2000000000", "tip");
    defer gpa.free(tip);

    var res = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // Data on stdout, diagnostics on stderr — the streams never mix (cli-stream).
    try std.testing.expectEqualStrings("", res.stderr);
    // Every crosslinked check-in appears exactly once: three commits ⇒ three lines.
    try std.testing.expectEqual(@as(usize, 3), lineCount(res.stdout));
}

test "cli timeline B-timeline-2: rows are ordered event.mtime descending, each dated from its mtime" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const root = try timelineCommit(gpa, io, tmp.dir, "a.txt", "root\n", true, "900000000", "root");
    defer gpa.free(root);
    const mid = try timelineCommit(gpa, io, tmp.dir, "a.txt", "mid\n", false, "1000000000", "mid");
    defer gpa.free(mid);
    const tip = try timelineCommit(gpa, io, tmp.dir, "a.txt", "tip\n", false, "2000000000", "tip");
    defer gpa.free(tip);

    var res = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr);
    // §2/§8.3 item 7: most recent first — tip (2e9), mid (1e9), root (900e6) —
    // the whole-repo clock, not one check-in's ancestry (timeline's diff vs log).
    var want: [512]u8 = undefined;
    const expected = try std.fmt.bufPrint(
        &want,
        "  {s}  " ++ iso_epoch_2e9 ++ "  trunk  tip\n" ++
            "  {s}  " ++ iso_epoch_1e9 ++ "  trunk  mid\n" ++
            "  {s}  " ++ iso_epoch_900e6 ++ "  trunk  root\n",
        .{ tip[0..12], mid[0..12], root[0..12] },
    );
    try std.testing.expectEqualStrings(expected, res.stdout);
}

test "cli timeline B-timeline-3: exact-mtime ties list the descendant first (B1), byte-reproducible across runs" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Two check-ins at the SAME epoch ⇒ equal event.mtime. They are a CHAIN,
    // so ancestry decides the tie (B1: a descendant always lists before its
    // ancestor — the generation key), never hash order; hash breaks ties only
    // between true concurrents. Deterministic and byte-reproducible either way.
    const root = try timelineCommit(gpa, io, tmp.dir, "a.txt", "root\n", true, "1000000000", "root");
    defer gpa.free(root);
    const child = try timelineCommit(gpa, io, tmp.dir, "a.txt", "child\n", false, "1000000000", "child");
    defer gpa.free(child);

    var res1 = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &res1);
    try expectExit(res1.term, 0);
    try std.testing.expectEqualStrings("", res1.stderr);
    var res2 = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &res2);
    try expectExit(res2.term, 0);
    // Byte-reproducible: a second run yields identical bytes.
    try std.testing.expectEqualStrings(res1.stdout, res2.stdout);
    // Genuinely two distinct check-ins tied on mtime (non-vacuous comparison).
    try std.testing.expect(!std.mem.eql(u8, root, child));

    var want: [512]u8 = undefined;
    const expected = try std.fmt.bufPrint(
        &want,
        "  {s}  " ++ iso_epoch_1e9 ++ "  trunk  child\n" ++
            "  {s}  " ++ iso_epoch_1e9 ++ "  trunk  root\n",
        .{ child[0..12], root[0..12] },
    );
    try std.testing.expectEqualStrings(expected, res1.stdout);
}

test "cli timeline B-timeline-4 (§6.1): the branch column is each check-in's own branch" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const root = try timelineCommit(gpa, io, tmp.dir, "a.txt", "root\n", true, "1000000000", "root");
    defer gpa.free(root);
    // A `--branch feature` fork lands the newer check-in on feature (§6.1/§6.2).
    try writeFile(io, tmp.dir, "a.txt", "two\n");
    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u", .epoch = "2000000000" }, &.{ "commit", "-m", "feature work", "--branch", "feature" });
    defer freeResult(gpa, &c);
    try expectExit(c.term, 0);
    const feat = try gpa.dupe(u8, try checkinHash(c.stdout));
    defer gpa.free(feat);

    var res = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr);
    var want: [512]u8 = undefined;
    const expected = try std.fmt.bufPrint(
        &want,
        "  {s}  " ++ iso_epoch_2e9 ++ "  feature  feature work\n" ++
            "  {s}  " ++ iso_epoch_1e9 ++ "  trunk  root\n",
        .{ feat[0..12], root[0..12] },
    );
    try std.testing.expectEqualStrings(expected, res.stdout);
}

test "cli timeline B-timeline-4 (§6.4): a tagless check-in shows the configured main-branch, never literal trunk" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // §6.4: set main-branch to a non-default name, then add a check-in with NO
    // branch tag. Its branch is the CONFIGURED value, never the literal "trunk".
    try writeDefaultBranch(gpa, io, tmp.dir, "main");
    const id = try appendSyntheticCheckin(gpa, io, tmp.dir, "tagless", null);
    defer gpa.free(id);

    var res = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr);
    var want: [256]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "  {s}  2020-01-01T00:00:00  main  tagless\n", .{id[0..12]});
    try std.testing.expectEqualStrings(expected, res.stdout);
    // §6.4 refutation: the configured name is emitted, the literal never is.
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "trunk") == null);
}

test "cli timeline B-timeline-5: a merge check-in is marked M; primary-only check-ins are not" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Two roots and a merge joining them (§3.2: base primary, other a merge
    // parent). Built by hand — a two-P-card manifest — since no `zit merge` exists.
    const base = try appendSyntheticCheckin(gpa, io, tmp.dir, "base", null);
    defer gpa.free(base);
    const other = try appendSyntheticCheckin(gpa, io, tmp.dir, "other", null);
    defer gpa.free(other);
    var pbuf: [160]u8 = undefined;
    const parents = try std.fmt.bufPrint(&pbuf, "{s} {s}", .{ base, other });
    const merge = try appendSyntheticCheckin(gpa, io, tmp.dir, "merge", parents);
    defer gpa.free(merge);

    var res = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr);
    // Three check-ins, one line each (the merge is never folded into a parent).
    try std.testing.expectEqual(@as(usize, 3), lineCount(res.stdout));
    // §3.2: the merge line (>=1 merge parent) carries "M "; the two roots carry "  ".
    var mbuf: [160]u8 = undefined;
    const merge_line = try std.fmt.bufPrint(&mbuf, "M {s}  2020-01-01T00:00:00  trunk  merge\n", .{merge[0..12]});
    var bbuf: [160]u8 = undefined;
    const base_line = try std.fmt.bufPrint(&bbuf, "  {s}  2020-01-01T00:00:00  trunk  base\n", .{base[0..12]});
    var obuf: [160]u8 = undefined;
    const other_line = try std.fmt.bufPrint(&obuf, "  {s}  2020-01-01T00:00:00  trunk  other\n", .{other[0..12]});
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, merge_line) != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, base_line) != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, other_line) != null);
}

test "cli timeline B-timeline-6: a multi-line comment contributes only its first line" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const id = try appendSyntheticCheckin(gpa, io, tmp.dir, "first line\nsecond line", null);
    defer gpa.free(id);

    var res = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr);
    // The embedded newline is cut: one line, first line only (invariant held).
    try std.testing.expectEqual(@as(usize, 1), lineCount(res.stdout));
    var want: [256]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "  {s}  2020-01-01T00:00:00  trunk  first line\n", .{id[0..12]});
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "second line") == null);
}

test "cli timeline B-timeline-7: an empty repository yields empty stdout, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"timeline"});
    defer freeResult(gpa, &res);
    // Emptiness is defined out of existence: a success with no rows, no diagnostic.
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try std.testing.expectEqual(@as(usize, 0), lineCount(res.stdout));
}

test "cli timeline B-timeline-8: -h/--help print the verbatim help to stdout, exit 0, help wins" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const expected =
        \\usage: zit timeline [-n N] [--offset N] [-b BRANCH | -c] [-p PATH] [-u LOGIN]
        \\                    [-r] [-v] [-F oneline|medium|full|html] [--weekday]
        \\                    [-R <path>] [-h|--help]
        \\
        \\List every check-in in the repository, most recent first. Each line shows
        \\the check-in's short hash, its ISO timestamp, its branch, and the first
        \\line of its comment. A check-in that joins two or more lines of development
        \\is marked "M". Order is by event time descending, ties broken by artifact
        \\hash, so the listing is byte-identical across runs.
        \\
        \\Filters narrow the listing: -b BRANCH to one branch, -c to the checkout's
        \\branch, -p PATH to the check-ins that change PATH against their primary
        \\parent, -u LOGIN to one author. -r lists oldest first; --offset N skips
        \\the first N matches before -n counts. The default (-F medium) is
        \\Fossil's shape — a date header once per day, with the weekday, then
        \\time, a twelve-character hash, *CURRENT* on the checkout's check-in,
        \\the comment, and (user: … tags: …); -F oneline is one line per
        \\check-in (hash, ISO time, branch, comment) for scripts; -F full adds
        \\each check-in's changed files, which -v adds to any format.
        \\
        \\--weekday prints each check-in's day of week between the hash and the
        \\date. Days are UTC, like every timestamp here, so a late Friday commit
        \\in your local zone can read Saturday. The computation is Ben Joffe's
        \\branchless day-of-week (benjoffe.com/fast-day-of-week).
        \\
        \\options:
        \\  -n, --limit N show only the N most recent entries; 0 means all
        \\      --offset N  skip the first N matching entries
        \\  -b, --branch B  only check-ins on branch B
        \\  -c, --current-branch  only check-ins on the checkout's branch
        \\  -p, --path P  only check-ins that change P
        \\  -u, --for-user U  only check-ins by U
        \\  -r, --reverse oldest first
        \\  -v, --verbose list each check-in's changed files
        \\  -F, --format F  oneline (default), medium, or full;
        \\                --oneline/--medium/--full are the same
        \\      --weekday show the UTC day of week beside each date
        \\  -R, --repo <path>
        \\                read this repository instead of the one found from the
        \\                current directory; <path> is the directory holding '.zit'
        \\                (or the '.zit' file itself). No working tree is required.
        \\  -h, --help    show this help and exit
        \\
        \\exit status:
        \\  0  success
        \\  1  operational failure (not a repository, unreadable state, a refused
        \\     action) — the diagnostic on stderr names it
        \\  2  usage error (an unknown option or unexpected argument)
        \\
    ;
    // -h/--help win even over an otherwise-invalid trailing token.
    for ([_][]const []const u8{ &.{ "timeline", "-h" }, &.{ "timeline", "--help" }, &.{ "timeline", "-h", "foo" } }) |args| {
        var res = try runZit(gpa, io, tmp.dir, args);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
    // `zit help timeline` teaches the identical text (DX D4).
    var routed = try runZit(gpa, io, tmp.dir, &.{ "help", "timeline" });
    defer freeResult(gpa, &routed);
    try expectExit(routed.term, 0);
    try std.testing.expectEqualStrings(expected, routed.stdout);
}

test "cli timeline B-timeline-9: an unknown option is a usage error (exit 2, stderr, empty stdout)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    for ([_][]const u8{ "-n", "--bogus" }) |opt| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "timeline", opt });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        var want: [160]u8 = undefined;
        const expected = try std.fmt.bufPrint(&want, "zit timeline: unknown option '{s}'\ntry 'zit timeline --help'\n", .{opt});
        try std.testing.expectEqualStrings(expected, res.stderr);
    }
}

test "cli timeline B-timeline-10: an extra argument is a usage error (exit 2, stderr, empty stdout)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // A bare positional and any token after `--` are operands timeline rejects.
    for ([_][]const []const u8{ &.{ "timeline", "foo" }, &.{ "timeline", "--", "foo" } }) |args| {
        var res = try runZit(gpa, io, tmp.dir, args);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings("zit timeline: unexpected argument 'foo'\ntry 'zit timeline --help'\n", res.stderr);
    }
}

test "cli timeline B-timeline-11: outside a repository fails exit 1 on stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Data-layer failure (exit 1), distinct from the exit-2 usage class.
    var res = try runZit(gpa, io, tmp.dir, &.{"timeline"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no zit checkout here") != null);
}

test "cli timeline B-timeline-12: `zit timeline >> log` appends (O_APPEND honored), rows to stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    const z = try appendSyntheticCheckin(gpa, io, tmp.dir, "z", null);
    defer gpa.free(z);

    // A streaming stdout writer honors the inherited O_APPEND offset: the row
    // lands after the pre-existing content, never clobbering it (cli-stream).
    var res = try runShell(gpa, io, tmp.dir, "printf 'PRE\\n' > log; \"$ZIT\" timeline -F oneline >> log");
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    const log = try readFile(gpa, io, tmp.dir, "log");
    defer gpa.free(log);
    var want: [256]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "PRE\n  {s}  2020-01-01T00:00:00  trunk  z\n", .{z[0..12]});
    try std.testing.expectEqualStrings(expected, log);
}

test "cli timeline: 'timeline' is listed in the command help and suggested for a near-miss" {
    // cli-verb-enum-wiring: the verb joins the single-source Verb enum, so the
    // command list names it and the did-you-mean suggester finds it (mirror leaves).
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var help = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &help);
    try expectExit(help.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "timeline") != null);

    var typo = try runZit(gpa, io, tmp.dir, &.{"timelien"});
    defer freeResult(gpa, &typo);
    try expectExit(typo.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, typo.stderr, "did you mean 'timeline'?") != null);
}

// ==========================================================================
// `zit branch` end-to-end (repo-state §6.1/§6.3/§6.4/§9 + CLI discipline).
// The multi-branch, closed, forked, and non-trunk-config derivations are unit-
// tested (no CLI creates a branch yet), so these e2e cases exercise the single
// trunk the CLI can build plus the full usage/help/stream/exit contract.
// ==========================================================================

const branch_help_expected =
    \\usage: zit branch [list] [-c] [-t] [-r] [-v] [--no-cache]
    \\       zit branch new NAME [BASIS]
    \\       zit branch (close | reopen | hide | unhide) NAME
    \\       zit branch current
    \\       zit branch info NAME
    \\
    \\List the repository's branches, one per line, ordered by name. Branches are
    \\derived from the active branch tags of the check-ins; the branch of the current
    \\checkout is marked with a leading "*". A branch whose most recent check-in is
    \\closed is marked "(closed)".
    \\
    \\The other actions are Fossil's. 'new' records a check-in that
    \\changes no file and starts NAME at the checkout's check-in (BASIS, when
    \\given, must be that check-in — update to it first). 'close' and 'reopen'
    \\add and cancel the closed tag on NAME's tip; 'hide' and 'unhide' do the
    \\same with the hidden tag. 'current' prints the checkout's branch; 'info'
    \\prints NAME's tip, count and root.
    \\
    \\options:
    \\  -v, --verbose   each branch with its tip and time
    \\  -c, --closed    list closed branches only
    \\  -t              most recently changed first
    \\  -r              reverse the order
    \\      --no-cache  bypass every derived sidecar (the cache AND the rebuild
    \\                  checkpoint) and re-derive from the full log, re-verifying
    \\                  whole-log integrity (the cache is refreshed)
    \\  -R, --repo <path>
    \\                read this repository instead of the one found from the
    \\                current directory; <path> is the directory holding '.zit'
    \\                (or the '.zit' file itself). No working tree is required.
    \\  -h, --help      show this help and exit
    \\
    \\exit status:
    \\  0  success
    \\  1  operational failure (not a repository, unreadable state, a refused
    \\     action) — the diagnostic on stderr names it
    \\  2  usage error (an unknown option or unexpected argument)
    \\
;

test "cli branch §6.3: a single-commit repo lists trunk marked '*'" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    _ = try checkinHash(commit.stdout);

    // §6.3: one branch derived from the root's active branch tag, named "trunk";
    // §6.1/§6.4: the checkout is on it, so it is marked. Data on stdout only.
    var res = try runZit(gpa, io, tmp.dir, &.{"branch"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("* trunk\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli branch §6.4: the current mark is the active-tag branch-of the checkout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // A trunk fork (two open leaves, checkout on the newer). §6.3 still groups
    // them into one "trunk" entry; §6.4 marks it because the checkout resolves
    // to trunk through the active-tag filter.
    const fork = try buildFork(gpa, io, tmp.dir);
    defer gpa.free(fork.child1);
    defer gpa.free(fork.child2);

    var res = try runZit(gpa, io, tmp.dir, &.{"branch"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("* trunk\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try std.testing.expectEqual(@as(usize, 1), lineCount(res.stdout));
}

test "cli branch B-branch-3: an unborn repository lists nothing (empty stdout, exit 0)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"branch"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli branch B-branch-4: an unknown option is a usage error (exit 2, stderr, empty stdout)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{ "branch", "-x" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit branch: unknown option '-x'\ntry 'zit branch --help'\n",
        res.stderr,
    );
}

test "cli branch B-branch-5: an extra argument is a usage error (exit 2, stderr, empty stdout)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{ "branch", "feature" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit branch: unexpected argument 'feature'\ntry 'zit branch --help'\n",
        res.stderr,
    );
}

test "cli branch B-branch-6: --help / -h / 'help branch' print the verbatim help (stdout, exit 0)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    for ([_][]const u8{ "--help", "-h" }) |flag| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "branch", flag });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(branch_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
    var routed = try runZit(gpa, io, tmp.dir, &.{ "help", "branch" });
    defer freeResult(gpa, &routed);
    try expectExit(routed.term, 0);
    try std.testing.expectEqualStrings(branch_help_expected, routed.stdout);
}

test "cli branch B-branch-7: outside a repository fails exit 1 on stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{"branch"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no zit checkout here") != null);
}

test "cli branch B-branch-8: a corrupt repository fails exit 1 with a stderr diagnostic" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var initial = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_only });
    const frame_offset = (try initial.stat(io)).size;
    initial.close(io);
    const root = try appendSyntheticCheckin(gpa, io, tmp.dir, "root", null);
    defer gpa.free(root);

    // Flip a byte inside the appended frame so the log reader rejects it.
    var file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_write });
    var byte: [1]u8 = undefined;
    try std.testing.expectEqual(@as(usize, 1), try file.readPositionalAll(io, &byte, frame_offset + 10));
    byte[0] ^= 0xff;
    try file.writePositionalAll(io, &byte, frame_offset + 10);
    file.close(io);

    var res = try runZit(gpa, io, tmp.dir, &.{"branch"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit branch: cannot read repository: repository is corrupt\n",
        res.stderr,
    );
}

test "cli branch B-branch-9: 'branch' is listed in top help and suggested for a near-miss" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var help = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &help);
    try expectExit(help.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "branch") != null);

    var typo = try runZit(gpa, io, tmp.dir, &.{"brnach"});
    defer freeResult(gpa, &typo);
    try expectExit(typo.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, typo.stderr, "did you mean 'branch'?") != null);
}

test "cli branch §6.4: a hand-edited .zit-checkout naming an unknown version leaves nothing marked (exit 0)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    _ = try checkinHash(commit.stdout);

    // Overwrite the checkout record with a well-formed-but-unknown version. The
    // current-mark resolver returns null (never interned), so trunk lists with
    // no "*"; the branch set itself (§6.3) is unaffected. Never an error.
    const unknown = "b" ** 64;
    try checkout.write(io, tmp.dir, .{ .version = unknown, .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" });

    var res = try runZit(gpa, io, tmp.dir, &.{"branch"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("  trunk\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

// --- tag verb (repo-state §5.1/§5.2, read-only enumeration) ----------------

/// An independent transcription of the `zit tag` §Help block (must match
/// `main.zig`'s `tag_help` byte-for-byte; drift fails the test).
const tag_help_expected =
    \\usage: zit tag list [--raw] [CHECKIN] [--no-cache]
    \\       zit tag add [--raw] [--propagate] NAME CHECKIN [VALUE]
    \\       zit tag cancel [--raw] NAME CHECKIN
    \\
    \\List tags, name a check-in, or attach a property to one. Plain list prints
    \\active singleton symbolic release names across the repository or on
    \\CHECKIN, one per line, sorted. Branch names, raw properties, and
    \\propagating tags are excluded. List --raw prints every active tag with
    \\its stored prefix and value.
    \\
    \\A tag is one of two things, and --raw is what picks between them.
    \\
    \\Without --raw, NAME is a symbolic NAME for one check-in: it resolves to
    \\that check-in everywhere a check-in name is accepted, and it stays put —
    \\unlike a branch name, later check-ins do not inherit it. Use it for
    \\release names. Two check-ins carrying one name make that name ambiguous.
    \\
    \\With --raw, NAME is written verbatim as a PROPERTY, outside name
    \\resolution entirely. Use it for a fact many check-ins may carry — a CI
    \\verdict, a review marker — optionally with a VALUE. Because raw tags are
    \\not names, plain list does not show them; list --raw does.
    \\
    \\cancel retracts a tag from a check-in. It takes the same --raw choice as
    \\the add that created it: cancelling 'v1' and cancelling '--raw v1' are
    \\different tags.
    \\
    \\Names beginning wiki-, tkt-, or event- are refused: fossil crosslinks
    \\wiki pages, tickets, and technotes through them.
    \\
    \\  ls          alias for list
    \\
    \\options:
    \\      --raw        NAME is a raw tag name, not a symbolic check-in name;
    \\                   on list, show every tag with its prefix intact
    \\      --propagate  add only: descendants inherit the tag (mint '*', not
    \\                   '+'). Hazardous without --raw: an inherited symbolic
    \\                   name drifts to the newest descendant carrying it.
    \\      --no-cache  bypass every derived sidecar (the cache AND the rebuild
    \\                  checkpoint) and re-derive from the full log, re-verifying
    \\                  whole-log integrity (the cache is refreshed)
    \\  -R, --repo <path>
    \\                read this repository instead of the one found from the
    \\                current directory; <path> is the directory holding '.zit'
    \\                (or the '.zit' file itself). No working tree is required.
    \\  -h, --help  show this help
    \\
    \\exit status:
    \\  0  success
    \\  1  operational failure (not a repository, unreadable state, a refused
    \\     action) — the diagnostic on stderr names it
    \\  2  usage error (an unknown option or unexpected argument)
    \\
;

/// Builds a raw check-in manifest with an injected T-card block and appends it
/// to `.zit`, returning its 64-hex artifact name (caller frees). The card run
/// C/D/F/<t>/U/Z is a valid check-in skeleton; a malformed `t_block` makes
/// `artifact.parse` reject the whole frame, so the loader skips it. Mirrors
/// `appendRawCheckinWithUppercaseParent`'s hand-framed, self-checksummed shape.
fn appendRawManifestWithT(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    comment: []const u8,
    t_block: []const u8,
) ![]u8 {
    var body_buf: [512]u8 = undefined;
    const body = try std.fmt.bufPrint(
        &body_buf,
        "C {s}\n" ++
            "D 2020-01-01T00:00:00\n" ++
            "F f 0000000000000000000000000000000000000000000000000000000000000000\n" ++
            "{s}" ++
            "U u\n",
        .{ comment, t_block },
    );
    var raw_buf: [576]u8 = undefined;
    @memcpy(raw_buf[0..body.len], body);
    var checksum: [32]u8 = undefined;
    const digest = artifact.selfChecksum(body, &checksum);
    raw_buf[body.len] = 'Z';
    raw_buf[body.len + 1] = ' ';
    @memcpy(raw_buf[body.len + 2 ..][0..digest.len], digest);
    raw_buf[body.len + 34] = '\n';
    return appendArtifact(gpa, io, dir, raw_buf[0 .. body.len + 35]);
}

/// Builds a valid cluster artifact (card run M/Z — a legitimate non-check-in
/// artifact type the crosslink loader admits then skips) and appends it,
/// returning its 64-hex name (caller frees).
fn appendCluster(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]u8 {
    var body_buf: [128]u8 = undefined;
    const body = try std.fmt.bufPrint(
        &body_buf,
        "M 1111111111111111111111111111111111111111111111111111111111111111\n",
        .{},
    );
    var raw_buf: [192]u8 = undefined;
    @memcpy(raw_buf[0..body.len], body);
    var checksum: [32]u8 = undefined;
    const digest = artifact.selfChecksum(body, &checksum);
    raw_buf[body.len] = 'Z';
    raw_buf[body.len + 1] = ' ';
    @memcpy(raw_buf[body.len + 2 ..][0..digest.len], digest);
    raw_buf[body.len + 34] = '\n';
    return appendArtifact(gpa, io, dir, raw_buf[0 .. body.len + 35]);
}

test "cli tag §2: plain list excludes propagating branch names" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    _ = try checkinHash(commit.stdout);

    // §2: branch identity is stored as propagating `branch` and `sym-trunk`
    // machinery. Neither is a singleton release name.
    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli tag §5.2: plain CHECKIN list excludes branch machinery" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const hash = try gpa.dupe(u8, try checkinHash(commit.stdout));
    defer gpa.free(hash);

    // §5.1/§5.2/§6.4: the root carries active propagating `branch` and
    // `sym-trunk` rows, but neither belongs in the singleton release view.
    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", hash });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli tag regression: plain list shows only singleton symbolic release tags" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const hash = try gpa.dupe(u8, try checkinHash(commit.stdout));
    defer gpa.free(hash);

    for ([_][]const []const u8{
        &.{ "tag", "add", "release-1", hash },
        &.{ "tag", "add", "--raw", "ci-pass", hash, "yes" },
        &.{ "tag", "add", "--propagate", "moving-name", hash },
    }) |args| {
        var add = try runZit(gpa, io, tmp.dir, args);
        defer freeResult(gpa, &add);
        try expectExit(add.term, 0);
        try std.testing.expectEqualStrings("", add.stderr);
    }

    for ([_][]const []const u8{
        &.{ "tag", "list" },
        &.{ "tag", "list", hash },
    }) |args| {
        var plain = try runZit(gpa, io, tmp.dir, args);
        defer freeResult(gpa, &plain);
        try expectExit(plain.term, 0);
        try std.testing.expectEqualStrings("release-1\n", plain.stdout);
        try std.testing.expectEqualStrings("", plain.stderr);
    }

    var raw_names = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "--raw" });
    defer freeResult(gpa, &raw_names);
    try expectExit(raw_names.term, 0);
    try std.testing.expectEqualStrings(
        "branch\nci-pass\nsym-moving-name\nsym-release-1\nsym-trunk\n",
        raw_names.stdout,
    );
    try std.testing.expectEqualStrings("", raw_names.stderr);

    var raw_checkin = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "--raw", hash });
    defer freeResult(gpa, &raw_checkin);
    try expectExit(raw_checkin.term, 0);
    try std.testing.expectEqualStrings(
        "branch trunk\nci-pass yes\nsym-moving-name\nsym-release-1\nsym-trunk\n",
        raw_checkin.stdout,
    );
    try std.testing.expectEqualStrings("", raw_checkin.stderr);
}

test "cli tag: a raw tag is a property, not a name — two check-ins carry it with distinct values" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    const h1 = try gpa.dupe(u8, try checkinHash(c1.stdout));
    defer gpa.free(h1);

    try writeFile(io, tmp.dir, "a.txt", "second\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    const h2 = try gpa.dupe(u8, try checkinHash(c2.stdout));
    defer gpa.free(h2);

    // The quarry §6 verdict shape: the SAME property on both check-ins, each
    // with its own run reference. Under the old sym-only mint this was the
    // corruption — `ci-pass` became an ambiguous check-in name.
    for ([_][2][]const u8{ .{ h1, "run-41" }, .{ h2, "run-42" } }) |pair| {
        var add = try runZit(gpa, io, tmp.dir, &.{ "tag", "add", "--raw", "ci-pass", pair[0], pair[1] });
        defer freeResult(gpa, &add);
        try expectExit(add.term, 0);
        try std.testing.expectEqualStrings("", add.stderr);
    }

    // Each check-in reports its own value through the raw property view.
    for ([_][2][]const u8{ .{ h1, "ci-pass run-41" }, .{ h2, "ci-pass run-42" } }) |pair| {
        var list = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "--raw", pair[0] });
        defer freeResult(gpa, &list);
        try expectExit(list.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, list.stdout, pair[1]) != null);
    }

    // ...and the property is NOT a check-in name: resolution never sees it.
    var info = try runZit(gpa, io, tmp.dir, &.{ "info", "ci-pass" });
    defer freeResult(gpa, &info);
    try expectNonzeroExit(info.term);
    try std.testing.expectEqualStrings("", info.stdout);

    // Plain list is the name space, so a property is absent from it; --raw is
    // the read half of the write, and shows it with its prefixless name.
    var names = try runZit(gpa, io, tmp.dir, &.{ "tag", "list" });
    defer freeResult(gpa, &names);
    try expectExit(names.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, names.stdout, "ci-pass") == null);

    var raw = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "--raw" });
    defer freeResult(gpa, &raw);
    try expectExit(raw.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "ci-pass") != null);
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "sym-trunk") != null);

    // cancel retracts it from one check-in only.
    var cancel = try runZit(gpa, io, tmp.dir, &.{ "tag", "cancel", "--raw", "ci-pass", h2 });
    defer freeResult(gpa, &cancel);
    try expectExit(cancel.term, 0);

    var after2 = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "--raw", h2 });
    defer freeResult(gpa, &after2);
    try std.testing.expect(std.mem.indexOf(u8, after2.stdout, "ci-pass") == null);

    var after1 = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "--raw", h1 });
    defer freeResult(gpa, &after1);
    try std.testing.expect(std.mem.indexOf(u8, after1.stdout, "ci-pass run-41") != null);
}

test "cli tag: bare add still mints a symbolic name (the surface is backward compatible)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const hash = try gpa.dupe(u8, try checkinHash(commit.stdout));
    defer gpa.free(hash);

    var add = try runZit(gpa, io, tmp.dir, &.{ "tag", "add", "rel-1", hash });
    defer freeResult(gpa, &add);
    try expectExit(add.term, 0);

    // Still a NAME: it resolves, and it lists prefix-stripped among the names.
    var info = try runZit(gpa, io, tmp.dir, &.{ "info", "rel-1" });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, hash) != null);

    var names = try runZit(gpa, io, tmp.dir, &.{ "tag", "list" });
    defer freeResult(gpa, &names);
    try std.testing.expect(std.mem.indexOf(u8, names.stdout, "rel-1") != null);

    // And its cancel is the sym-namespace one: the name stops resolving.
    var cancel = try runZit(gpa, io, tmp.dir, &.{ "tag", "cancel", "rel-1", hash });
    defer freeResult(gpa, &cancel);
    try expectExit(cancel.term, 0);

    var gone = try runZit(gpa, io, tmp.dir, &.{ "info", "rel-1" });
    defer freeResult(gpa, &gone);
    try expectNonzeroExit(gone.term);
}

test "cli tag: refused mints — reserved prefixes, an empty VALUE, and --propagate off add" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const hash = try gpa.dupe(u8, try checkinHash(commit.stdout));
    defer gpa.free(hash);

    // fossil crosslinks wiki pages, tickets and technotes through these
    // prefixes; minting one here would forge that association.
    for ([_][]const u8{ "wiki-Home", "tkt-1234", "event-abc" }) |name| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "add", "--raw", name, hash });
        defer freeResult(gpa, &res);
        try expectNonzeroExit(res.term);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(res.stderr.len != 0);
    }

    // §4.7.16's value is `[ SP value ]`: "present but empty" has no spelling.
    var empty = try runZit(gpa, io, tmp.dir, &.{ "tag", "add", "--raw", "unminted-prop", hash, "" });
    defer freeResult(gpa, &empty);
    try expectNonzeroExit(empty.term);
    try std.testing.expectEqualStrings("", empty.stdout);

    // A flag that cannot apply is named, never accepted and ignored (exit 2).
    var prop = try runZit(gpa, io, tmp.dir, &.{ "tag", "cancel", "--propagate", "unminted-prop", hash });
    defer freeResult(gpa, &prop);
    try expectExit(prop.term, 2);
    try std.testing.expectEqualStrings("", prop.stdout);

    // None of the refusals minted anything.
    var raw = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "--raw" });
    defer freeResult(gpa, &raw);
    try expectExit(raw.term, 0);
    for ([_][]const u8{ "wiki-", "tkt-", "event-", "unminted-prop" }) |needle| {
        try std.testing.expect(std.mem.indexOf(u8, raw.stdout, needle) == null);
    }
}

test "cli tag B-tag-11: --help / -h / 'help tag' print the verbatim help (stdout, exit 0)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    for ([_][]const u8{ "--help", "-h" }) |flag| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "tag", flag });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(tag_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
    var routed = try runZit(gpa, io, tmp.dir, &.{ "help", "tag" });
    defer freeResult(gpa, &routed);
    try expectExit(routed.term, 0);
    try std.testing.expectEqualStrings(tag_help_expected, routed.stdout);
}

test "cli tag B-tag-12: no subcommand is a usage error (exit 2, stderr, empty stdout)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{"tag"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit tag: no subcommand given\ntry 'zit tag --help'\n",
        res.stderr,
    );
}

test "cli tag B-tag-13: an unknown subcommand is a usage error (exit 2, stderr, empty stdout)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Each stand-in for "not a subcommand" has in turn become one: `add` at
    // REGRESSION B6, `cancel` at the tag rework. `find` is fossil's, and is not ours.
    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "find" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit tag: unknown subcommand 'find'\ntry 'zit tag --help'\n",
        res.stderr,
    );
}

test "cli REGRESSION B6: tag add's two positionals are both required (exit 2, stderr)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var no_name = try runZit(gpa, io, tmp.dir, &.{ "tag", "add" });
    defer freeResult(gpa, &no_name);
    try expectExit(no_name.term, 2);
    try std.testing.expectEqualStrings("", no_name.stdout);
    try std.testing.expectEqualStrings(
        "zit tag: no name given\ntry 'zit tag --help'\n",
        no_name.stderr,
    );

    var no_checkin = try runZit(gpa, io, tmp.dir, &.{ "tag", "add", "v1" });
    defer freeResult(gpa, &no_checkin);
    try expectExit(no_checkin.term, 2);
    try std.testing.expectEqualStrings("", no_checkin.stdout);
    try std.testing.expectEqualStrings(
        "zit tag: no check-in given\ntry 'zit tag --help'\n",
        no_checkin.stderr,
    );
}

test "cli REGRESSION B6: tag add on an unresolvable CHECKIN mints nothing (exit 1)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try b6BuildRoot(gpa, io, tmp.dir);
    defer gpa.free(root);
    {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "tag", "add", "v1", "nosuchname" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings("zit tag: check-in 'nosuchname' not found\n", res.stderr);
    }
    // Resolution precedes the mint, so the failed run left no tag behind.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("", res.stdout);
    }
}

test "cli tag B-tag-14: an unresolvable CHECKIN fails exit 1 on stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "nosuchname" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit tag: check-in 'nosuchname' not found\n", res.stderr);
}

test "cli tag B-tag-15: 'tag' is listed in the command help and suggested for a near-miss" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var help = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &help);
    try expectExit(help.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "tag") != null);

    var typo = try runZit(gpa, io, tmp.dir, &.{"tags"});
    defer freeResult(gpa, &typo);
    try expectExit(typo.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, typo.stderr, "did you mean 'tag'?") != null);
}

test "cli tag B-tag-16 (ls): the ls alias equals list; data on stdout only" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    _ = try checkinHash(commit.stdout);

    var list = try runZit(gpa, io, tmp.dir, &.{ "tag", "list" });
    defer freeResult(gpa, &list);
    var ls = try runZit(gpa, io, tmp.dir, &.{ "tag", "ls" });
    defer freeResult(gpa, &ls);
    try expectExit(ls.term, 0);
    try std.testing.expectEqualStrings("", ls.stdout);
    try std.testing.expectEqualStrings("", ls.stderr);
    try std.testing.expectEqualStrings(list.stdout, ls.stdout);
}

test "cli tag B-tag-17: an unborn repository lists no tags (empty stdout, exit 0)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli tag §5.1: an accepted self-tag T-card surfaces in the raw view" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Anchors the raw skeleton: a valid `*sym-alpha` self-tag (§5.1 propagate
    // sign) crosslinks and surfaces with its stored prefix under --raw. The
    // rejection tests below reuse this skeleton, changing only the T-card.
    const hash = try appendRawManifestWithT(gpa, io, tmp.dir, "tagged", "T *sym-alpha *\n");
    defer gpa.free(hash);

    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "--raw", hash });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("sym-alpha\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli tag §5.1: a T-card with a bad leading char contributes no tag" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // §5.1: a leading char other than +/-/* is a fatal crosslink error, so the
    // manifest is rejected at parse and never becomes a resolvable check-in.
    const hash = try appendRawManifestWithT(gpa, io, tmp.dir, "bad", "T Qsym-bogus *\n");
    defer gpa.free(hash);

    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", hash });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not found") != null);
}

test "cli tag §4.3: an unsigned T-card name contributes no tag" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // §4.3: a tag name lacking a -, +, or * prefix is rejected at parse time.
    const hash = try appendRawManifestWithT(gpa, io, tmp.dir, "bad", "T sym-bogus *\n");
    defer gpa.free(hash);

    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", hash });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not found") != null);
}

test "cli tag §4.3: an all-hex tag name contributes no tag" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // §4.3: a tag name that is entirely hexadecimal after prefix removal is
    // rejected (it must stay distinguishable from a hash prefix).
    const hash = try appendRawManifestWithT(gpa, io, tmp.dir, "bad", "T +deadbeef *\n");
    defer gpa.free(hash);

    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", hash });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not found") != null);
}

test "cli tag §4.3: a non-ascending T-card manifest applies none of its tags" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // §4.3: a T-card sequence not strictly ascending by (name, uuid) is
    // rejected, and no crosslink effect is applied on the manifest's behalf.
    const hash = try appendRawManifestWithT(gpa, io, tmp.dir, "bad", "T *sym-zebra *\nT *sym-apple *\n");
    defer gpa.free(hash);

    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", hash });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not found") != null);
}

test "cli tag §4.2: a non-control/manifest artifact contributes no tag" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    _ = try checkinHash(commit.stdout);

    // §4.2 step 2: the tag loop runs only for control/manifest/event artifacts;
    // a cluster is another type, so it contributes no tag. Plain listing stays
    // empty because the real commit contributes branch machinery, not a
    // singleton release tag.
    const cluster = try appendCluster(gpa, io, tmp.dir);
    defer gpa.free(cluster);

    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

// ==========================================================================
// `zit info NAME` end-to-end (repo-state §3/§6/§7/§8/§9/§10 + CLI discipline).
// `info` resolves NAME through the §8.3 procedure and describes the check-in it
// names. The load-bearing property is that resolution is recency-based (§8.2),
// NOT open-leaf enumeration (§8.5/§10): a name can name a closed or non-leaf
// check-in that `leaves` would omit. These cases re-observe that divergence
// through the CLI so the wiring itself is proven.
// ==========================================================================

/// An independent transcription of `info`'s §Help block (must match
/// `main.zig`'s `info_help` byte-for-byte; drift fails the test).
const info_help_expected =
    \\usage: zit info [NAME...] [-F text|json] [--no-cache]
    \\
    \\With no NAME, describe the check-in the working tree is on (Fossil's
    \\bare 'info'); NAME is any check-in name.
    \\
    \\Resolve each NAME to a single check-in and describe it. NAME may be a
    \\full 40- or 64-character artifact ID, a unique ID prefix, a branch or
    \\tag name, a reserved form, or a timestamp — 'zit help names' states the
    \\full procedure. Resolution follows recency, not open-leaf status: a
    \\name can resolve to a closed or non-leaf check-in that 'zit leaves'
    \\would not list.
    \\
    \\Several NAMEs share one repository open; their blocks are separated by
    \\a blank line. A NAME of '-' reads names from stdin, one per line. A
    \\name that does not resolve is reported and the rest are still
    \\described; the exit status is then 1.
    \\
    \\options:
    \\  -F, --format <f>
    \\                text (default): the block above per check-in.
    \\                json: one object per NAME, in the order given, with the
    \\                keys name, hash, branch, leaf, parent (null for a root),
    \\                merge, cherrypick, backout (arrays), and comment, user,
    \\                date when an amend override is active; a name that does
    \\                not resolve is {"name":..., "error":...}.
    \\      --no-cache  bypass every derived sidecar (the cache AND the rebuild
    \\                  checkpoint) and re-derive from the full log, re-verifying
    \\                  whole-log integrity (the cache is refreshed)
    \\  -R, --repo <path>
    \\                read this repository instead of the one found from the
    \\                current directory; <path> is the directory holding '.zit'
    \\                (or the '.zit' file itself). No working tree is required.
    \\  -h, --help      show this help and exit
    \\
    \\exit status:
    \\  0  success
    \\  1  operational failure (not a repository, unreadable state, a refused
    \\     action) — the diagnostic on stderr names it
    \\  2  usage error (an unknown option or unexpected argument)
    \\
;

/// Writes a config frame naming `branch` as the repository's main-branch
/// (repo-state §6.4), opening `.zit` read-write exactly as `appendArtifactAs`
/// does — so `info` can be shown to thread the configured default branch rather
/// than assuming the literal "trunk".
fn writeDefaultBranch(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, branch: []const u8) !void {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_write });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.open(gpa, fs.storageFile(), 0);
    defer log.deinit(gpa);
    try log.putConfig(.{ .hash_policy = 0, .default_branch = branch, .project_id = "p" });
    try log.commit();
}

test "cli info §10.2/§8.5: on a fork whose newer sibling is closed, info trunk names the newer closed check-in while leaves surfaces the older open one" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // child1 (mtime 1e9, open) and child2 (mtime 2e9, the checkout). Close the
    // newer sibling child2 (§7.4 control artifact).
    const fork = try buildFork(gpa, io, tmp.dir);
    defer gpa.free(fork.child1);
    defer gpa.free(fork.child2);
    const ctl = try appendClosedControl(gpa, io, tmp.dir, "2020-01-01T00:00:00", fork.child2);
    defer gpa.free(ctl);

    // §8.2 max-mtime: `info trunk` resolves child2 — its closed state and
    // leaf-table membership are immaterial to the selection (§8.5, §9.3).
    var info = try runZit(gpa, io, tmp.dir, &.{ "info", "trunk" });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    try std.testing.expectEqualStrings("", info.stderr);
    var pbuf: [200]u8 = undefined;
    const prefix = try std.fmt.bufPrint(&pbuf, "{s}\nbranch:  trunk\nleaf:    closed\nparent:  ", .{fork.child2});
    try std.testing.expect(std.mem.startsWith(u8, info.stdout, prefix));
    try std.testing.expect(std.mem.endsWith(u8, info.stdout, "\n"));

    // §7.4/§7.7: the default `leaves` view surfaces only the older OPEN leaf
    // child1 — the divergence that proves name resolution is NOT open-leaf
    // enumeration (§8.1-not-conflated, §8.5-not-openleaf-substitute).
    var leaves = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &leaves);
    try expectExit(leaves.term, 0);
    var lbuf: [128]u8 = undefined;
    _ = &lbuf;
    try std.testing.expectEqual(@as(usize, 1), lineCount(leaves.stdout));
    try std.testing.expect(std.mem.indexOf(u8, leaves.stdout, cli.shortHash(fork.child1)) != null);
    // info named child2; leaves named child1 — the two answers genuinely differ.
    try std.testing.expect(!std.mem.eql(u8, fork.child1, fork.child2));
}

test "cli info §10.1: on a fork with the older sibling closed, info trunk and leaves agree on the open newer leaf" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const fork = try buildFork(gpa, io, tmp.dir);
    defer gpa.free(fork.child1);
    defer gpa.free(fork.child2);
    // Close the OLDER sibling child1; the newer child2 stays open.
    const ctl = try appendClosedControl(gpa, io, tmp.dir, "2020-01-01T00:00:00", fork.child1);
    defer gpa.free(ctl);

    // §8.2: `info trunk` resolves child2 (max mtime, open leaf here).
    var info = try runZit(gpa, io, tmp.dir, &.{ "info", "trunk" });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    try std.testing.expectEqualStrings("", info.stderr);
    var pbuf: [200]u8 = undefined;
    const prefix = try std.fmt.bufPrint(&pbuf, "{s}\nbranch:  trunk\nleaf:    open\nparent:  ", .{fork.child2});
    try std.testing.expect(std.mem.startsWith(u8, info.stdout, prefix));

    // §7.4: `leaves` lists only the open child2 — so here info and leaves AGREE.
    var leaves = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &leaves);
    try expectExit(leaves.term, 0);
    var lbuf: [128]u8 = undefined;
    _ = &lbuf;
    try std.testing.expectEqual(@as(usize, 1), lineCount(leaves.stdout));
    try std.testing.expect(std.mem.indexOf(u8, leaves.stdout, cli.shortHash(fork.child2)) != null);
    try std.testing.expect(std.mem.indexOf(u8, leaves.stdout, "*CURRENT*") != null);
}

test "cli info §6.1: a check-in on trunk reports branch: trunk" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const root = try checkinHash(commit.stdout);

    // §6.1: the root carries the active branch=trunk tag, so branchOf is trunk.
    // A lone root check-in is an open leaf with no parent line.
    var info = try runZit(gpa, io, tmp.dir, &.{ "info", root });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    try std.testing.expectEqualStrings("", info.stderr);
    var want: [128]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "{s}\nbranch:  trunk\nleaf:    open\n", .{root});
    try std.testing.expectEqualStrings(expected, info.stdout);
}

test "cli info §6.4/§9: a tagless check-in reports the configured main-branch, not literal trunk" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // §6.4: set main-branch to a non-default name, then add a check-in with NO
    // branch tag. Its branch is the CONFIGURED value, never the literal "trunk".
    try writeDefaultBranch(gpa, io, tmp.dir, "main");
    const id = try appendSyntheticCheckin(gpa, io, tmp.dir, "tagless", null);
    defer gpa.free(id);

    var info = try runZit(gpa, io, tmp.dir, &.{ "info", id });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    try std.testing.expectEqualStrings("", info.stderr);
    var want: [128]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "{s}\nbranch:  main\nleaf:    open\n", .{id});
    try std.testing.expectEqualStrings(expected, info.stdout);
}

test "cli info §7.1/§7.4: leaf status renders open, closed, and no" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // (a) a lone check-in is an open leaf; (b) closing it makes it a closed
    // leaf-table member (§7.4) — still a leaf, so 'closed', not 'no'.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a.txt", "root\n");
        try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
        var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
        defer freeResult(gpa, &commit);
        try expectExit(commit.term, 0);
        const root = try gpa.dupe(u8, try checkinHash(commit.stdout));
        defer gpa.free(root);

        var open_info = try runZit(gpa, io, tmp.dir, &.{ "info", root });
        defer freeResult(gpa, &open_info);
        try expectExit(open_info.term, 0);
        var want_open: [128]u8 = undefined;
        const expect_open = try std.fmt.bufPrint(&want_open, "{s}\nbranch:  trunk\nleaf:    open\n", .{root});
        try std.testing.expectEqualStrings(expect_open, open_info.stdout);

        const ctl = try appendClosedControl(gpa, io, tmp.dir, "2020-01-01T00:00:00", root);
        defer gpa.free(ctl);
        var closed_info = try runZit(gpa, io, tmp.dir, &.{ "info", root });
        defer freeResult(gpa, &closed_info);
        try expectExit(closed_info.term, 0);
        var want_closed: [128]u8 = undefined;
        const expect_closed = try std.fmt.bufPrint(&want_closed, "{s}\nbranch:  trunk\nleaf:    closed\n", .{root});
        try std.testing.expectEqualStrings(expect_closed, closed_info.stdout);
    }

    // (c) a check-in with a same-branch child is not a leaf at all: 'no'.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a.txt", "root\n");
        try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
        var c_root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u", .epoch = "1000000000" }, &.{ "commit", "-m", "root" });
        defer freeResult(gpa, &c_root);
        try expectExit(c_root.term, 0);
        const root = try gpa.dupe(u8, try checkinHash(c_root.stdout));
        defer gpa.free(root);
        try writeFile(io, tmp.dir, "a.txt", "edit\n");
        var c_child = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u", .epoch = "2000000000" }, &.{ "commit", "-m", "child" });
        defer freeResult(gpa, &c_child);
        try expectExit(c_child.term, 0);
        const child = try gpa.dupe(u8, try checkinHash(c_child.stdout));
        defer gpa.free(child);

        var root_info = try runZit(gpa, io, tmp.dir, &.{ "info", root });
        defer freeResult(gpa, &root_info);
        try expectExit(root_info.term, 0);
        var want_no: [128]u8 = undefined;
        const expect_no = try std.fmt.bufPrint(&want_no, "{s}\nbranch:  trunk\nleaf:    no\n", .{root});
        try std.testing.expectEqualStrings(expect_no, root_info.stdout);
    }
}

test "cli info §3.2/§3.3: renders the primary parent and each merge parent; a root has none" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Two independent roots and a merge child whose P card lists p1 then p2.
    const p1 = try appendSyntheticCheckin(gpa, io, tmp.dir, "p1", null);
    defer gpa.free(p1);
    const p2 = try appendSyntheticCheckin(gpa, io, tmp.dir, "p2", null);
    defer gpa.free(p2);
    var parents_buf: [129]u8 = undefined;
    const parents = try std.fmt.bufPrint(&parents_buf, "{s} {s}", .{ p1, p2 });
    const merge = try appendSyntheticCheckin(gpa, io, tmp.dir, "merge", parents);
    defer gpa.free(merge);

    // §3.2: p1 (first P-card token) is the primary parent; §3.3: p2 is a merge
    // parent, shown on its own line.
    var mi = try runZit(gpa, io, tmp.dir, &.{ "info", merge });
    defer freeResult(gpa, &mi);
    try expectExit(mi.term, 0);
    try std.testing.expectEqualStrings("", mi.stderr);
    var want_merge: [320]u8 = undefined;
    const expect_merge = try std.fmt.bufPrint(
        &want_merge,
        "{s}\nbranch:  trunk\nleaf:    open\nparent:  {s}\nmerge:   {s}\n",
        .{ merge, p1, p2 },
    );
    try std.testing.expectEqualStrings(expect_merge, mi.stdout);

    // A root has no parent line (and, having a same-branch child, is not a leaf).
    var pi = try runZit(gpa, io, tmp.dir, &.{ "info", p1 });
    defer freeResult(gpa, &pi);
    try expectExit(pi.term, 0);
    var want_root: [128]u8 = undefined;
    const expect_root = try std.fmt.bufPrint(&want_root, "{s}\nbranch:  trunk\nleaf:    no\n", .{p1});
    try std.testing.expectEqualStrings(expect_root, pi.stdout);
}

test "cli info §8.1: help lists info and did-you-mean suggests it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // The top-level command list names the verb (discoverability).
    var help = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &help);
    try expectExit(help.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "info") != null);

    // A single-substitution typo maps uniquely to 'info' (distance 1; every
    // other verb is farther), so did-you-mean suggests it.
    var typo = try runZit(gpa, io, tmp.dir, &.{"infp"});
    defer freeResult(gpa, &typo);
    try expectExit(typo.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, typo.stderr, "did you mean 'info'?") != null);
}

test "cli info §8.1/§8.5: -h/--help and 'help info' print the verbatim help on stdout, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    for ([_][]const u8{ "--help", "-h" }) |flag| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "info", flag });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(info_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
    var routed = try runZit(gpa, io, tmp.dir, &.{ "help", "info" });
    defer freeResult(gpa, &routed);
    try expectExit(routed.term, 0);
    try std.testing.expectEqualStrings(info_help_expected, routed.stdout);
}

test "cli info §8.1: a bad -F value and an unknown option each exit 2 to stderr" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Missing NAME — detected before any repo discovery, so no repo is needed.
    // A bare `info` describes the current check-in (Fossil's shape); the
    // usage errors are an unknown option and an extra positional.
    var no_name = try runZit(gpa, io, tmp.dir, &.{ "info", "--bogus" });
    defer freeResult(gpa, &no_name);
    try expectExit(no_name.term, 2);
    try std.testing.expectEqualStrings("", no_name.stdout);
    try std.testing.expectEqualStrings(
        "zit info: unknown option '--bogus'\ntry 'zit info --help'\n",
        no_name.stderr,
    );

    // A format that is neither text nor json. A second positional
    // is not an error any more: it is another name to describe.
    var extra = try runZit(gpa, io, tmp.dir, &.{ "info", "-F", "yaml", "a" });
    defer freeResult(gpa, &extra);
    try expectExit(extra.term, 2);
    try std.testing.expectEqualStrings("", extra.stdout);
    try std.testing.expectEqualStrings(
        "zit info: unknown option 'yaml'\ntry 'zit info --help'\n",
        extra.stderr,
    );

    // An unknown option.
    var opt = try runZit(gpa, io, tmp.dir, &.{ "info", "--bogus" });
    defer freeResult(gpa, &opt);
    try expectExit(opt.term, 2);
    try std.testing.expectEqualStrings("", opt.stdout);
    try std.testing.expectEqualStrings(
        "zit info: unknown option '--bogus'\ntry 'zit info --help'\n",
        opt.stderr,
    );
}

test "cli info §8.3: an unresolved name exits 1 with the diagnostic on stderr only" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    // A name no check-in carries is a resolution failure (§8.3 NotFound): exit 1,
    // diagnostic on stderr, nothing on stdout (cli-stream-separation).
    var res = try runZit(gpa, io, tmp.dir, &.{ "info", "nosuch" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("zit info: check-in 'nosuch' not found\n", res.stderr);
}

// ==========================================================================
// `zit ls` end-to-end. Lists a check-in's effective file set, one
// `<blob-id> <filename>` line per file in canonical §4.5.3 order. The blob id
// oracle is the artifact §7.2 sha3-256 of the file content, computed here from
// the RFC rule — never captured from zit or Fossil.
// ==========================================================================

/// Appends one `ls` line — `<sha3-256 hex> SP <name> LF` — computed from the
/// §7.2 hash-over-content rule, so the e2e expectation is RFC-derived.
fn appendLsLine(gpa: std.mem.Allocator, list: *std.ArrayList(u8), name: []const u8, content: []const u8) !void {
    // `ls` lists names, as Fossil's does; the hash rides under
    // --long. `content` is kept by the callers for the --long checks.
    _ = content;
    try list.appendSlice(gpa, name);
    try list.append(gpa, '\n');
}

test "cli ls §8.2: prints each effective file's hash and name for a resolved check-in" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "alpha\n");
    try writeFile(io, tmp.dir, "b.txt", "beta\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const hash = try checkinHash(commit.stdout);

    // The effective set is both files, in canonical filename order (a.txt < b.txt),
    // each `<sha3-256(content)> <name>`.
    var expected: std.ArrayList(u8) = .empty;
    defer expected.deinit(gpa);
    try appendLsLine(gpa, &expected, "a.txt", "alpha\n");
    try appendLsLine(gpa, &expected, "b.txt", "beta\n");

    var res = try runZit(gpa, io, tmp.dir, &.{ "ls", hash });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(expected.items, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli ls: no VERSION lists the current checkout; unborn checkout is a failure" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // (a) After a commit the checkout names the new check-in; bare `zit ls` lists
    // exactly that check-in's files.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "only.txt", "content\n");
        try addManaged(gpa, io, tmp.dir, &.{"only.txt"});
        var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
        defer freeResult(gpa, &commit);
        try expectExit(commit.term, 0);

        var expected: std.ArrayList(u8) = .empty;
        defer expected.deinit(gpa);
        try appendLsLine(gpa, &expected, "only.txt", "content\n");

        var res = try runZit(gpa, io, tmp.dir, &.{"ls"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(expected.items, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }

    // (b) A fresh repository's checkout is unborn: bare `zit ls` has nothing to
    // list — exit 1, diagnostic on stderr, empty stdout.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);

        var res = try runZit(gpa, io, tmp.dir, &.{"ls"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "unborn") != null);
    }
}

test "cli ls §8.3: an unknown version is a resolve failure (exit 1); not-a-repo is a failure" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // (a) In a real repo, a name no check-in carries is a §8.3 resolution failure:
    // exit 1, diagnostic on stderr, empty stdout.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a.txt", "root\n");
        try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
        var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
        defer freeResult(gpa, &commit);
        try expectExit(commit.term, 0);

        var res = try runZit(gpa, io, tmp.dir, &.{ "ls", "nosuch" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings("zit ls: check-in 'nosuch' not found\n", res.stderr);
    }

    // (b) Outside any repository, `zit ls` fails to discover a repo: exit 1,
    // not-a-repo diagnostic on stderr, empty stdout.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try runZit(gpa, io, tmp.dir, &.{"ls"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no zit checkout here") != null);
    }
}

// ==========================================================================
// `zit cat` end-to-end. Emits one file's uncompressed content at a resolved
// check-in, byte-for-byte on stdout. The content oracle is the exact bytes the
// test wrote before committing — never captured from zit or Fossil. Whether a
// blob is stored full or as a delta is a storage detail cat must see through:
// its output is always the uncompressed bytes, hash-verified against the F card
// (artifact §7.2).
// ==========================================================================

test "cli cat §7.2/delta §4: emits the reconstructed uncompressed bytes for a file stored as a delta chain" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // A large file whose second version differs in one byte: commit stores the
    // child's blob as a profitable delta against the parent's blob for the same
    // path (§4.1/§7.2 storage), so one of the two versions is reconstructed by
    // walking a delta frame — cat must emit the exact uncompressed bytes either
    // way, and blob reconstruction re-verifies the §7.2 hash.
    var v1: std.ArrayList(u8) = .empty;
    defer v1.deinit(gpa);
    for (0..300) |_| try v1.appendSlice(gpa, "the quick brown fox jumps over\n");
    var v2: std.ArrayList(u8) = .empty;
    defer v2.deinit(gpa);
    try v2.appendSlice(gpa, v1.items);
    v2.items[1500] = 'X'; // a single-byte change deep in the middle

    try writeFile(io, tmp.dir, "big.txt", v1.items);
    try addManaged(gpa, io, tmp.dir, &.{"big.txt"});
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "v1" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);
    const root_hash = try gpa.dupe(u8, try checkinHash(root.stdout));
    defer gpa.free(root_hash);

    try writeFile(io, tmp.dir, "big.txt", v2.items);
    var child = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "v2" });
    defer freeResult(gpa, &child);
    try expectExit(child.term, 0);

    // Current checkout (child) → v2; the explicit root check-in → v1. Both
    // reconstruct their stored blob (one of them across the delta chain).
    var cur = try runZit(gpa, io, tmp.dir, &.{ "cat", "big.txt" });
    defer freeResult(gpa, &cur);
    try expectExit(cur.term, 0);
    try std.testing.expectEqualStrings(v2.items, cur.stdout);
    try std.testing.expectEqualStrings("", cur.stderr);

    var old = try runZit(gpa, io, tmp.dir, &.{ "cat", "big.txt", root_hash });
    defer freeResult(gpa, &old);
    try expectExit(old.term, 0);
    try std.testing.expectEqualStrings(v1.items, old.stdout);
    try std.testing.expectEqualStrings("", old.stderr);
}

test "cli cat §8.2: resolves FILE within a check-in's effective set — the unchanged and the changed file each read correctly across two commits" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Root {a:"a0", b:"b0"}; child changes only a → "a1". At the child check-in
    // the effective set (§8.2) is {a:"a1", b:"b0"}: cat of the untouched file
    // yields its original bytes, cat of the changed file yields the new bytes.
    try writeFile(io, tmp.dir, "a.txt", "a0\n");
    try writeFile(io, tmp.dir, "b.txt", "b0\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);

    try writeFile(io, tmp.dir, "a.txt", "a1\n");
    var child = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "edit a" });
    defer freeResult(gpa, &child);
    try expectExit(child.term, 0);

    var ca = try runZit(gpa, io, tmp.dir, &.{ "cat", "a.txt" });
    defer freeResult(gpa, &ca);
    try expectExit(ca.term, 0);
    try std.testing.expectEqualStrings("a1\n", ca.stdout); // the changed file

    var cb = try runZit(gpa, io, tmp.dir, &.{ "cat", "b.txt" });
    defer freeResult(gpa, &cb);
    try expectExit(cb.term, 0);
    try std.testing.expectEqualStrings("b0\n", cb.stdout); // carried forward unchanged
    try std.testing.expectEqualStrings("", cb.stderr);
}

test "cli cat §8.2: a file removed at a check-in is absent from its effective set — not found, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Root tracks a and b; the child stops tracking b (`zit rm`) and commits, so
    // b is not in the child's effective set. cat of b at the child reports
    // not-found on stderr, exit 1, empty stdout (cli-stream-separation).
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try writeFile(io, tmp.dir, "b.txt", "b\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);

    var rm = try runZit(gpa, io, tmp.dir, &.{ "rm", "b.txt" });
    defer freeResult(gpa, &rm);
    try expectExit(rm.term, 0);
    var child = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "drop b", "--user", "e2e" });
    defer freeResult(gpa, &child);
    try expectExit(child.term, 0);

    var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "b.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not in the check-in") != null);

    // a is still tracked, so it reads back its content.
    var ca = try runZit(gpa, io, tmp.dir, &.{ "cat", "a.txt" });
    defer freeResult(gpa, &ca);
    try expectExit(ca.term, 0);
    try std.testing.expectEqualStrings("a\n", ca.stdout);
}

test "cli cat §7.2: binary content with embedded NUL is emitted verbatim — no truncation, no added newline" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Every byte value 0..255 twice, so a NUL sits well before the end and the
    // content has no trailing LF. cat MUST pass all bytes through unchanged — the
    // content path is []const u8 (NUL-legal), and cat adds no framing.
    var content: [512]u8 = undefined;
    for (&content, 0..) |*b, i| b.* = @intCast(i % 256);
    try writeFile(io, tmp.dir, "blob.bin", &content);
    try addManaged(gpa, io, tmp.dir, &.{"blob.bin"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "binary" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "blob.bin" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualSlices(u8, &content, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli cat: no FILE is a usage error (exit 2); an unborn checkout and an unresolvable VERSION each fail (exit 1)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // (a) `zit cat` with no FILE is a usage error: exit 2, diagnostic on stderr,
    // empty stdout.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        var res = try runZit(gpa, io, tmp.dir, &.{"cat"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no file given") != null);
    }

    // (b) A fresh repository's checkout is unborn (§7.5): bare `zit cat FILE` has
    // no check-in to read — exit 1, diagnostic on stderr, empty stdout.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "a.txt" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "unborn") != null);
    }

    // (c) A name no check-in carries is a §8.3 resolution failure (NotFound):
    // exit 1, the resolve diagnostic on stderr, empty stdout.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a.txt", "root\n");
        try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
        var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
        defer freeResult(gpa, &commit);
        try expectExit(commit.term, 0);

        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "a.txt", "nosuch" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings("zit cat: check-in 'nosuch' not found\n", res.stderr);
    }
}

// ==========================================================================
// `zit artifact` end-to-end (contracts/artifact.md B-artifact-1..14). zit's
// plumbing window: a read-only describe (`whatis`) / dump (`--raw`) of ONE
// stored artifact addressed by its hash — every artifact kind, file blobs
// included. Card interpretation flows through the ONE artifact parser; the
// oracles here are RFC-derived (sha3-256 of content for a blob name; the
// stored manifest bytes read back through the library), never fossil.
// ==========================================================================

const artifact_help_expected =
    \\usage: zit artifact HASH [--raw]
    \\
    \\Describe or dump a single stored artifact addressed by its hash. HASH is a full
    \\or abbreviated (>=4 character) lowercase hexadecimal artifact ID, resolved
    \\against every stored artifact — file blobs included. Branch and tag names are
    \\not accepted; use 'zit info' for those.
    \\
    \\Without --raw, print a summary: the full hash, the artifact's size, its type,
    \\and — for a structural artifact — one line per card and whether its Z
    \\self-checksum verifies. A blob reports its size and whether it is binary.
    \\
    \\With --raw, write the artifact's exact stored bytes to standard output,
    \\verbatim — no trailing newline is added and binary content passes through
    \\unchanged. Hashing those bytes reproduces HASH.
    \\
    \\options:
    \\      --raw       write the exact stored bytes instead of a summary
    \\  -R, --repo <path>
    \\                read this repository instead of the one found from the
    \\                current directory; <path> is the directory holding '.zit'
    \\                (or the '.zit' file itself). No working tree is required.
    \\  -h, --help      show this help and exit
    \\
    \\exit status:
    \\  0  success
    \\  1  operational failure (not a repository, unreadable state, a refused
    \\     action) — the diagnostic on stderr names it
    \\  2  usage error (an unknown option or unexpected argument)
    \\
;

/// Lowercase-hex sha3-256 of `content` — the §7.2 artifact name of stored
/// content, copied onto `gpa` (caller frees). The RFC-derived blob-hash oracle.
fn sha3HexAlloc(gpa: std.mem.Allocator, content: []const u8) ![]u8 {
    var digest: [32]u8 = undefined;
    std.crypto.hash.sha3.Sha3_256.hash(content, &digest, .{});
    const hex = "0123456789abcdef";
    const out = try gpa.alloc(u8, 64);
    for (digest, 0..) |b, i| {
        out[i * 2] = hex[b >> 4];
        out[i * 2 + 1] = hex[b & 0x0f];
    }
    return out;
}

/// Appends one artifact frame keyed by `digest` (its raw 32-byte name) holding
/// `content`, straight into the repo's `.zit` through the library — the only
/// way to seed an artifact the CLI itself would never mint: a Z-mismatched
/// structural artifact (B-artifact-5) and a prefix-colliding pair
/// (B-artifact-7). A read-write reopen + append + commit, exactly like a real
/// writer. The derive tolerates a non-parseable/Z-mismatched frame as an opaque
/// blob (repoview.linkOne "crosslink whatever parses"), so `repo.open` succeeds.
fn injectFrame(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, digest: [32]u8, content: []const u8) !void {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_write });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.open(gpa, fs.storageFile(), 0);
    defer log.deinit(gpa);
    _ = try log.put(&digest, content);
    try log.commit();
}

test "cli artifact B-artifact-1: a manifest summary carries the hash, type, size, and one line per card" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "alpha\n");
    try writeFile(io, tmp.dir, "b.txt", "beta\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const ck = try gpa.dupe(u8, try checkinHash(commit.stdout));
    defer gpa.free(ck);

    // The stored-manifest byte length is the size oracle, read back through the
    // library (never a hand-count).
    var mc = try parseCheckin(gpa, io, tmp.dir, ck);
    defer mc.deinit();
    const size = mc.bytes.len;

    var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", ck });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr); // stream discipline

    // Header line is the full hash; the type and size lines are exact.
    var lines = std.mem.splitScalar(u8, res.stdout, '\n');
    try std.testing.expectEqualStrings(ck, lines.first());
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "type: manifest\n") != null);
    const size_line = try std.fmt.allocPrint(gpa, "size: {d} bytes\n", .{size});
    defer gpa.free(size_line);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, size_line) != null);

    // One F line per file, each `F <name> <sha3(content)>` (§4.7.6 decoded via
    // the ONE parser). The blob hashes are RFC-derived here.
    const a_hash = try sha3HexAlloc(gpa, "alpha\n");
    defer gpa.free(a_hash);
    const b_hash = try sha3HexAlloc(gpa, "beta\n");
    defer gpa.free(b_hash);
    const a_line = try std.fmt.allocPrint(gpa, "\nF a.txt {s}\n", .{a_hash});
    defer gpa.free(a_line);
    const b_line = try std.fmt.allocPrint(gpa, "\nF b.txt {s}\n", .{b_hash});
    defer gpa.free(b_line);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, a_line) != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, b_line) != null);
    // The comment, user, and the verified Z verdict are shown too.
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "\nC root\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "\nU u\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "\nZ: verified\n") != null);
}

test "cli artifact B-artifact-2: a control artifact reports type control and its T cards' sign, name, target" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    // `amend --branch` mints a control artifact whose T cards move the check-in
    // onto a new branch (§4.7.16): a `*sym-feature` add and a `-sym-trunk`
    // cancel, both targeting the root check-in.
    var am = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "amend", root_hash, "--branch", "feature" });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 0);
    const control_hash = try gpa.dupe(u8, try amendHash(am.stdout));
    defer gpa.free(control_hash);

    var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", control_hash });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr);

    var lines = std.mem.splitScalar(u8, res.stdout, '\n');
    try std.testing.expectEqualStrings(control_hash, lines.first());
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "type: control\n") != null);
    // T <sign><name> <target>: sign+name glued, self/hash target rendered.
    const sym_add = try std.fmt.allocPrint(gpa, "\nT *sym-feature {s}\n", .{root_hash});
    defer gpa.free(sym_add);
    const sym_cancel = try std.fmt.allocPrint(gpa, "\nT -sym-trunk {s}\n", .{root_hash});
    defer gpa.free(sym_cancel);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, sym_add) != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, sym_cancel) != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "\nZ: verified\n") != null);
}

test "cli artifact B-artifact-3: a file blob reports type file, its size, and its binary/text verdict" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // A text file and a NUL-bearing binary file. Each blob's name is the
    // §7.2 sha3 of its content.
    const text = "plain text content\n";
    var binary: [64]u8 = undefined;
    for (&binary, 0..) |*b, i| b.* = @intCast(i % 256); // includes NUL at 0, no trailing LF
    try writeFile(io, tmp.dir, "t.txt", text);
    try writeFile(io, tmp.dir, "b.bin", &binary);
    try addManaged(gpa, io, tmp.dir, &.{ "t.txt", "b.bin" });
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    const text_hash = try sha3HexAlloc(gpa, text);
    defer gpa.free(text_hash);
    const bin_hash = try sha3HexAlloc(gpa, &binary);
    defer gpa.free(bin_hash);

    // Text blob: type file, exact size, binary: no. No card listing, no Z line.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", text_hash });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("", res.stderr);
        const expected = try std.fmt.allocPrint(gpa, "{s}\ntype: file\nsize: {d} bytes\nbinary: no\n", .{ text_hash, text.len });
        defer gpa.free(expected);
        try std.testing.expectEqualStrings(expected, res.stdout);
    }
    // Binary blob: binary: yes, exact size.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", bin_hash });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        const expected = try std.fmt.allocPrint(gpa, "{s}\ntype: file\nsize: {d} bytes\nbinary: yes\n", .{ bin_hash, binary.len });
        defer gpa.free(expected);
        try std.testing.expectEqualStrings(expected, res.stdout);
    }
}

test "cli artifact B-artifact-4: --raw is byte-exact and its bytes hash back to HASH for manifest, control, and blob" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir); // one manifest + one blob "v1\n"
    defer gpa.free(root_hash);
    var am = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "amend", root_hash, "--branch", "feature" });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 0);
    const control_hash = try gpa.dupe(u8, try amendHash(am.stdout));
    defer gpa.free(control_hash);
    const blob_hash = try sha3HexAlloc(gpa, "v1\n");
    defer gpa.free(blob_hash);

    // For each kind: --raw emits the exact stored bytes, and sha3-256 of those
    // bytes reproduces HASH (§3.4 round-trip). The blob additionally equals its
    // source content verbatim — no trailing newline added.
    for ([_][]const u8{ root_hash, control_hash, blob_hash }) |h| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", h, "--raw" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("", res.stderr);
        const round = try sha3HexAlloc(gpa, res.stdout);
        defer gpa.free(round);
        try std.testing.expectEqualStrings(h, round); // hashing the emitted bytes reproduces HASH
    }
    var blob = try runZit(gpa, io, tmp.dir, &.{ "artifact", blob_hash, "--raw" });
    defer freeResult(gpa, &blob);
    try expectExit(blob.term, 0);
    try std.testing.expectEqualStrings("v1\n", blob.stdout); // verbatim, no added framing
}

test "cli artifact B-artifact-5: a Z-mismatched structural artifact reports the mismatch, exit 0 (window, not gate)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Build a structural manifest (C ⇒ §5.3 MANIFEST fallback) and seal it with
    // a DELIBERATELY WRONG §6.2 Z digest, then inject it under its real §7.2
    // name so `blob` accepts it. `parse` (gate 4) would reject it — the derive
    // therefore skips it as a blob — but the summary's `parseWindow` assembles
    // it and REPORTS the mismatch.
    const body = "C zmismatch\nD 2026-01-01T00:00:00\n";
    var zbuf: [32]u8 = undefined;
    const correct = artifact.selfChecksum(body, &zbuf);
    var full: std.ArrayList(u8) = .empty;
    defer full.deinit(gpa);
    try full.appendSlice(gpa, body);
    try full.appendSlice(gpa, "Z ");
    var bad: [32]u8 = undefined;
    @memcpy(&bad, correct);
    bad[0] = if (correct[0] == 'a') 'b' else 'a'; // corrupt one hex digit ⇒ mismatch
    try full.appendSlice(gpa, &bad);
    try full.append(gpa, '\n');

    var raw_digest: [32]u8 = undefined;
    std.crypto.hash.sha3.Sha3_256.hash(full.items, &raw_digest, .{});
    try injectFrame(gpa, io, tmp.dir, raw_digest, full.items);
    const name = try sha3HexAlloc(gpa, full.items);
    defer gpa.free(name);

    var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", name });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0); // mismatch is reported, not enforced
    try std.testing.expectEqualStrings("", res.stderr);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "type: manifest\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "\nZ: checksum mismatch\n") != null);
}

test "cli artifact B-artifact-6: an uppercase abbreviated prefix resolves; the header carries the full lowercase hash" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "only.txt", "unique blob content\n");
    try addManaged(gpa, io, tmp.dir, &.{"only.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    const blob_hash = try sha3HexAlloc(gpa, "unique blob content\n");
    defer gpa.free(blob_hash);
    // A 12-char UPPERCASE prefix — case-insensitive resolution (§3.6).
    var upper: [12]u8 = undefined;
    for (blob_hash[0..12], 0..) |c, i| upper[i] = std.ascii.toUpper(c);

    var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", &upper });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    var lines = std.mem.splitScalar(u8, res.stdout, '\n');
    try std.testing.expectEqualStrings(blob_hash, lines.first()); // full LOWERCASE hash, however addressed
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "type: file\n") != null);
}

test "cli artifact B-artifact-7: an ambiguous prefix is refused with exit 1, diagnostic on stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Two injected artifacts whose names both begin "abcd" (§8.3 item 8 makes a
    // multi-match a refusal, never a silent pick). The prefix names both.
    const d1 = [_]u8{ 0xab, 0xcd, 0x00 } ++ [_]u8{0} ** 29;
    const d2 = [_]u8{ 0xab, 0xcd, 0x11 } ++ [_]u8{0} ** 29;
    try injectFrame(gpa, io, tmp.dir, d1, "one");
    try injectFrame(gpa, io, tmp.dir, d2, "two");

    var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", "abcd" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "ambiguous") != null);
}

test "cli artifact B-artifact-8: an unknown hash and a syntactically invalid one are one not-found family (exit 1, empty stdout)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "root\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    // A well-formed but unknown hex hash, a non-hex token, a too-short token,
    // and a too-long token all refuse identically — no distinct error surface.
    for ([_][]const u8{ "deadbeefdeadbeef", "zznothex", "abc", "a" ** 65 }) |bad| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", bad });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no artifact matches") != null);
    }
}

test "cli artifact B-artifact-9: no invocation mutates .zit, .zit-checkout, or the working tree" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "content\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    const ck = try gpa.dupe(u8, try checkinHash(commit.stdout));
    defer gpa.free(ck);
    const blob_hash = try sha3HexAlloc(gpa, "content\n");
    defer gpa.free(blob_hash);

    const zit_before = try tmp.dir.readFileAlloc(io, ".zit", gpa, .limited(1 << 20));
    defer gpa.free(zit_before);
    const ck_before = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(ck_before);
    const tree_before = try readFile(gpa, io, tmp.dir, "a.txt");
    defer gpa.free(tree_before);

    // A summary, a raw dump, and two refusals — none may touch stored state.
    inline for (.{
        &[_][]const u8{ "artifact", ck },
        &[_][]const u8{ "artifact", ck, "--raw" },
        &[_][]const u8{ "artifact", blob_hash },
        &[_][]const u8{ "artifact", "deadbeef" },
    }) |tail| {
        var res = try runZit(gpa, io, tmp.dir, tail);
        freeResult(gpa, &res);
    }

    const zit_after = try tmp.dir.readFileAlloc(io, ".zit", gpa, .limited(1 << 20));
    defer gpa.free(zit_after);
    const ck_after = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(ck_after);
    const tree_after = try readFile(gpa, io, tmp.dir, "a.txt");
    defer gpa.free(tree_after);
    try std.testing.expectEqualSlices(u8, zit_before, zit_after);
    try std.testing.expectEqualSlices(u8, ck_before, ck_after);
    try std.testing.expectEqualSlices(u8, tree_before, tree_after);
}

test "cli artifact B-artifact-10: -h/--help and 'help artifact' print the verbatim help on stdout, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    for ([_][]const u8{ "--help", "-h" }) |flag| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", flag });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(artifact_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
    var routed = try runZit(gpa, io, tmp.dir, &.{ "help", "artifact" });
    defer freeResult(gpa, &routed);
    try expectExit(routed.term, 0);
    try std.testing.expectEqualStrings(artifact_help_expected, routed.stdout);
}

test "cli artifact B-artifact-11: missing HASH, an unknown option, and a second positional each exit 2 to stderr" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const cases = [_]struct { tail: []const []const u8, needle: []const u8 }{
        .{ .tail = &.{"artifact"}, .needle = "zit artifact: no hash given\n" },
        .{ .tail = &.{ "artifact", "--bogus" }, .needle = "zit artifact: unknown option '--bogus'\n" },
        .{ .tail = &.{ "artifact", "abcd", "efgh" }, .needle = "zit artifact: unexpected argument 'efgh'\n" },
    };
    for (cases) |c| {
        var res = try runZit(gpa, io, tmp.dir, c.tail);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, c.needle) != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "try 'zit artifact --help'\n") != null);
    }
}

test "cli artifact B-artifact-12/nongoal: outside a repo exits 1 with not-a-repo; a symbolic name is a not-found refusal" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // (a) Outside any repository: the standard not-a-repo diagnostic, exit 1,
    // empty stdout (openRepo runs before hash resolution).
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", "deadbeef" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no zit checkout here") != null);
    }

    // (b) A symbolic name (a branch) is NOT an artifact address — `zit info`
    // owns names — so `zit artifact trunk` is the not-found refusal, exit 1.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "a.txt", "root\n");
        try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
        var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
        defer freeResult(gpa, &commit);
        try expectExit(commit.term, 0);

        var res = try runZit(gpa, io, tmp.dir, &.{ "artifact", "trunk" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no artifact matches 'trunk'") != null);
    }
}

test "cli artifact B-artifact-14: help lists artifact and did-you-mean suggests it; there is no whatis sibling verb" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // The top-level command list names the verb (discoverability).
    var help = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &help);
    try expectExit(help.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "artifact") != null);

    // `whatis` is not a zit verb — the summary IS the whatis mode (single
    // spelling). It resolves to unknown-command, exit 2.
    var whatis = try runZit(gpa, io, tmp.dir, &.{ "whatis", "deadbeef" });
    defer freeResult(gpa, &whatis);
    try expectExit(whatis.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, whatis.stderr, "unknown command 'whatis'") != null);
}

// --- clone interop (sync §3.7/§4/§8) --------------------------------------
//
// The milestone e2e: a real `zit clone` client converging against a real
// `zit serve` server over loopback HTTP. Repo A is populated with a committed
// file; A is served; clone builds a fresh repo B; B's on-disk artifact set is
// read back through the library and must equal A's — proving the client store
// converged to the server's content over the wire, not from any fossil oracle.

const net = std.Io.net;

/// Allocates a fresh loopback port for a child `zit serve` by binding
/// `127.0.0.1:0`, reading back the kernel-assigned port, then closing the
/// reservation listener so the child can bind it. Every server-interop scenario
/// draws its port here instead of a hardcoded constant, so overlapping runs of
/// this suite (or unrelated host load) never collide on a fixed port — the
/// defect behind the flaky push, where a concurrent run's `zit serve` failed to bind
/// a shared port and the client's push crossed to the wrong server.
///
/// The reservation listener closes before the child binds, so a close→child-bind
/// race window remains; it is unavoidable because a child server cannot inherit
/// the listener, and it is orders of magnitude narrower than a fixed-port
/// collision. This mirrors `freshLoopbackPort` in src/smoke.zig (the 0.1.1 work
/// that moved the Fossil smoke tests off fixed ports for the same reason);
/// duplicated rather than shared because e2e and smoke are independent test
/// binaries that already each carry their own `waitForListen`.
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;
}

/// Spawns `zit serve --port <port>` as a detached child whose cwd is `dir`
/// (so `discoverRoot` finds `dir/.zit`). stdout/stderr are discarded — the
/// test drives it purely over the socket. Caller `kill`s the returned child.
fn spawnServe(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, port: u16) !std.process.Child {
    return spawnServeFronted(gpa, io, dir, port, false);
}

/// `spawnServe` with `--fronted` when asked.
fn spawnServeFronted(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, port: u16, fronted: bool) !std.process.Child {
    var env = std.process.Environ.Map.init(gpa);
    defer env.deinit();
    try env.put("TZ", "UTC");
    try env.put("HOME", "/zit-e2e-isolated-home-does-not-exist");
    try env.put("PATH", "/usr/bin:/bin");

    const cwd_abs = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd_abs);
    const abs_exe = try std.fs.path.resolve(gpa, &.{ cwd_abs, zit_exe });
    defer gpa.free(abs_exe);
    const port_str = try std.fmt.allocPrint(gpa, "{d}", .{port});
    defer gpa.free(port_str);

    // `spawn` builds the child image during this call; once it returns the
    // parent no longer references the argv/env buffers, so the defers above
    // free them safely after the child exists.
    const argv_plain = [_][]const u8{ abs_exe, "serve", "--port", port_str };
    const argv_fronted = [_][]const u8{ abs_exe, "serve", "--port", port_str, "--fronted" };
    return std.process.spawn(io, .{
        .argv = if (fronted) &argv_fronted else &argv_plain,
        .cwd = .{ .dir = dir },
        .environ_map = &env,
        .stdout = .ignore,
        .stderr = .ignore,
    });
}

/// Bounded wait for `zit serve` to bind: retry-connect to the loopback port
/// until a connection is accepted or a wall-clock deadline elapses (never a
/// fixed sleep). A successful TCP connect proves the listener is up (connect
/// completes on the listen backlog, before `accept`). The deadline guarantees
/// termination if the server never binds (e.g. the port is already in use).
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;
    }
}

/// Reads every content-bearing, non-config artifact digest from `dir/.zit`
/// through the library (LogStore keys frames by the raw binary digest), sorted
/// so two repositories' sets compare directly. Caller frees via `freeDigests`.
fn collectArtifactDigests(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![][]u8 {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_only });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);

    const rids = try log.ridsInPersistedOrder(gpa); // non-phantom, persisted order
    defer gpa.free(rids);
    var list: std.ArrayListUnmanaged([]u8) = .empty;
    errdefer {
        for (list.items) |h| gpa.free(h);
        list.deinit(gpa);
    }
    for (rids) |rid| {
        if (log.kindOf(rid) == .config) continue; // config is repo metadata, not synced
        try list.append(gpa, try gpa.dupe(u8, log.hashOf(rid)));
    }
    const slice = try list.toOwnedSlice(gpa);
    std.mem.sort([]u8, slice, {}, struct {
        fn lt(_: void, a: []u8, b: []u8) bool {
            return std.mem.order(u8, a, b) == .lt;
        }
    }.lt);
    return slice;
}

fn freeDigests(gpa: std.mem.Allocator, digests: [][]u8) void {
    for (digests) |h| gpa.free(h);
    gpa.free(digests);
}

/// Reads a repository's stored sync project code (§9.1) from its `.zit`
/// config frame. Caller owns the returned bytes.
fn readProjectCode(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]u8 {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_only });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);
    var oc = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult;
    defer oc.deinit(gpa);
    return gpa.dupe(u8, oc.config.project_code);
}

/// Asserts two artifact-digest sets are identical (same count, same members).
/// Both inputs are sorted by `collectArtifactDigests`, so an elementwise walk
/// is a set-equality proof.
fn expectSameDigestSet(a: []const []const u8, b: []const []const u8) !void {
    try std.testing.expectEqual(a.len, b.len);
    for (a, b) |x, y| try std.testing.expectEqualSlices(u8, x, y);
}

test "cli remote sync §4/§6: bare prints, a URL sets, and --unset clears the saved remote" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // A fresh repo has no saved remote.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{"remote"});
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try std.testing.expectEqualStrings("no remote set\n", r.stdout);
    }
    // The §9.1 project code `init` seeded — the oracle for field preservation:
    // writing a remote must not disturb it (guards the config-RMW field-drop
    // regression the shared `updateConfig` helper closes).
    const code_before = try readProjectCode(gpa, io, tmp.dir);
    defer gpa.free(code_before);
    // Setting one echoes it back and persists it.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "remote", "http://h.example/xfer" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try std.testing.expectEqualStrings("remote set to http://h.example/xfer\n", r.stdout);
    }
    // The project code survived the remote write untouched.
    {
        const code_after = try readProjectCode(gpa, io, tmp.dir);
        defer gpa.free(code_after);
        try std.testing.expectEqualStrings(code_before, code_after);
    }
    {
        var r = try runZit(gpa, io, tmp.dir, &.{"remote"});
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try std.testing.expectEqualStrings("http://h.example/xfer\n", r.stdout);
    }
    // Clearing it returns the repo to "no remote set".
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "remote", "--unset" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try std.testing.expectEqualStrings("remote cleared\n", r.stdout);
    }
    {
        var r = try runZit(gpa, io, tmp.dir, &.{"remote"});
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try std.testing.expectEqualStrings("no remote set\n", r.stdout);
    }
}

test "cli sync §4/§6: an omitted URL with no saved remote is a usage error for pull, push, and sync" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // No remote saved: each sync-family verb with no URL is a usage error (exit 2),
    // diagnostic on stderr, nothing on stdout.
    for ([_][]const u8{ "pull", "push", "sync" }) |verb| {
        var r = try runZit(gpa, io, tmp.dir, &.{verb});
        defer freeResult(gpa, &r);
        try expectExit(r.term, 2);
        try std.testing.expectEqualStrings("", r.stdout);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "no saved remote") != null);
    }
}

test "cli pull sync §4: a saved remote is resolved and used verbatim when the URL arg is omitted" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Save a remote nothing listens on — stored verbatim. `zit pull` with no
    // URL must resolve THIS URL and hand it to the transport, whose connect
    // failure quotes it back — proving verbatim use (not a usage error, not
    // "not a repo"). Port 1 is reserved and closed on every host.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "remote", "http://127.0.0.1:1/xfer" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
    }
    var r = try runZit(gpa, io, tmp.dir, &.{"pull"});
    defer freeResult(gpa, &r);
    try expectExit(r.term, 1); // a connect failure, not a usage error
    try std.testing.expect(std.mem.indexOf(u8, r.stderr, "http://127.0.0.1:1/xfer") != null);
}

test "cli remote: a word that is not a URL is refused and the saved remote survives" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "remote", "http://h.example/xfer" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
    }
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "remote", "list" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 2);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "not an http:// or https:// URL") != null);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "no subcommands") != null);
    }
    var r = try runZit(gpa, io, tmp.dir, &.{"remote"});
    defer freeResult(gpa, &r);
    try expectExit(r.term, 0);
    try std.testing.expectEqualStrings("http://h.example/xfer\n", r.stdout);
}

/// An HTTP gate in front of `zit serve`: every request without an
/// `Authorization` header is answered 401 with a Basic challenge; one with
/// the header is relayed byte for byte to the real server and its reply
/// relayed back. This is the shape of a forge that gates a READ, which
/// fossil's own server never does over HTTP, so no fossil can stand in for
/// it. Each request's Authorization value (or `-`) is appended to a log.
const basic_gate_proxy =
    \\import re, socket, sys
    \\lport, tport, log = int(sys.argv[1]), int(sys.argv[2]), sys.argv[3]
    \\srv = socket.socket()
    \\srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    \\srv.bind(("127.0.0.1", lport))
    \\srv.listen(16)
    \\def recv_http(c):
    \\    data = b""
    \\    while b"\r\n\r\n" not in data:
    \\        chunk = c.recv(65536)
    \\        if not chunk:
    \\            return data
    \\        data += chunk
    \\    head, _, body = data.partition(b"\r\n\r\n")
    \\    if re.search(rb"expect:\s*100-continue", head, re.I):
    \\        c.sendall(b"HTTP/1.1 100 Continue\r\n\r\n")
    \\    m = re.search(rb"content-length:\s*(\d+)", head, re.I)
    \\    n = int(m.group(1)) if m else 0
    \\    while len(body) < n:
    \\        chunk = c.recv(65536)
    \\        if not chunk:
    \\            break
    \\        body += chunk
    \\    return head + b"\r\n\r\n" + body
    \\while True:
    \\    c, _ = srv.accept()
    \\    req = recv_http(c)
    \\    if not req:
    \\        c.close()
    \\        continue
    \\    head = req.split(b"\r\n\r\n", 1)[0]
    \\    auth = re.search(rb"^authorization:\s*(.*?)\r?$", head, re.I | re.M)
    \\    with open(log, "ab") as f:
    \\        f.write((auth.group(1) if auth else b"-") + b"\n")
    \\    if not auth:
    \\        c.sendall(b"HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"gate\"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
    \\        c.close()
    \\        continue
    \\    u = socket.create_connection(("127.0.0.1", tport))
    \\    u.sendall(req)
    \\    while True:
    \\        chunk = u.recv(65536)
    \\        if not chunk:
    \\            break
    \\        c.sendall(chunk)
    \\    u.close()
    \\    c.close()
    \\
;

fn spawnBasicGate(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, lport: u16, tport: u16) !std.process.Child {
    try writeFile(io, dir, "gate.py", basic_gate_proxy);
    var env = std.process.Environ.Map.init(gpa);
    defer env.deinit();
    try env.put("PATH", "/usr/bin:/bin");
    const l = try std.fmt.allocPrint(gpa, "{d}", .{lport});
    defer gpa.free(l);
    const t = try std.fmt.allocPrint(gpa, "{d}", .{tport});
    defer gpa.free(t);
    return std.process.spawn(io, .{
        .argv = &.{ "python3", "gate.py", l, t, "gate.log" },
        .cwd = .{ .dir = dir },
        .environ_map = &env,
        .stdout = .ignore,
        .stderr = .ignore,
    });
}

test "cli: a bare -- ends the options, so a path beginning with a dash is reachable" {
    // Without an end-of-options marker such a file could not be named at all:
    // passed bare it parses as a flag, and `./-x.txt` parses fine and then
    // matches nothing, because a path is compared literally against the
    // check-in's file list. A forge serving such a repository answered 500 for
    // every content route on that file.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "-x.txt", "dash\n");
    try writeFile(io, tmp.dir, "plain.txt", "plain\n");
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "add", "--", "-x.txt", "plain.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
    const tip = try commitHashDup(gpa, io, tmp.dir, "dash name");
    defer gpa.free(tip);

    // The verb the issue was filed against, and its two neighbours.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "cat", "--", "-x.txt", "trunk" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expectEqualStrings("dash\n", r.stdout);
    }
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "annotate", "--", "-x.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "dash") != null);
    }
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "log", "--", "trunk", "-x.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, tip) != null);
    }
    // Before the marker the flag still wins, which is what makes the marker
    // the only escape rather than one of two.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "cat", "-x.txt", "trunk" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 2);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "unknown option") != null);
    }
    // A second `--` is an operand, not another marker.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "cat", "--", "--", "trunk" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "not in the check-in") != null);
    }
}

test "cli tarball and zip: the marker is consumed, so both operands still arrive" {
    // These two spell one parser, and it let the marker fall into the VERSION
    // slot instead of consuming it: VERSION then landed in OUTPUTFILE and
    // OUTPUTFILE was refused as one argument too many. A caller that adopted
    // the marker uniformly lost both verbs outright, and because the failure
    // needs a SECOND operand to appear, a one-operand check would have passed.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    const tip = try commitHashDup(gpa, io, tmp.dir, "seed");
    defer gpa.free(tip);

    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "tarball", "--", "trunk", "out.tar.gz" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
    try std.testing.expect((try tmp.dir.statFile(io, "out.tar.gz", .{})).size > 0);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "zip", "--", "trunk", "out.zip" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
    try std.testing.expect((try tmp.dir.statFile(io, "out.zip", .{})).size > 0);
}

test "cli clone: plain http to another host says so on stderr, and loopback does not" {
    // Credentials are refused over plaintext http to a remote host, but the
    // exchange itself still crosses readable, a private repository's content
    // included. `.invalid` never resolves (RFC 2606), so the clone fails to
    // connect right after the note, with no server and no network needed.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var remote = try runZit(gpa, io, tmp.dir, &.{ "clone", "http://zit-e2e.invalid/r", "a" });
    defer freeResult(gpa, &remote);
    try expectExit(remote.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, remote.stderr, "http://zit-e2e.invalid/r is unencrypted http") != null);

    // Loopback: a closed port fails the same way, and says nothing about http.
    const port = try freshLoopbackPort(io);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{port});
    defer gpa.free(url);
    var local = try runZit(gpa, io, tmp.dir, &.{ "clone", url, "b" });
    defer freeResult(gpa, &local);
    try expectExit(local.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, local.stderr, "unencrypted") == null);
}

test "cli clone: a read gated by an HTTP Basic challenge is answered with the credential, and refused without one by name" {
    // A forge in front of a private repository answers a clone's first,
    // login-less round with 401 and a Basic challenge. The login card cannot
    // answer that round (its secret needs the project code the first reply
    // teaches), so the credential never reached the wire and the status was
    // fatal. The transport now answers the challenge with the plaintext the
    // verb holds and carries the header on every later round, as fossil's
    // --httpauth does.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "a.txt", "a\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"a.txt"});
    const tip = try commitHashDup(gpa, io, tmp_a.dir, "seed");
    defer gpa.free(tip);

    const serve_port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, serve_port);
    defer server.kill(io);
    try waitForListen(io, serve_port);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    const gate_port = try freshLoopbackPort(io);
    var gate = try spawnBasicGate(gpa, io, tmp_b.dir, gate_port, serve_port);
    defer gate.kill(io);
    try waitForListen(io, gate_port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{gate_port});
    defer gpa.free(url);

    // Anonymous: refused, and the refusal says what the peer asked for.
    {
        var r = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "anon" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "wants a login") != null);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "--user") != null);
    }
    // Named: the challenge is answered and the clone lands.
    {
        var r = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "repo", "--user", "u", "--password", "p" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
    {
        var repo = try tmp_b.dir.openDir(io, "repo", .{});
        defer repo.close(io);
        var r = try runZit(gpa, io, repo, &.{ "timeline", "-n", "1" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "seed") != null);
    }
    // The wire, as the gate saw it: the anonymous clone's one request bare;
    // the named clone's first request bare and challenged, every request
    // after it carrying `Basic base64("u:p")`.
    const log = try tmp_b.dir.readFileAlloc(io, "gate.log", gpa, .limited(1 << 16));
    defer gpa.free(log);
    var lines = std.mem.splitScalar(u8, log, '\n');
    try std.testing.expectEqualStrings("-", lines.next().?); // anonymous clone
    try std.testing.expectEqualStrings("-", lines.next().?); // named clone, round 0, challenged
    var carried: usize = 0;
    while (lines.next()) |ln| {
        if (ln.len == 0) continue;
        try std.testing.expectEqualStrings("Basic dTpw", ln);
        carried += 1;
    }
    try std.testing.expect(carried >= 2); // the retried round 0 and at least one later round
}

test "cli clone: a fresh clone converges to the served repo's artifacts over HTTP (sync §3.7/§8)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: init + a committed file, so A's log holds real artifacts (the
    // file blob and its manifest) — genuine content for the wire to carry.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "hello.txt", "hello from zit over http\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    var commit = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    // A's artifact-digest set is the convergence oracle: the RFC + the actual
    // committed content decide it, never a second fossil.
    const a_digests = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);
    try std.testing.expect(a_digests.len >= 1);

    // Serve A on a loopback port; wait (bounded) for the listener to accept.
    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // Clone A into a fresh repo B over real HTTP, targeting the BARE ROOT path
    // "/" (sync §4 L815-819 — the client sends the repo URL path unchanged and
    // MUST NOT append /xfer; a conformant `fossil clone http://host:port/`
    // POSTs to "/"). `zit serve` dispatches sync on method + Content-Type
    // regardless of path, so the root URL clones. (The fossil-client-from-root
    // case belongs in `zig build smoke`.)
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    try std.testing.expectEqualStrings("", clone.stderr);

    // THE PROOF: B exists and its on-disk artifact set equals A's — a real zit
    // client store converged to a real zit server's content over the wire
    // (sync §3.7). A partial/empty clone fails here; the assertion is not weakened.
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    const b_digests = try collectArtifactDigests(gpa, io, b_dir);
    defer freeDigests(gpa, b_digests);
    try expectSameDigestSet(a_digests, b_digests);

    // §9.1 — B adopted A's project code: the shared identity travels in the
    // clone reply's `push` card (§8.2/§8.3) and is persisted into B's config.
    // Both are the same real, 40-hex, non-placeholder code (not all zeros).
    const a_code = try readProjectCode(gpa, io, tmp_a.dir);
    defer gpa.free(a_code);
    const b_code = try readProjectCode(gpa, io, b_dir);
    defer gpa.free(b_code);
    try std.testing.expectEqual(@as(usize, 40), a_code.len);
    try std.testing.expect(!std.mem.eql(u8, a_code, "0" ** 40));
    try std.testing.expectEqualStrings(a_code, b_code);
}

test "cli clone/open: DIR derives from the URL, the clone opens itself, --no-open keeps a bare store, `open URL` clones" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "hello.txt", "hello\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    var commit = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/proj.fossil", .{port});
    defer gpa.free(url);

    // `zit clone URL` with no DIR: the last URL segment ('.fossil' dropped)
    // names the directory, and the clone is followed by an open — the tree
    // is materialized and the checkout record exists, without a second verb.
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    try std.testing.expect(std.mem.indexOf(u8, clone.stdout, "opened ") != null);
    var b_dir = try tmp_b.dir.openDir(io, "proj", .{});
    defer b_dir.close(io);
    try b_dir.access(io, ".zit-checkout", .{});
    try b_dir.access(io, "hello.txt", .{});

    // --no-open: the store is cloned but no working tree is materialized.
    var bare = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "bare", "--no-open" });
    defer freeResult(gpa, &bare);
    try expectExitDiag(bare.term, 0, bare);
    try std.testing.expect(std.mem.indexOf(u8, bare.stdout, "opened ") == null);
    var bare_dir = try tmp_b.dir.openDir(io, "bare", .{});
    defer bare_dir.close(io);
    try std.testing.expectError(error.FileNotFound, bare_dir.access(io, "hello.txt", .{}));

    // `zit open URL` is the same clone-then-open.
    var tmp_c = std.testing.tmpDir(.{});
    defer tmp_c.cleanup();
    var open = try runZit(gpa, io, tmp_c.dir, &.{ "open", url });
    defer freeResult(gpa, &open);
    try expectExitDiag(open.term, 0, open);
    var c_dir = try tmp_c.dir.openDir(io, "proj", .{});
    defer c_dir.close(io);
    try c_dir.access(io, ".zit-checkout", .{});
    try c_dir.access(io, "hello.txt", .{});
}

test "cli clone: the clone URL is saved as the remote — canonical, password-free, --once opts out, config never crosses the wire" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A carries a sentinel remote of its own: config frames are local
    // state, so the sentinel must never appear in a clone's bytes.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "hello.txt", "remote-saving clone\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    var commit = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    {
        var setr = try runZit(gpa, io, tmp_a.dir, &.{ "remote", "http://config-leak-sentinel.example/x" });
        defer freeResult(gpa, &setr);
        try expectExit(setr.term, 0);
    }

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // Clone with a password in the URL's userinfo. The connection still lands
    // on loopback; the credential exists only to prove it cannot be stored.
    const cred_url = try std.fmt.allocPrint(gpa, "http://user:[email protected]:{d}/", .{port});
    defer gpa.free(cred_url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", cred_url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    // The saved remote is the canonical form: username kept, password gone.
    {
        var rem = try runZit(gpa, io, b_dir, &.{"remote"});
        defer freeResult(gpa, &rem);
        try expectExit(rem.term, 0);
        const want = try std.fmt.allocPrint(gpa, "http://[email protected]:{d}/\n", .{port});
        defer gpa.free(want);
        try std.testing.expectEqualStrings(want, rem.stdout);
    }
    // Byte-scan, not config read-back: the password and A's sentinel appear
    // NOWHERE in the cloned store.
    {
        const store_bytes = try readFile(gpa, io, b_dir, ".zit");
        defer gpa.free(store_bytes);
        try std.testing.expect(std.mem.indexOf(u8, store_bytes, "hunter2secret") == null);
        try std.testing.expect(std.mem.indexOf(u8, store_bytes, "config-leak-sentinel") == null);
    }
    // The acceptance headline: the very next pull needs no URL.
    {
        var pull = try runZit(gpa, io, b_dir, &.{"pull"});
        defer freeResult(gpa, &pull);
        try expectExitDiag(pull.term, 0, pull);
    }

    // --once: same clone, nothing saved, pull refuses with the NoRemote text.
    var tmp_c = std.testing.tmpDir(.{});
    defer tmp_c.cleanup();
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    {
        var clone = try runZit(gpa, io, tmp_c.dir, &.{ "clone", url, "repo", "--once" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var c_dir = try tmp_c.dir.openDir(io, "repo", .{});
    defer c_dir.close(io);
    {
        var rem = try runZit(gpa, io, c_dir, &.{"remote"});
        defer freeResult(gpa, &rem);
        try expectExit(rem.term, 0);
        try std.testing.expectEqualStrings("no remote set\n", rem.stdout);
    }
    {
        // No URL and no saved remote is a usage error (exit 2), matching the
        // earlier behavior of a bare `zit pull` in a remoteless repository.
        var pull = try runZit(gpa, io, c_dir, &.{"pull"});
        defer freeResult(gpa, &pull);
        try expectExit(pull.term, 2);
        try std.testing.expect(std.mem.indexOf(u8, pull.stderr, "no repository URL given and no saved remote") != null);
    }
}

// --- §9.3 configuration exchange: the project-scope submanifest settings ---
//
// draft-fossil-submanifests-00 §6.3 requires the partition settings to reach
// every writer of a project ("configuration propagating to clones ... is
// REQUIRED"); §8.1 says the same of `enable-submanifests`, and §6.4 adds
// `submanifest-auto-retune`. [FossilSync] §9.3's scalar
// `/config` channel is the transport. These tests run TWO real repositories
// over real HTTP — the gap the feature closes is exactly "the clone comes up
// with different settings from the origin", which nothing below the CLI can
// observe.

/// The stored `submanifest-auto-retune`, read back through the library. The
/// §6.4 setting is not in `submanifest.Settings` (which models only §6.3's
/// partition state), so it is read off the config frame directly.
fn readAutoRetune(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !bool {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_only });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);
    var oc = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult;
    defer oc.deinit(gpa);
    return oc.config.submanifest_auto_retune;
}

/// POST one hand-built request-card stream to a running `zit serve` and return
/// the decoded reply cards. A raw socket rather than `zit push`: the §9.3 r30
/// `config` card is one no zit client emits, so there is no verb that produces
/// this request and the wire has to be written by hand. Caller owns the result.
fn postXferCards(gpa: std.mem.Allocator, io: std.Io, port: u16, request_cards: []const u8) ![]u8 {
    return postXferCardsHeaders(gpa, io, port, request_cards, "");
}

/// `postXferCards` with extra request-header lines (each `\r\n`-terminated),
/// the channel a front uses to hand `zit serve --fronted` its rule.
fn postXferCardsHeaders(gpa: std.mem.Allocator, io: std.Io, port: u16, request_cards: []const u8, extra_headers: []const u8) ![]u8 {
    const got = try postXferCardsHead(gpa, io, port, request_cards, extra_headers);
    gpa.free(got.head);
    return got.body;
}

/// A response's head and decoded body, both the caller's to free.
const XferHead = struct { head: []u8, body: []u8 };

/// `postXferCardsHeaders`, keeping the response head too, for a test that
/// reads a response header.
fn postXferCardsHead(gpa: std.mem.Allocator, io: std.Io, port: u16, request_cards: []const u8, extra_headers: []const u8) !XferHead {
    const framed = try container.encode(gpa, request_cards, .default);
    defer gpa.free(framed);
    const stream = try dialServe(io, port);
    defer stream.close(io);
    {
        var wbuf: [4096]u8 = undefined;
        var w = stream.writer(io, &wbuf);
        try w.interface.print(
            "POST /xfer HTTP/1.1\r\nHost: z\r\nContent-Type: application/x-fossil\r\n" ++
                "Content-Length: {d}\r\n{s}Connection: close\r\n\r\n",
            .{ framed.len, extra_headers },
        );
        try w.interface.writeAll(framed);
        try w.interface.flush();
    }
    var rbuf: [4096]u8 = undefined;
    var r = stream.reader(io, &rbuf);
    // `keep_alive = false` on the server side means Content-Length plus a close,
    // so the read ends at EOF.
    const raw = try r.interface.allocRemaining(gpa, .limited(1 << 20));
    defer gpa.free(raw);
    const sep = std.mem.indexOf(u8, raw, "\r\n\r\n") orelse return error.TestUnexpectedResult;
    const head = try gpa.dupe(u8, raw[0..sep]);
    errdefer gpa.free(head);
    return .{ .head = head, .body = try container.decode(gpa, raw[sep + 4 ..], 1 << 20) };
}

test "cli clone submanifests §6.3/§8.1: a clone adopts the origin's three project-scope settings" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: a real check-in, then all three settings moved off their
    // defaults. `enable` is the verb surface; `submanifest-auto-retune` has no
    // flag yet, so it is written through the config frame (see `forceAutoRetune`).
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "hello.txt", "hello from zit over http\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    var commit = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    {
        var en = try runZit(gpa, io, tmp_a.dir, &.{ "submanifest", "enable", "--k", "3" });
        defer freeResult(gpa, &en);
        try expectExitDiag(en.term, 0, en);
    }
    try forceAutoRetune(gpa, io, tmp_a.dir, true);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    try std.testing.expectEqualStrings("", clone.stderr);

    // THE PROOF: B came up with A's settings, not with the `init` defaults —
    // §6.3's project scope, over the §9.3 scalar channel, between two real
    // repositories. A clone that ignored the config card fails here.
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    const b = try readSubmanifestSettings(gpa, io, b_dir);
    try std.testing.expect(b.enabled);
    try std.testing.expectEqual(@as(u8, 3), b.k);
    try std.testing.expect(try readAutoRetune(gpa, io, b_dir));

    // The same three values as the operator sees them in the clone.
    {
        var show = try runZit(gpa, io, b_dir, &.{ "submanifest", "show" });
        defer freeResult(gpa, &show);
        try expectExitDiag(show.term, 0, show);
        try std.testing.expect(std.mem.indexOf(u8, show.stdout, "enable-submanifests: on") != null);
        try std.testing.expect(std.mem.indexOf(u8, show.stdout, "submanifest-k:       3") != null);
        try std.testing.expect(std.mem.indexOf(u8, show.stdout, "submanifest-auto-retune: on") != null);
    }

    // And they are A's values, read back from A rather than restated.
    const a = try readSubmanifestSettings(gpa, io, tmp_a.dir);
    try std.testing.expectEqual(a.enabled, b.enabled);
    try std.testing.expectEqual(a.k, b.k);
    try std.testing.expectEqual(
        try readAutoRetune(gpa, io, tmp_a.dir),
        try readAutoRetune(gpa, io, b_dir),
    );
}

test "cli clone submanifests §8.1: cloning a defaulted origin yields a defaulted clone" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: never configured. The origin's answer is three explicit
    // default-valued rows, not silence, so this pins that adopting them leaves
    // the clone exactly where `init` put it — off, unpinned, no auto-retune.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "hello.txt", "plain repository\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    var commit = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    const a_digests = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    try std.testing.expectEqualStrings("", clone.stderr);

    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    const b = try readSubmanifestSettings(gpa, io, b_dir);
    try std.testing.expect(!b.enabled);
    try std.testing.expect(!b.pinned());
    try std.testing.expect(!try readAutoRetune(gpa, io, b_dir));

    // The clone is otherwise unchanged: the configuration round costs the
    // convergence nothing.
    const b_digests = try collectArtifactDigests(gpa, io, b_dir);
    defer freeDigests(gpa, b_digests);
    try expectSameDigestSet(a_digests, b_digests);
}

test "cli serve §9.3 r31: an unauthorized pushed config card is refused and changes nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: enabled at k=3, auto-retune off — a settled configuration a
    // stranger is about to try to rewrite.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "hello.txt", "served repository\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    var commit = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    {
        var en = try runZit(gpa, io, tmp_a.dir, &.{ "submanifest", "enable", "--k", "3" });
        defer freeResult(gpa, &en);
        try expectExitDiag(en.term, 0, en);
    }

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // §9.3 r30 — a well-formed scalar config push, from nobody: `zit serve`
    // holds no credential, so no requester can ever reach Admin.
    var payload_buf: [sync.submanifest_config_payload_max]u8 = undefined;
    const payload = try sync.writeSubmanifestConfigPayload(false, 11, true, &payload_buf);
    const request = try std.fmt.allocPrint(
        gpa,
        "pragma client-version 20000\nconfig /config {d}\n{s}\n",
        .{ payload.len, payload },
    );
    defer gpa.free(request);
    const reply = try postXferCards(gpa, io, port, request);
    defer gpa.free(reply);

    // §9.3 r31 L1927-1929 — the exact required text (§11.1 r2 fossilizes its
    // spaces on the wire), and nothing else in the reply: the rejection resets
    // the partial output.
    try std.testing.expectEqualStrings("error not\\sauthorized\\sto\\spush\\sconfiguration\n", reply);

    // THE PROOF: A's stored configuration is byte-for-byte what it was. A
    // server that applied the push would show k=11 and auto-retune on here.
    const a = try readSubmanifestSettings(gpa, io, tmp_a.dir);
    try std.testing.expect(a.enabled);
    try std.testing.expectEqual(@as(u8, 3), a.k);
    try std.testing.expect(!try readAutoRetune(gpa, io, tmp_a.dir));
}

test "cli serve §9.3: a peer that never asks for configuration still clones unchanged" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // The interop case for a peer predating this feature: a request that
    // carries NO reqconfig must draw no config card and no error — the "group
    // mask zero" no-op extended to a peer that simply does not ask. Simulated
    // at the wire, because every zit clone now does ask.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "hello.txt", "served repository\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    var commit = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    {
        var en = try runZit(gpa, io, tmp_a.dir, &.{ "submanifest", "enable", "--k", "3" });
        defer freeResult(gpa, &en);
        try expectExitDiag(en.term, 0, en);
    }
    const a_digests = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    const silent = try postXferCards(gpa, io, port, "pragma client-version 20000\npull s p\n");
    defer gpa.free(silent);
    try std.testing.expect(std.mem.indexOf(u8, silent, "config") == null);
    try std.testing.expect(std.mem.indexOf(u8, silent, "error") == null);

    // §9.3 r28 — `/shun` IS modelled now, but this repository shuns nothing, so
    // an empty table answers with no cards and no error. Same silent shape a
    // group with no rows always had; the populated case is its own test below.
    const table = try postXferCards(gpa, io, port, "pragma client-version 20000\nreqconfig /shun\n");
    defer gpa.free(table);
    try std.testing.expect(std.mem.indexOf(u8, table, "config /") == null);
    try std.testing.expect(std.mem.indexOf(u8, table, "error") == null);

    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    try expectCloneConverges(gpa, io, &tmp_a, url, a_digests);
}

test "sync §9.3 r28: the /shun config group carries the shun table to a clone" {
    // The gap the round-trip run found, and the reason it is a SAFETY gap
    // rather than a cosmetic one: fossil's shun table is its takedown
    // mechanism. A clone that arrives without it will, on its next sync with
    // any third party, re-accept exactly the content an operator was ordered to
    // remove — and the scale run showed the loss produced 12 spurious phantoms
    // as well, every one of them a shunned artifact.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "origin", .default_dir);
    var origin = try tmp.dir.openDir(io, "origin", .{});
    defer origin.close(io);
    try initRepo(gpa, io, origin);

    // Three check-ins so there is content to suppress that is not the tip:
    // `shun` refuses the check-in the working tree sits on.
    inline for (.{ "a", "b", "c" }) |name| {
        try writeFile(io, origin, name ++ ".txt", "content " ++ name ++ "\n");
        try addManaged(gpa, io, origin, &.{name ++ ".txt"});
        var c = try runCommitEnv(gpa, io, origin, .{ .user = "alice" }, &.{ "commit", "-m", name });
        defer freeResult(gpa, &c);
        try expectExitDiag(c.term, 0, c);
    }

    var ls = try runZit(gpa, io, origin, &.{ "ls", "--long" }); // the blob-id leads a --long line
    defer freeResult(gpa, &ls);
    try expectExitDiag(ls.term, 0, ls);
    const first_line_end = std.mem.indexOfScalar(u8, ls.stdout, '\n').?;
    const blob = ls.stdout[0..std.mem.indexOfScalar(u8, ls.stdout[0..first_line_end], ' ').?];

    const reason = "DMCA takedown 2026-08";
    var sh = try runZit(gpa, io, origin, &.{ "shun", "--force", "--reason", reason, blob });
    defer freeResult(gpa, &sh);
    try expectExitDiag(sh.term, 0, sh);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, origin, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // The wire itself, before any clone: one `config /shun` card carrying
    // fossil's `configure_send_group` row shape.
    const cards = try postXferCards(gpa, io, port, "pragma client-version 20000\nreqconfig /shun\n");
    defer gpa.free(cards);
    try std.testing.expect(std.mem.indexOf(u8, cards, "config /shun ") != null);
    try std.testing.expect(std.mem.indexOf(u8, cards, blob) != null);
    try std.testing.expect(std.mem.indexOf(u8, cards, " scom '") != null);
    try std.testing.expect(std.mem.indexOf(u8, cards, reason) != null);
    try std.testing.expect(std.mem.indexOf(u8, cards, "error") == null);

    // And end to end: the clone comes up holding the tombstone, reason intact.
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    var cl = try runZit(gpa, io, tmp.dir, &.{ "clone", "--no-open", url, "clone" });
    defer freeResult(gpa, &cl);
    try expectExitDiag(cl.term, 0, cl);
    try std.testing.expect(std.mem.indexOf(u8, cl.stdout, "adopted 1 shun row") != null);

    var clone_dir = try tmp.dir.openDir(io, "clone", .{});
    defer clone_dir.close(io);
    var list = try runZit(gpa, io, clone_dir, &.{ "shun", "--list" });
    defer freeResult(gpa, &list);
    try expectExitDiag(list.term, 0, list);
    try std.testing.expect(std.mem.indexOf(u8, list.stdout, blob) != null);
    try std.testing.expect(std.mem.indexOf(u8, list.stdout, reason) != null);
}

// --- pull interop (sync §3.2/§3.3/§3.7) ------------------------------------
//
// The pull milestone e2e: an incremental `zit pull` into an EXISTING repo B over
// loopback HTTP. B is cloned from A, then A grows a second check-in, then B pulls
// A's new artifacts and its on-disk set must equal A's new full set — proving the
// pull client fetched exactly what B lacked and converged (sync §3.7), over the
// wire, not from any fossil oracle. `zit serve` reads a fixed snapshot of the
// repository at startup, so A is re-served after the second commit; the restart
// is a harness detail, not part of what pull is asked to prove.

test "cli pull: an incremental pull fetches a repo's new artifacts over HTTP (sync §3.2/§3.3/§3.7)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: init + a committed file — A's first check-in, real artifacts to seed.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "one.txt", "first artifact over http\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"one.txt"});
    var commit1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit1);
    try expectExit(commit1.term, 0);

    // Serve A's seed state and clone it into B; B now shares A's project-code and
    // holds A's first-check-in artifacts (the pull starts from a real subset).
    const clone_port = try freshLoopbackPort(io);
    var clone_server = try spawnServe(gpa, io, tmp_a.dir, clone_port);
    try waitForListen(io, clone_port);
    const clone_url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{clone_port});
    defer gpa.free(clone_url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", clone_url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    // The seed server has served its purpose; stop it before A grows so the next
    // server starts from a snapshot that includes the second check-in.
    clone_server.kill(io);

    // A grows: a second committed file, so A now holds artifacts B does not.
    try writeFile(io, tmp_a.dir, "two.txt", "second artifact B has never seen\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"two.txt"});
    var commit2 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "grow" });
    defer freeResult(gpa, &commit2);
    try expectExit(commit2.term, 0);

    // A's new full artifact-digest set is the convergence oracle for the pull.
    const a_digests = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);
    try std.testing.expect(a_digests.len > b_seed_len(gpa, io, tmp_b.dir));

    // Re-serve A (now two check-ins) on a fresh port; pull from within B.
    const pull_port = try freshLoopbackPort(io);
    var pull_server = try spawnServe(gpa, io, tmp_a.dir, pull_port);
    defer pull_server.kill(io);
    try waitForListen(io, pull_port);
    const pull_url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{pull_port});
    defer gpa.free(pull_url);

    var b_repo = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_repo.close(io);
    var pull = try runZit(gpa, io, b_repo, &.{ "pull", pull_url });
    defer freeResult(gpa, &pull);
    try expectExitDiag(pull.term, 0, pull);
    try std.testing.expectEqualStrings("", pull.stderr);

    // THE PROOF: B's on-disk artifact set now equals A's NEW full set — B received
    // the second check-in's artifacts over the wire and converged (sync §3.7). A
    // pull that fetched nothing (or only part) fails here; the assertion is not weakened.
    const b_digests = try collectArtifactDigests(gpa, io, b_repo);
    defer freeDigests(gpa, b_digests);
    try expectSameDigestSet(a_digests, b_digests);
}

/// The count of artifacts B holds straight after its clone — the pull's starting
/// subset. Read back through the library so the incremental assertion (A grew
/// past this) rests on B's real on-disk state, not an assumed number.
fn b_seed_len(gpa: std.mem.Allocator, io: std.Io, tmp_b: std.Io.Dir) usize {
    var b_repo = tmp_b.openDir(io, "repo", .{}) catch return 0;
    defer b_repo.close(io);
    const d = collectArtifactDigests(gpa, io, b_repo) catch return 0;
    defer freeDigests(gpa, d);
    return d.len;
}

// --- incremental pull, no server restart (sync §3.8) -----------------------
//
// Direction A of the fossil incremental-pull interop: a `zit serve` must
// advertise the CURRENT on-disk repository per request, so a check-in committed
// to A *while the server is live* is pulled by B without restarting serve. The
// server is stateless per request (§3.8), so it re-reads A's log every request
// rather than snapshotting its holdings once at startup. One server, spawned
// before the second commit and never restarted, is the whole point.

test "cli pull: a commit made while the server runs is pulled without a restart (sync §3.8)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: init + a first committed file — real seed artifacts.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "one.txt", "first artifact over http\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"one.txt"});
    var commit1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit1);
    try expectExit(commit1.term, 0);

    // ONE server for A, spawned now and never restarted — the invariant under test.
    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{port});
    defer gpa.free(url);

    // Clone A into B over the live server; B holds A's seed check-in.
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_repo = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_repo.close(io);
    const seed_digests = try collectArtifactDigests(gpa, io, b_repo);
    const seed_len = seed_digests.len;
    freeDigests(gpa, seed_digests);

    // A grows a SECOND check-in while the SAME server keeps running — no restart.
    try writeFile(io, tmp_a.dir, "two.txt", "second artifact committed with serve still live\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"two.txt"});
    var commit2 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "grow" });
    defer freeResult(gpa, &commit2);
    try expectExit(commit2.term, 0);

    // A's new full digest set is the convergence oracle.
    const a_digests = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);
    try std.testing.expect(a_digests.len > seed_len);

    // Pull from B against the still-live server. It MUST see the second check-in.
    var pull = try runZit(gpa, io, b_repo, &.{ "pull", url });
    defer freeResult(gpa, &pull);
    try expectExitDiag(pull.term, 0, pull);
    try std.testing.expectEqualStrings("", pull.stderr);

    // THE PROOF: B's on-disk artifact set equals A's NEW full set — the server
    // advertised the live commit and B converged (§3.7), no restart involved.
    const b_digests = try collectArtifactDigests(gpa, io, b_repo);
    defer freeDigests(gpa, b_digests);
    try expectSameDigestSet(a_digests, b_digests);
}

// --- push interop (sync §3.3/§3.7) -----------------------------------------
//
// The push milestone e2e: an outgoing `zit push` that UPLOADS a repo's new
// artifacts into an EXISTING served repo over loopback HTTP. B is cloned from A,
// then B grows a second check-in, then B pushes its new artifacts to A, and A's
// on-disk set must equal B's full set — proving the push client uploaded exactly
// what A lacked and the server persisted it durably (sync §3.3/§3.7), over the
// wire, not from any fossil oracle. This is the mirror of pull: content flows the
// other way, and the durable landing is on the SERVER (A), not the client.

test "cli push: an outgoing push lands a repo's new artifacts on the server over HTTP (sync §3.3/§3.7)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: init + a committed file — A's first check-in, real artifacts to seed.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "one.txt", "first artifact over http\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"one.txt"});
    var commit1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit1);
    try expectExit(commit1.term, 0);

    // Serve A's seed state and clone it into B; B now shares A's project-code and
    // holds A's first-check-in artifacts (the push starts from a real subset).
    const clone_port = try freshLoopbackPort(io);
    var clone_server = try spawnServe(gpa, io, tmp_a.dir, clone_port);
    try waitForListen(io, clone_port);
    const clone_url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{clone_port});
    defer gpa.free(clone_url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", clone_url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    clone_server.kill(io);

    // B grows: a second committed file, so B now holds artifacts A does not.
    var b_repo = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_repo.close(io);
    try writeFile(io, b_repo, "two.txt", "second artifact A has never seen\n");
    try addManaged(gpa, io, b_repo, &.{"two.txt"});
    var commit2 = try runCommitEnv(gpa, io, b_repo, .{ .user = "u" }, &.{ "commit", "-m", "grow" });
    defer freeResult(gpa, &commit2);
    try expectExit(commit2.term, 0);

    // B's full artifact-digest set is the convergence oracle: after the push, A
    // must hold exactly this set. Captured now, before the wire runs.
    const b_digests = try collectArtifactDigests(gpa, io, b_repo);
    defer freeDigests(gpa, b_digests);
    try std.testing.expect(b_digests.len > b_seed_len(gpa, io, tmp_b.dir) - 1);

    // Serve A (still only the seed check-in) and push B's content up to it.
    const push_port = try freshLoopbackPort(io);
    var push_server = try spawnServe(gpa, io, tmp_a.dir, push_port);
    defer push_server.kill(io);
    try waitForListen(io, push_port);
    const push_url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{push_port});
    defer gpa.free(push_url);

    var push = try runZit(gpa, io, b_repo, &.{ "push", push_url });
    defer freeResult(gpa, &push);
    try expectExitDiag(push.term, 0, push);
    try std.testing.expectEqualStrings("", push.stderr);

    // Stop the server so its file handles are released before A is read back — the
    // durable landing is on disk, independent of the live process.
    push_server.kill(io);

    // THE PROOF: A's on-disk artifact set now equals B's full set — A received the
    // second check-in's artifacts over the wire and the server persisted them
    // durably (sync §3.3/§3.7). A push the server dropped (or only half-stored)
    // fails here; the assertion is not weakened.
    const a_digests = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);
    try expectSameDigestSet(a_digests, b_digests);
}

// --- real-socket robustness (serve accept/read loop) -----------------------
//
// `serve_fuzz` drives the in-memory `driveHttp` seam; the socket accept/read
// loop — the part that can HANG — is only exercised by the happy-path interop
// tests above. `zit serve` accepts one connection to completion before the next
// (single-threaded, `serve.zig`), so a peer that connects and then stalls its
// request — a byte-dribbling slow-loris, a half-sent body, an idle hold — would
// pin the accept loop and wedge the server for every other client (the hung
// spawned server `ci.yml` names as the feared failure). These four scenarios
// abuse a LIVE `zit serve` over loopback and assert, with bounded waits, that
// the server survives: a fresh request is still answered within a bounded
// window, and a full `clone` sync round still converges. The bound is the
// per-connection deadline in `serve.zig` (`default_conn_read_timeout_ns`); the
// stalled connection is abandoned when it wins, freeing the loop.

/// Open a raw loopback TCP connection to a live `zit serve` on `port`. Unlike a
/// `zit` client this sends bytes the harness controls verbatim, so it can stall
/// mid-request. Caller `close`s the returned stream.
fn dialServe(io: std.Io, port: u16) !net.Stream {
    const addr = net.IpAddress.parseIp4("127.0.0.1", port) catch return error.BadTestAddress;
    return addr.connect(io, .{ .mode = .stream });
}

/// Read the first bytes of the server's HTTP reply on `stream`, returning true
/// iff they are a status line (`HTTP/…`). Run as a `Select` task so a wedged
/// server (no reply ever comes) is a cancellation point the deadline interrupts.
fn probeReadOk(io: std.Io, stream: net.Stream) bool {
    var buf: [64]u8 = undefined;
    var r = stream.reader(io, &buf);
    const head = r.interface.take(5) catch return false;
    return std.mem.eql(u8, head, "HTTP/");
}

fn probeDeadline(io: std.Io, ns: u64) void {
    io.sleep(.{ .nanoseconds = @intCast(ns) }, .awake) catch {};
}

/// Bounded liveness proof: open a FRESH connection, send a well-formed HTTP
/// request head, and require a status-line reply within `window_ns`. On a
/// single-threaded server this reply cannot arrive until any prior stalled
/// connection is abandoned by the serve deadline, so `window_ns` must exceed it;
/// the race converts a persistent wedge into `error.ServerWedged` rather than an
/// unbounded hang (the bounded-waits rule: interval AND deadline). A clean
/// reply proves the accept loop was freed and the server still serves.
fn expectServesFreshRequest(io: std.Io, port: u16, window_ns: u64) !void {
    const stream = dialServe(io, port) catch return error.ProbeConnectFailed;
    defer stream.close(io);
    {
        var wbuf: [128]u8 = undefined;
        var w = stream.writer(io, &wbuf);
        // A GET is a well-formed request the server routes and answers (with a
        // §4 classification status); the point is that a fresh connection is
        // accepted and answered at all — proof the loop is not pinned.
        try w.interface.writeAll("GET /xfer HTTP/1.1\r\nHost: z\r\nConnection: close\r\n\r\n");
        try w.interface.flush();
    }
    const Outcome = union(enum) { replied: bool, expired: void };
    var slots: [2]Outcome = undefined;
    var race = std.Io.Select(Outcome).init(io, &slots);
    try race.concurrent(.replied, probeReadOk, .{ io, stream });
    race.concurrent(.expired, probeDeadline, .{ io, window_ns }) catch {
        // No concurrency for the deadline task: fall back to an unbounded read
        // (the server is expected to reply; this only loses the wedge ceiling).
        const got = race.await() catch {
            race.cancelDiscard();
            return error.ProbeCanceled;
        };
        race.cancelDiscard();
        if (got == .replied and got.replied) return;
        return error.ServerWedged;
    };
    const outcome = race.await() catch {
        race.cancelDiscard();
        return error.ProbeCanceled;
    };
    race.cancelDiscard();
    switch (outcome) {
        .replied => |ok| if (!ok) return error.ServerBadReply,
        .expired => return error.ServerWedged,
    }
}

/// Prove a full sync round still converges after the abuse: clone the served
/// repo into a fresh tree and require its on-disk artifact set to equal the
/// server's (`expectSameDigestSet`) — the interop oracle, unweakened.
fn expectCloneConverges(gpa: std.mem.Allocator, io: std.Io, tmp_parent: *std.testing.TmpDir, url: []const u8, want: []const []const u8) !void {
    var clone = try runZit(gpa, io, tmp_parent.dir, &.{ "clone", url, "recover" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_parent.dir.openDir(io, "recover", .{});
    defer b_dir.close(io);
    const got = try collectArtifactDigests(gpa, io, b_dir);
    defer freeDigests(gpa, got);
    try expectSameDigestSet(want, got);
}

/// The bounded ceiling for `expectServesFreshRequest`: a stalled connection is
/// abandoned by the serve deadline (`serve.default_conn_read_timeout_ns`, 5 s),
/// after which a fresh loopback request is answered in milliseconds. 15 s is 3×
/// that deadline — ample slack for a loaded runner while still bounding a true
/// wedge into a clean failure well inside the e2e step timeout.
const serve_probe_window_ns: u64 = 15_000_000_000;

/// Seed a one-commit repo in `dir` and return its artifact-digest set (the
/// convergence oracle). Caller frees via `freeDigests`.
fn seedServedRepo(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![][]u8 {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "seed.txt", "served content for robustness\n");
    try addManaged(gpa, io, dir, &.{"seed.txt"});
    var commit = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);
    return collectArtifactDigests(gpa, io, dir);
}

test "serve robustness: a byte-dribbled slow-loris head does not wedge the server" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    const a_digests = try seedServedRepo(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // Slow-loris: dribble a partial request head one byte per 50 ms and NEVER
    // complete it, holding the connection open. Bounded: 8 bytes × 50 ms = 400 ms
    // of dribble, then hold — the head terminator (\r\n\r\n) never arrives, so
    // `receiveHead` on the server blocks until its deadline abandons it.
    const loris = try dialServe(io, port);
    defer loris.close(io);
    {
        var wbuf: [8]u8 = undefined;
        var w = loris.writer(io, &wbuf);
        const dribble = "POST /xf"; // a partial, never-terminated request head
        for (dribble) |c| {
            w.interface.writeByte(c) catch break;
            w.interface.flush() catch break;
            io.sleep(.{ .nanoseconds = 50_000_000 }, .awake) catch {};
        }
    }

    // The server survives: a fresh request is answered within the bounded window
    // (served only once the loris connection is abandoned by the deadline).
    try expectServesFreshRequest(io, port, serve_probe_window_ns);

    // And a full sync round still converges against the same live server.
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    try expectCloneConverges(gpa, io, &tmp_a, url, a_digests);
}

test "serve robustness: a valid head then a half-sent body does not wedge the server" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    const a_digests = try seedServedRepo(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // Send a COMPLETE, well-formed sync request head declaring a 100-byte body,
    // then send only 50 bytes and HOLD. The server accepts the head, then blocks
    // reading the remaining body bytes that never come, until the deadline.
    const partial = try dialServe(io, port);
    defer partial.close(io);
    {
        var wbuf: [256]u8 = undefined;
        var w = partial.writer(io, &wbuf);
        try w.interface.writeAll(
            "POST /xfer HTTP/1.1\r\nHost: z\r\n" ++
                "Content-Type: application/x-fossil\r\nContent-Length: 100\r\n" ++
                "Connection: close\r\n\r\n",
        );
        try w.interface.writeAll("x" ** 50); // half of the declared 100-byte body
        try w.interface.flush();
    }

    try expectServesFreshRequest(io, port, serve_probe_window_ns);

    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    try expectCloneConverges(gpa, io, &tmp_a, url, a_digests);
}

test "serve robustness: a client that disconnects mid-response does not wedge the server" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    const a_digests = try seedServedRepo(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // Send a complete well-formed request, then close the connection WITHOUT
    // reading the reply — the peer vanishes while the server is answering. The
    // server's write to a gone peer fails per-connection and the loop moves on.
    {
        const abrupt = try dialServe(io, port);
        var wbuf: [128]u8 = undefined;
        var w = abrupt.writer(io, &wbuf);
        try w.interface.writeAll("GET /xfer HTTP/1.1\r\nHost: z\r\nConnection: close\r\n\r\n");
        try w.interface.flush();
        abrupt.close(io); // disconnect without draining the response
    }

    try expectServesFreshRequest(io, port, serve_probe_window_ns);

    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    try expectCloneConverges(gpa, io, &tmp_a, url, a_digests);
}

test "serve robustness: a burst of idle connections that never send a byte does not wedge the server" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    const a_digests = try seedServedRepo(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // A burst of connections that connect and immediately give up without ever
    // sending a byte. Each is a clean EOF the accept loop must drain and move
    // past — a connection-churn peer must not pin or leak the server. Bounded:
    // a fixed count, each opened and closed in turn (sequential, never held).
    const idle_connections: u32 = 5;
    var i: u32 = 0;
    while (i < idle_connections) : (i += 1) {
        const idle = try dialServe(io, port);
        idle.close(io); // zero bytes sent, then gone
    }

    try expectServesFreshRequest(io, port, serve_probe_window_ns);

    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    try expectCloneConverges(gpa, io, &tmp_a, url, a_digests);
}

// ==========================================================================
// `zit extras` (contracts/extras.md, B-extras-1..18). The sweep lists every
// on-disk file the repo is not tracking, one sorted root-relative path per
// line. Each test drives the real binary; expected bytes come from the clause
// text and inline ASCII, never from running fossil.
// ==========================================================================

/// The verbatim `zit extras` help — an independent transcription of `main.zig`'s
/// `extras_help` (B-extras-11), so a drift between the two is caught here.
const extras_help_expected =
    \\usage: zit extras [--dotfiles] [-h|--help]
    \\
    \\List every file in the checkout that zit is not tracking — each on-disk file
    \\under the checkout root that is neither a tracked file nor a zit control file.
    \\Paths are shown relative to the checkout root, one per line, sorted. Files in
    \\subdirectories are included; directories themselves are not. Symlinks are
    \\listed but never followed. With nothing untracked, the output is empty.
    \\
    \\Names beginning with a dot are not listed unless --dotfiles is given.
    \\
    \\options:
    \\      --dotfiles
    \\                also list names beginning with a dot (also settable with
    \\                `zit settings dotfiles on`)
    \\  -h, --help    show this help and exit
    \\
    \\exit status:
    \\  0  success
    \\  1  operational failure (not a repository, unreadable state, a refused
    \\     action) — the diagnostic on stderr names it
    \\  2  usage error (an unknown option or unexpected argument)
    \\
;

test "cli extras B-extras-1: lists every untracked file once, sorted, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // Three untracked files in reverse-of-sorted creation order; `.zit` and
    // `.zit-checkout` (control) are present but never listed.
    for ([_][]const u8{ "zebra", "apple", "mango" }) |name| try touch(io, tmp.dir, name);

    var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // ASCII ascending: apple < mango < zebra. Each appears exactly once.
    try std.testing.expectEqualStrings("apple\nmango\nzebra\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli extras B-extras-3: a nested file prints as sub/dir/name, no ./ or leading /" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDirPath(io, "sub/dir");
    try touch(io, tmp.dir, "sub/dir/name");

    var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("sub/dir/name\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli extras B-extras-4: a tracked file is not listed even when on disk" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "keep");
    try touch(io, tmp.dir, "loose");
    try addManaged(gpa, io, tmp.dir, &.{"keep"});

    var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("loose\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli extras B-extras-10: an empty checkout yields empty stdout, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir); // only the control files exist

    var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli extras B-extras-11: -h and --help print verbatim help to stdout, exit 0, help wins over a trailing token" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Help wins even when an otherwise-invalid token trails it (B-extras-11).
    const invocations = [_][]const []const u8{
        &.{ "extras", "--help" },
        &.{ "extras", "-h" },
        &.{ "extras", "--help", "foo" },
        &.{ "extras", "-h", "--all" },
    };
    for (invocations) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(extras_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
}

test "cli extras B-extras-12: an unknown option is a usage error, exit 2, stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const cases = [_]struct { flag: []const u8 }{ .{ .flag = "--all" }, .{ .flag = "-0" } };
    for (cases) |c| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "extras", c.flag });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        const expected = try std.fmt.allocPrint(
            gpa,
            "zit extras: unknown option '{s}'\ntry 'zit extras --help'\n",
            .{c.flag},
        );
        defer gpa.free(expected);
        try std.testing.expectEqualStrings(expected, res.stderr);
    }
}

test "cli extras B-extras-13: a positional argument is a usage error, exit 2, stderr" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // A bare operand and any token after `--` are both extra arguments.
    const invocations = [_][]const []const u8{
        &.{ "extras", "foo" },
        &.{ "extras", "--", "foo" },
    };
    for (invocations) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings(
            "zit extras: unexpected argument 'foo'\ntry 'zit extras --help'\n",
            res.stderr,
        );
    }
}

test "cli extras B-extras-14: outside a repository, exit 1, not_a_repo on stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try touch(io, tmp.dir, "a"); // a file, but no repo anywhere above

    var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit: no zit checkout here (looked for .zit-checkout in this directory and its parents)\n" ++
            "if this is a bare repository, name it with -R, or run 'zit open' to create a working tree\n",
        res.stderr,
    );
}

test "cli extras B-extras-15: a corrupt .zit-managed fails cleanly, exit 1, stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try corruptSection(io, tmp.dir, .managed);

    var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit extras: cannot read tracked files: .zit-checkout is corrupt\n",
        res.stderr,
    );
}

test "cli extras B-extras-16: read-only — the tree is unchanged after a run" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "one");
    try tmp.dir.createDirPath(io, "d");
    try touch(io, tmp.dir, "d/two");

    var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("d/two\none\n", res.stdout);

    // extras writes no state and moves no working file (B-extras-16): the user
    // files remain, and none of the artifacts a mutator would create appeared.
    try std.testing.expect(fileExists(io, tmp.dir, "one"));
    try std.testing.expect(fileExists(io, tmp.dir, "d/two"));
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
    try std.testing.expect(!fileExists(io, tmp.dir, ".zit-cache"));
    try std.testing.expect(!fileExists(io, tmp.dir, ".zit-tmp-pack"));
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .undo));
}

test "cli extras B-extras-17: `zit extras >> log` appends, rows to stdout (O_APPEND honored)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "g1");

    // The shell creates `log` (itself now an untracked file) BEFORE extras runs,
    // so the sweep lists both `g1` and `log`; appending must preserve the
    // pre-existing line. ASCII: `g1` < `log`.
    var res = try runShell(gpa, io, tmp.dir, "printf 'PRE-EXISTING-LINE\\n' > log; \"$ZIT\" extras >> log");
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    const log = try readFile(gpa, io, tmp.dir, "log");
    defer gpa.free(log);
    try std.testing.expectEqualStrings("PRE-EXISTING-LINE\ng1\nlog\n", log);
}

test "cli extras B-extras-11: 'extras' is listed in the command help and suggested for a near-miss" {
    // The verb joins the single-source Verb enum, so the command list names it,
    // `zit help extras` routes to the verb's own verbatim help (B-extras-11), and
    // the did-you-mean suggester finds a near-miss.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var help = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &help);
    try expectExit(help.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "extras") != null);

    var topic = try runZit(gpa, io, tmp.dir, &.{ "help", "extras" });
    defer freeResult(gpa, &topic);
    try expectExit(topic.term, 0);
    try std.testing.expectEqualStrings(extras_help_expected, topic.stdout);

    var typo = try runZit(gpa, io, tmp.dir, &.{"extraz"});
    defer freeResult(gpa, &typo);
    try expectExit(typo.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, typo.stderr, "did you mean 'extras'?") != null);
}

// ==========================================================================
// `zit clean` (contracts/clean.md, B-clean-1..18). The destructive companion to
// `extras`: it consumes the SAME sweep unchanged and adds deletion, gated behind
// `--force`. Each test drives the real binary; expected bytes come from the
// clause text and the settled help/refusal strings, never from running fossil.
// ==========================================================================

/// The verbatim `zit clean` help — an independent transcription of `main.zig`'s
/// `clean_help` (B-clean-12/cli-help-verbatim-stdout-exit-0), so a drift between
/// the two is caught here.
const clean_help_expected =
    \\usage: zit clean [-n|--dry-run] [-f|--force] [--emptydirs] [--dotfiles]
    \\
    \\Delete every file in the checkout that zit is not tracking — the same set
    \\`zit extras` lists. This is destructive and cannot be undone: `zit undo` does
    \\not recover a clean.
    \\
    \\By default clean refuses and deletes nothing: it reports how many untracked
    \\files would be deleted and stops. Pass --force to delete them, or --dry-run to
    \\preview the exact list first. clean never prompts.
    \\
    \\Files matching the `clean-glob` setting are the exception: the setting has
    \\already declared them disposable, so they are deleted without --force and the
    \\run says how many went. `keep-glob` wins over `clean-glob`, and --dry-run
    \\still deletes nothing.
    \\
    \\Tracked files and zit's control files are never touched, and names beginning
    \\with a dot are left alone unless --dotfiles is given. Only files are deleted;
    \\directories left empty stay unless --emptydirs is given, and a directory the
    \\`empty-dirs` setting names is never removed.
    \\
    \\options:
    \\  -n, --dry-run list what would be deleted, one path per line; delete nothing
    \\  -f, --force   delete the untracked files
    \\      --emptydirs
    \\                also remove the directories the deletions leave empty
    \\      --dotfiles
    \\                also consider names beginning with a dot (also settable
    \\                with `zit settings dotfiles on`)
    \\  -h, --help    show this help and exit
    \\
    \\If both --dry-run and --force are given, --dry-run wins and nothing is deleted.
    \\
    \\exit status:
    \\  0  nothing to delete, a --dry-run preview, or every file deleted
    \\  1  refused (no --force), a file could not be deleted, or not a zit repository
    \\  2  usage error (an unknown option or an unexpected argument)
    \\
;

/// The verbatim B-clean-1 refusal, naming a plural count of extras.
const clean_refuse_three =
    "zit clean: 3 untracked files would be deleted; refusing without --force\n" ++
    "run 'zit clean --dry-run' to preview, or 'zit clean --force' to delete\n";

test "cli clean B-clean-1: no flags with extras present refuses, exit 1, names the count, deletes nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    for ([_][]const u8{ "zebra", "apple", "mango" }) |name| try touch(io, tmp.dir, name);

    var res = try runZit(gpa, io, tmp.dir, &.{"clean"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    // The gate: stdout stays empty; the two-line refusal (count + pointers) is on
    // stderr; and every extra is still on disk (nothing was deleted).
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(clean_refuse_three, res.stderr);
    for ([_][]const u8{ "zebra", "apple", "mango" }) |name|
        try std.testing.expect(fileExists(io, tmp.dir, name));
}

test "trust: a versioned before-commit is inert until accepted, and an edit revokes consent" {
    // The security property the whole design exists for, driven through the real
    // binary. Git refuses to let hooks travel at all (hence husky/lefthook);
    // fossil lets them travel AND executes them, with a boundary that happens to
    // depend on the transport. Splitting declaration from consent gives the
    // project its checks and this machine its veto.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "a\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});
    var c0 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c0);
    try expectExitDiag(c0.term, 0, c0);

    // A tracked declaration — the shape that clones with the repository.
    try tmp.dir.createDir(io, ".zit-settings", .default_dir);
    try writeFile(io, tmp.dir, ".zit-settings/before-commit", "echo ran-the-check\n");
    try addManaged(gpa, io, tmp.dir, &.{".zit-settings/before-commit"});

    // 1. Untrusted: the commit is REFUSED, not warned about. A warning would
    //    let an operator believe they were gated when they were not.
    try writeFile(io, tmp.dir, "f.txt", "b\n");
    var refused = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &refused);
    try expectExit(refused.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, refused.stderr, "has not been accepted here") != null);
    // The command is shown, because that is what consent would be given TO.
    try std.testing.expect(std.mem.indexOf(u8, refused.stderr, "echo ran-the-check") != null);

    // 2. Bare `zit trust` SHOWS and grants nothing: consent to something you
    //    have not been shown is not consent.
    var shown = try runZit(gpa, io, tmp.dir, &.{"trust"});
    defer freeResult(gpa, &shown);
    try expectExitDiag(shown.term, 0, shown);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "echo ran-the-check") != null);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "NOT trusted") != null);
    var still = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &still);
    try expectExit(still.term, 1); // showing did not grant

    // 3. Accepted: the check runs and the commit proceeds.
    var granted = try runZit(gpa, io, tmp.dir, &.{ "trust", "--yes" });
    defer freeResult(gpa, &granted);
    try expectExitDiag(granted.term, 0, granted);
    var ok = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &ok);
    try expectExitDiag(ok.term, 0, ok);
    try std.testing.expect(std.mem.indexOf(u8, ok.stdout, "ran-the-check") != null);

    // 4. THE PROPERTY: an edited declaration revokes consent by itself, because
    //    the record pins CONTENT. This is the pulled-poison case, and the
    //    replacement command must not execute -- proven by the sentinel file it
    //    would have written.
    try writeFile(io, tmp.dir, ".zit-settings/before-commit", "echo poisoned > pwned.txt\n");
    try writeFile(io, tmp.dir, "f.txt", "c\n");
    var changed = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "three" });
    defer freeResult(gpa, &changed);
    try expectExit(changed.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, changed.stderr, "CHANGED since you accepted it") != null);
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "pwned.txt", .{}));

    // 5. A failing check aborts the commit and mints nothing.
    try writeFile(io, tmp.dir, ".zit-settings/before-commit", "exit 3\n");
    var regrant = try runZit(gpa, io, tmp.dir, &.{ "trust", "--yes" });
    defer freeResult(gpa, &regrant);
    try expectExitDiag(regrant.term, 0, regrant);
    var failed = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "four" });
    defer freeResult(gpa, &failed);
    try expectExit(failed.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, failed.stderr, "nothing was committed") != null);

    // 6. --no-checks skips, and says which command it skipped. Loud on purpose:
    //    withholding an escape hatch does not prevent bypass, it makes people
    //    delete the declaration, which is permanent and invisible.
    var skipped = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "four", "--no-checks" });
    defer freeResult(gpa, &skipped);
    try expectExitDiag(skipped.term, 0, skipped);
    try std.testing.expect(std.mem.indexOf(u8, skipped.stderr, "SKIPPING before-commit") != null);

    // 7. Revoking puts it back to refusing.
    var revoked = try runZit(gpa, io, tmp.dir, &.{ "trust", "--revoke" });
    defer freeResult(gpa, &revoked);
    try expectExitDiag(revoked.term, 0, revoked);
    try writeFile(io, tmp.dir, "f.txt", "d\n");
    var after = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "five" });
    defer freeResult(gpa, &after);
    try expectExit(after.term, 1);
}

test "trust: a locally set before-commit runs without any trust record" {
    // Writing the command IS the consent, so the tier decides: a value this
    // operator put in their own repository or global file cannot have been
    // supplied by a peer, and asking them to trust their own typing would be
    // ceremony rather than safety.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "a\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});
    var c0 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c0);
    try expectExitDiag(c0.term, 0, c0);

    var set = try runZit(gpa, io, tmp.dir, &.{ "settings", "before-commit", "echo local-check" });
    defer freeResult(gpa, &set);
    try expectExitDiag(set.term, 0, set);

    var shown = try runZit(gpa, io, tmp.dir, &.{"trust"});
    defer freeResult(gpa, &shown);
    try expectExitDiag(shown.term, 0, shown);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "no trust record needed") != null);

    try writeFile(io, tmp.dir, "f.txt", "b\n");
    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);
    try std.testing.expect(std.mem.indexOf(u8, c.stdout, "local-check") != null);
}

test "stat: a BARE store answers project-code under -R, with no checkout, port, or child process" {
    // The blocker this issue names: a repository's project code was obtainable
    // only by speaking the sync protocol to a server you started yourself.
    // A consumer needing one constant had to find a free port, spawn `zit
    // serve`, poll for readiness, POST a clone container and scrape the push
    // card -- a subprocess and a listening socket per repository, with a
    // five-second worst case at startup.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "wt", .default_dir);
    var wt = try tmp.dir.openDir(io, "wt", .{});
    defer wt.close(io);
    try initRepo(gpa, io, wt);
    try writeFile(io, wt, "f.txt", "x\n");
    try addManaged(gpa, io, wt, &.{"f.txt"});
    var c = try runCommitEnv(gpa, io, wt, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);

    // The code as the checkout reports it, to compare against.
    var in_wt = try runZit(gpa, io, wt, &.{"stat"});
    defer freeResult(gpa, &in_wt);
    try expectExitDiag(in_wt.term, 0, in_wt);
    const pc_line = "project-code ";
    const at = std.mem.indexOf(u8, in_wt.stdout, pc_line).?;
    const end = std.mem.indexOfScalarPos(u8, in_wt.stdout, at, '\n').?;
    const code = in_wt.stdout[at + pc_line.len .. end];
    try std.testing.expect(code.len == 40 or code.len == 64);

    // A BARE store: a directory holding `.zit` and nothing else -- no checkout
    // record, which is exactly what a forge serves.
    try tmp.dir.createDir(io, "bare", .default_dir);
    var bare = try tmp.dir.openDir(io, "bare", .{});
    defer bare.close(io);
    const store_bytes = try wt.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(store_bytes);
    try writeFile(io, bare, ".zit", store_bytes);

    var bare_stat = try runZit(gpa, io, tmp.dir, &.{ "stat", "-R", "bare" });
    defer freeResult(gpa, &bare_stat);
    try expectExitDiag(bare_stat.term, 0, bare_stat);
    // Same identity, read without a working tree.
    const b_at = std.mem.indexOf(u8, bare_stat.stdout, pc_line).?;
    const b_end = std.mem.indexOfScalarPos(u8, bare_stat.stdout, b_at, '\n').?;
    try std.testing.expectEqualStrings(code, bare_stat.stdout[b_at + pc_line.len .. b_end]);
    try std.testing.expect(std.mem.indexOf(u8, bare_stat.stdout, "hash-policy ") != null);
    // And the census still answers, so one read serves both questions.
    try std.testing.expect(std.mem.indexOf(u8, bare_stat.stdout, "artifacts ") != null);
}

test "cli: the warm image and a cold derive resolve the branch-walk forms identically" {
    // The regression this pins is a SILENT one. `branchValueOf` read tagxref
    // directly, and tagxref is empty on an image backing -- so every check-in
    // looked like it was on `main_branch`, the §8.4 walk never left the branch,
    // and `start:`/`root:` answered with a check-in from somewhere else
    // entirely. Exit 0, no diagnostic, wrong listing.
    //
    // Compared against `--no-cache`, which forces the full-log derive: the two
    // paths must agree byte for byte. Edge equality alone does not catch this
    // (the edges were right; the branch identity read beside them was not), so
    // it is asserted here through the real binary rather than in a unit view.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    // A real second branch, so the walk has somewhere to stop. Without one
    // every rid shares a branch and the comparison proves nothing.
    try writeFile(io, tmp.dir, "b.txt", "two\n");
    try addManaged(gpa, io, tmp.dir, &.{"b.txt"});
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "side", "--branch", "feature" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);

    try writeFile(io, tmp.dir, "c.txt", "three\n");
    try addManaged(gpa, io, tmp.dir, &.{"c.txt"});
    var c3 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "more" });
    defer freeResult(gpa, &c3);
    try expectExit(c3.term, 0);

    // Warm the cache so the first comparison runs against a real image rather
    // than against a derive that happens to write one.
    var warm = try runZit(gpa, io, tmp.dir, &.{ "ls", "tip" });
    defer freeResult(gpa, &warm);
    try expectExitDiag(warm.term, 0, warm);

    for ([_][]const u8{ "tip", "feature", "start:feature", "root:feature", "trunk", "start:trunk" }) |sel| {
        var img = try runZit(gpa, io, tmp.dir, &.{ "ls", sel });
        defer freeResult(gpa, &img);
        var derived = try runZit(gpa, io, tmp.dir, &.{ "ls", sel, "--no-cache" });
        defer freeResult(gpa, &derived);
        try expectExitDiag(img.term, 0, img);
        try expectExitDiag(derived.term, 0, derived);
        // Both the listing AND that it is not empty: two empty listings agree
        // for the wrong reason, which is how the original defect passed.
        try std.testing.expect(img.stdout.len > 0);
        try std.testing.expectEqualStrings(derived.stdout, img.stdout);

        // `info` reports the resolved check-in itself, so a walk landing on the
        // wrong rid shows up here even when the two file listings coincide.
        var i_img = try runZit(gpa, io, tmp.dir, &.{ "info", sel });
        defer freeResult(gpa, &i_img);
        var i_der = try runZit(gpa, io, tmp.dir, &.{ "info", sel, "--no-cache" });
        defer freeResult(gpa, &i_der);
        try expectExitDiag(i_img.term, 0, i_img);
        try std.testing.expectEqualStrings(i_der.stdout, i_img.stdout);
    }
}

test "cli -R: a bad --repo path says which mistake was made, not 'no checkout here'" {
    // The message for a DISCOVERY failure tells the operator to name the
    // repository with -R. Printed for a `-R` failure it advises supplying the
    // flag that was already supplied, and it reads identically whether the
    // path was mistyped or simply holds no repository -- so a stale path in a
    // script looks exactly like a bug in the verb. Both were reported
    // against `tarball`; both affected every `-R` verb.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "x\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});
    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);

    // A path that does not exist names itself, and does not advise -R.
    var missing = try runZit(gpa, io, tmp.dir, &.{ "ls", "-R", "does-not-exist", "tip" });
    defer freeResult(gpa, &missing);
    try expectExit(missing.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, missing.stderr, "no such repository") != null);
    try std.testing.expect(std.mem.indexOf(u8, missing.stderr, "does-not-exist") != null);
    try std.testing.expect(std.mem.indexOf(u8, missing.stderr, "name it with -R") == null);

    // A real directory holding no repository is the OTHER mistake, and says so
    // differently — conflating the two is what cost the reporter time.
    try tmp.dir.createDir(io, "empty", .default_dir);
    var bare = try runZit(gpa, io, tmp.dir, &.{ "ls", "-R", "empty", "tip" });
    defer freeResult(gpa, &bare);
    try expectExit(bare.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, bare.stderr, "no repository at") != null);
    try std.testing.expect(std.mem.indexOf(u8, bare.stderr, "no .zit there") != null);

    // Without -R the discovery message is unchanged: it is correct there, and
    // it is what tells someone in the wrong directory what to do.
    var tmp_empty = std.testing.tmpDir(.{});
    defer tmp_empty.cleanup();
    var discovered = try runZit(gpa, io, tmp_empty.dir, &.{ "ls", "tip" });
    defer freeResult(gpa, &discovered);
    try expectExit(discovered.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, discovered.stderr, "no zit checkout here") != null);

    // The archive verbs this was filed against, from a directory that is not a
    // checkout: the primary complaint, pinned so it cannot regress.
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    const store_bytes = try tmp.dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(store_bytes);
    try away.dir.createDir(io, "store", .default_dir);
    var store_dir = try away.dir.openDir(io, "store", .{});
    defer store_dir.close(io);
    try writeFile(io, store_dir, ".zit", store_bytes);
    for ([_][]const u8{ "tarball", "zip" }) |verb| {
        var r = try runZit(gpa, io, away.dir, &.{ verb, "-R", "store", "tip", "--list" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "f.txt") != null);
    }
}

test "stat: the no-checkout diagnostic no longer claims .zit is missing when it is present" {
    // The second defect this issue flags. Discovery requires a CHECKOUT, but the
    // message said "no .zit found" -- in a directory where `.zit` was sitting
    // right there. An operator reading it had every reason to suspect a corrupt
    // repository rather than a missing working tree.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "wt", .default_dir);
    var wt = try tmp.dir.openDir(io, "wt", .{});
    defer wt.close(io);
    try initRepo(gpa, io, wt);
    const store_bytes = try wt.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(store_bytes);
    try tmp.dir.createDir(io, "bare", .default_dir);
    var bare = try tmp.dir.openDir(io, "bare", .{});
    defer bare.close(io);
    try writeFile(io, bare, ".zit", store_bytes);

    var st = try runZit(gpa, io, bare, &.{"status"});
    defer freeResult(gpa, &st);
    try expectExit(st.term, 1);
    // It must not assert the absent thing is `.zit`, because `.zit` is here.
    try std.testing.expect(std.mem.indexOf(u8, st.stderr, "no .zit found") == null);
    try std.testing.expect(std.mem.indexOf(u8, st.stderr, ".zit-checkout") != null);
    // And it names the remedy that actually works from here.
    try std.testing.expect(std.mem.indexOf(u8, st.stderr, "-R") != null);
}

test "settings keep-glob: clean never deletes a kept file, extras still lists it" {
    // The distinction that justifies a second glob list. `ignore-glob` removes a
    // file from the extras set entirely; `keep-glob` leaves it visible and only
    // makes it undeletable — "keep telling me this is here, never remove it",
    // for local config and scratch.
    //
    // It carries more weight in zit than in fossil: fossil's `clean` is undoable
    // by default and zit's is not, so this list is the only thing between a
    // mistyped glob and permanent loss.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "t.txt", "tracked\n");
    try addManaged(gpa, io, tmp.dir, &.{"t.txt"});
    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);

    try writeFile(io, tmp.dir, ".env", "SECRET=1\n");
    try writeFile(io, tmp.dir, "junk.tmp", "junk\n");

    var set = try runZit(gpa, io, tmp.dir, &.{ "settings", "keep-glob", ".env" });
    defer freeResult(gpa, &set);
    try expectExitDiag(set.term, 0, set);

    // extras is UNAFFECTED: a kept file is still an extra.
    var ex = try runZit(gpa, io, tmp.dir, &.{ "extras", "--dotfiles" });
    defer freeResult(gpa, &ex);
    try expectExitDiag(ex.term, 0, ex);
    try std.testing.expect(std.mem.indexOf(u8, ex.stdout, ".env") != null);
    try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "junk.tmp") != null);

    // The preview omits the kept file and says how many it protected.
    var dry = try runZit(gpa, io, tmp.dir, &.{ "clean", "--dry-run", "--dotfiles" });
    defer freeResult(gpa, &dry);
    try expectExitDiag(dry.term, 0, dry);
    try std.testing.expect(std.mem.indexOf(u8, dry.stdout, "junk.tmp") != null);
    try std.testing.expect(std.mem.indexOf(u8, dry.stdout, ".env") == null);
    try std.testing.expect(std.mem.indexOf(u8, dry.stderr, "kept by keep-glob") != null);

    // And the sweep agrees with the preview -- the property that matters, since
    // a file previewed as safe but deleted here is unrecoverable.
    var force = try runZit(gpa, io, tmp.dir, &.{ "clean", "--force", "--dotfiles" });
    defer freeResult(gpa, &force);
    try expectExitDiag(force.term, 0, force);
    try std.testing.expect(tmp.dir.statFile(io, ".env", .{}) != error.FileNotFound);
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "junk.tmp", .{}));

    // A tracked .zit-settings/keep-glob overrides the stored value, which is how
    // the protection travels to everyone working on the project.
    try tmp.dir.createDir(io, ".zit-settings", .default_dir);
    try writeFile(io, tmp.dir, ".zit-settings/keep-glob", "*.tmp\n");
    try writeFile(io, tmp.dir, "junk.tmp", "back\n");
    try writeFile(io, tmp.dir, ".env", "SECRET=1\n");
    var dry2 = try runZit(gpa, io, tmp.dir, &.{ "clean", "--dry-run", "--dotfiles" });
    defer freeResult(gpa, &dry2);
    try expectExitDiag(dry2.term, 0, dry2);
    // The versioned list protects *.tmp and says nothing about .env, so the
    // repository's own value is fully replaced, not merged.
    try std.testing.expect(std.mem.indexOf(u8, dry2.stdout, "junk.tmp") == null);
    try std.testing.expect(std.mem.indexOf(u8, dry2.stdout, ".env") != null);
}

test "cli clean B-clean-2: the refusal is non-interactive — stdin closed behaves identically" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    for ([_][]const u8{ "zebra", "apple", "mango" }) |name| try touch(io, tmp.dir, name);

    // stdin redirected from /dev/null: clean never reads it, never prompts — the
    // output is byte-identical to the plain B-clean-1 run.
    var res = try runShell(gpa, io, tmp.dir, "\"$ZIT\" clean < /dev/null");
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(clean_refuse_three, res.stderr);
    for ([_][]const u8{ "zebra", "apple", "mango" }) |name|
        try std.testing.expect(fileExists(io, tmp.dir, name));
}

test "cli clean B-clean-3: --dry-run previews to stdout, exit 0, leaving the tree byte-identical" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    for ([_][]const u8{ "zebra", "apple", "mango" }) |name| try touch(io, tmp.dir, name);

    var res = try runZit(gpa, io, tmp.dir, &.{ "clean", "--dry-run" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("apple\nmango\nzebra\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    // --dry-run deletes nothing (B-clean-3): every file is still present.
    for ([_][]const u8{ "zebra", "apple", "mango" }) |name|
        try std.testing.expect(fileExists(io, tmp.dir, name));
}

test "cli clean B-clean-4: the --dry-run preview is byte-identical to `zit extras`" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    for ([_][]const u8{ "zebra", "apple", "mango" }) |name| try touch(io, tmp.dir, name);

    var dry = try runZit(gpa, io, tmp.dir, &.{ "clean", "--dry-run" });
    defer freeResult(gpa, &dry);
    var ext = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &ext);
    try expectExit(dry.term, 0);
    try expectExit(ext.term, 0);
    // Same paths, same order, same form — clean and extras share the ONE sweep.
    try std.testing.expectEqualStrings(ext.stdout, dry.stdout);
    try std.testing.expectEqualStrings("apple\nmango\nzebra\n", dry.stdout);
}

test "cli clean B-clean-5: --force deletes exactly the extras set, exit 0, tracked and control survive" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "keep");
    try touch(io, tmp.dir, "loose1");
    try touch(io, tmp.dir, "loose2");
    try addManaged(gpa, io, tmp.dir, &.{"keep"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "clean", "--force" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // Full success is silent on both streams (cli-stream-separation).
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    // Exactly the extras set is gone; the tracked file and every control entry
    // remain — untouchable by construction (B-clean-7).
    try std.testing.expect(!fileExists(io, tmp.dir, "loose1"));
    try std.testing.expect(!fileExists(io, tmp.dir, "loose2"));
    try std.testing.expect(fileExists(io, tmp.dir, "keep"));
    try std.testing.expect(fileExists(io, tmp.dir, ".zit"));
    try std.testing.expect(fileExists(io, tmp.dir, ".zit-checkout"));
    try std.testing.expect(sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli clean B-clean-6: --force is idempotent — a subsequent extras lists nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "one");
    try touch(io, tmp.dir, "two");

    var first = try runZit(gpa, io, tmp.dir, &.{ "clean", "--force" });
    defer freeResult(gpa, &first);
    try expectExit(first.term, 0);

    // The extras that were deleted are gone and nothing tracked/control became an
    // extra: extras (and a re-run clean --dry-run) over the now-clean tree is empty.
    var ext = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &ext);
    try expectExit(ext.term, 0);
    try std.testing.expectEqualStrings("", ext.stdout);

    var again = try runZit(gpa, io, tmp.dir, &.{ "clean", "--dry-run" });
    defer freeResult(gpa, &again);
    try expectExit(again.term, 0);
    try std.testing.expectEqualStrings("", again.stdout);
}

test "cli clean B-clean-7: a tracked file and every control entry survive a --force byte-for-byte" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    {
        var f = try tmp.dir.createFile(io, "tracked", .{});
        defer f.close(io);
        try f.writePositionalAll(io, "PAYLOAD\n", 0);
    }
    try addManaged(gpa, io, tmp.dir, &.{"tracked"});
    try touch(io, tmp.dir, "extra");

    var res = try runZit(gpa, io, tmp.dir, &.{ "clean", "--force" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expect(!fileExists(io, tmp.dir, "extra"));
    // The tracked file's content is unchanged, and the object store / checkout
    // record / managed list are all intact — clean never re-checks membership.
    const body = try readFile(gpa, io, tmp.dir, "tracked");
    defer gpa.free(body);
    try std.testing.expectEqualStrings("PAYLOAD\n", body);
    try std.testing.expect(fileExists(io, tmp.dir, ".zit"));
    try std.testing.expect(fileExists(io, tmp.dir, ".zit-checkout"));
    try std.testing.expect(sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli clean B-clean-8: an empty extras set is a success no-op for both --force and un-forced" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir); // only the control files exist — no extras

    const invocations = [_][]const []const u8{
        &.{ "clean", "--force" },
        &.{"clean"},
    };
    for (invocations) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        // Emptiness is a success, never a refusal: no diagnostic, no count.
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
}

test "cli clean B-clean-9: partial failure continues siblings, reports the failure, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "a");
    try tmp.dir.createDirPath(io, "sub");
    try touch(io, tmp.dir, "sub/stuck");

    // Make `sub` non-writable so unlinking `sub/stuck` fails with EACCES, while
    // the sweep can still read `sub` (r-x). `a` (sorted first) deletes cleanly.
    try tmp.dir.setFilePermissions(io, "sub", File.Permissions.fromMode(0o555), .{ .follow_symlinks = false });
    defer tmp.dir.setFilePermissions(io, "sub", File.Permissions.fromMode(0o777), .{ .follow_symlinks = false }) catch {};

    var res = try runZit(gpa, io, tmp.dir, &.{ "clean", "--force" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit clean: 'sub/stuck': could not delete: permission denied\n",
        res.stderr,
    );

    // Restore write access before the readback so traversal (and cleanup) is
    // unhindered: the deletable sibling `a` is gone, the undeletable one remains.
    try tmp.dir.setFilePermissions(io, "sub", File.Permissions.fromMode(0o777), .{ .follow_symlinks = false });
    try std.testing.expect(!fileExists(io, tmp.dir, "a"));
    try std.testing.expect(fileExists(io, tmp.dir, "sub/stuck"));
}

test "cli clean B-clean-10: exit is 0 on full success and exactly 1 on a partial failure, never 2" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // All deletions succeed → exit 0.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try touch(io, tmp.dir, "x");
        var ok = try runZit(gpa, io, tmp.dir, &.{ "clean", "--force" });
        defer freeResult(gpa, &ok);
        try expectExit(ok.term, 0);
    }

    // At least one deletion fails → exit exactly 1 (operation class, not usage 2).
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try tmp.dir.createDirPath(io, "sub");
        try touch(io, tmp.dir, "sub/stuck");
        try tmp.dir.setFilePermissions(io, "sub", File.Permissions.fromMode(0o555), .{ .follow_symlinks = false });
        defer tmp.dir.setFilePermissions(io, "sub", File.Permissions.fromMode(0o777), .{ .follow_symlinks = false }) catch {};

        var bad = try runZit(gpa, io, tmp.dir, &.{ "clean", "--force" });
        defer freeResult(gpa, &bad);
        try expectExit(bad.term, 1);

        try tmp.dir.setFilePermissions(io, "sub", File.Permissions.fromMode(0o777), .{ .follow_symlinks = false });
    }
}

test "cli clean B-clean-11: --dry-run wins over --force — preview, exit 0, nothing deleted" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    for ([_][]const u8{ "zebra", "apple", "mango" }) |name| try touch(io, tmp.dir, name);

    const invocations = [_][]const []const u8{
        &.{ "clean", "--dry-run", "--force" },
        &.{ "clean", "--force", "--dry-run" },
    };
    for (invocations) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        // --dry-run is the safety override: it can never be upgraded into a delete.
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("apple\nmango\nzebra\n", res.stdout);
        for ([_][]const u8{ "zebra", "apple", "mango" }) |name|
            try std.testing.expect(fileExists(io, tmp.dir, name));
    }
}

test "cli clean B-clean-12: -h/--help print verbatim help to stdout, exit 0, help wins and deletes nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "extra");

    // Help wins over a trailing invalid token, an unknown option, and --force.
    const invocations = [_][]const []const u8{
        &.{ "clean", "--help" },
        &.{ "clean", "-h" },
        &.{ "clean", "--help", "foo" },
        &.{ "clean", "-h", "--all" },
        &.{ "clean", "--force", "--help" },
    };
    for (invocations) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(clean_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
        // Help never sweeps and never deletes (B-clean-12).
        try std.testing.expect(fileExists(io, tmp.dir, "extra"));
    }
}

test "cli clean B-clean-13: an unknown option is a usage error, exit 2, stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // `-f` and `-n` are the --force/--dry-run aliases; these are not options.
    inline for (.{ "--all", "-0", "-x", "--emptydir" }) |flag| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "clean", flag });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        const expected = try std.fmt.allocPrint(
            gpa,
            "zit clean: unknown option '{s}'\ntry 'zit clean --help'\n",
            .{flag},
        );
        defer gpa.free(expected);
        try std.testing.expectEqualStrings(expected, res.stderr);
    }
}

test "cli clean B-clean-14: a positional or a token after -- is a usage error, exit 2, stderr" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const Case = struct { argv: []const []const u8, token: []const u8 };
    const cases = [_]Case{
        .{ .argv = &.{ "clean", "foo" }, .token = "foo" },
        .{ .argv = &.{ "clean", "--", "foo" }, .token = "foo" },
        .{ .argv = &.{ "clean", "--", "--force" }, .token = "--force" },
    };
    for (cases) |c| {
        var res = try runZit(gpa, io, tmp.dir, c.argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        const expected = try std.fmt.allocPrint(
            gpa,
            "zit clean: unexpected argument '{s}'\ntry 'zit clean --help'\n",
            .{c.token},
        );
        defer gpa.free(expected);
        try std.testing.expectEqualStrings(expected, res.stderr);
    }
}

test "cli clean B-clean-15: outside a repository, exit 1, not_a_repo on stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try touch(io, tmp.dir, "a"); // a file, but no repo anywhere above

    // The repository check precedes any sweep, regardless of --force/--dry-run.
    const invocations = [_][]const []const u8{
        &.{"clean"},
        &.{ "clean", "--force" },
        &.{ "clean", "--dry-run" },
    };
    for (invocations) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings(
            "zit: no zit checkout here (looked for .zit-checkout in this directory and its parents)\n" ++
                "if this is a bare repository, name it with -R, or run 'zit open' to create a working tree\n",
            res.stderr,
        );
    }
}

test "cli clean B-clean-16: a corrupt .zit-managed fails cleanly BEFORE any deletion, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "extra");
    try corruptSection(io, tmp.dir, .managed);

    // Even with --force, a tracked set that cannot be read is a tree clean must
    // not touch: nothing is deleted (the extra is still on disk).
    var res = try runZit(gpa, io, tmp.dir, &.{ "clean", "--force" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit clean: cannot read tracked files: .zit-checkout is corrupt\n",
        res.stderr,
    );
    try std.testing.expect(fileExists(io, tmp.dir, "extra"));
}

test "cli clean B-clean-17: `zit clean --dry-run >> log` appends, rows to stdout (O_APPEND honored)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "g1");

    // The shell creates `log` (itself now an untracked file) BEFORE clean runs, so
    // the preview lists both `g1` and `log`; appending must preserve the
    // pre-existing line. ASCII: `g1` < `log`.
    var res = try runShell(gpa, io, tmp.dir, "printf 'PRE-EXISTING-LINE\\n' > log; \"$ZIT\" clean --dry-run >> log");
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    const log = try readFile(gpa, io, tmp.dir, "log");
    defer gpa.free(log);
    try std.testing.expectEqualStrings("PRE-EXISTING-LINE\ng1\nlog\n", log);
}

test "cli clean B-clean-18: over a deep nested tree --dry-run equals extras and --force deletes that exact set" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDirPath(io, "x/y/z");
    try touch(io, tmp.dir, "x/y/z/deep");
    try touch(io, tmp.dir, "top");

    // The ONE shared sweep, bounds included: preview equals extras and equals the
    // derived ascending listing ("top" < "x/y/z/deep").
    var ext = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &ext);
    var dry = try runZit(gpa, io, tmp.dir, &.{ "clean", "--dry-run" });
    defer freeResult(gpa, &dry);
    try expectExit(dry.term, 0);
    try std.testing.expectEqualStrings(ext.stdout, dry.stdout);
    try std.testing.expectEqualStrings("top\nx/y/z/deep\n", dry.stdout);

    // --force deletes exactly that set; a subsequent extras lists nothing.
    var force = try runZit(gpa, io, tmp.dir, &.{ "clean", "--force" });
    defer freeResult(gpa, &force);
    try expectExit(force.term, 0);
    try std.testing.expect(!fileExists(io, tmp.dir, "top"));
    try std.testing.expect(!fileExists(io, tmp.dir, "x/y/z/deep"));

    var after = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &after);
    try expectExit(after.term, 0);
    try std.testing.expectEqualStrings("", after.stdout);
}

test "cli clean B-clean-12: 'clean' is listed in the command help and suggested for a near-miss" {
    // The verb joins the single-source Verb enum, so the command list names it,
    // `zit help clean` routes to the verb's own verbatim help (B-clean-12), and
    // the did-you-mean suggester finds a near-miss.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var help = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &help);
    try expectExit(help.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "clean") != null);

    var topic = try runZit(gpa, io, tmp.dir, &.{ "help", "clean" });
    defer freeResult(gpa, &topic);
    try expectExit(topic.term, 0);
    try std.testing.expectEqualStrings(clean_help_expected, topic.stdout);

    var typo = try runZit(gpa, io, tmp.dir, &.{"cleen"});
    defer freeResult(gpa, &typo);
    try expectExit(typo.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, typo.stderr, "did you mean 'clean'?") != null);
}

// ==========================================================================
// `zit addremove` (contracts/addremove.md, B-addremove-1..13). One scan, two
// verdicts: every untracked file the SAME `extras.zig` sweep reports is ADDED,
// every tracked file missing from disk is REMOVED — fossil/hg parity (git spells
// it `add -A`). Mutates `.zit-managed` only; working files are never touched.
// Each test drives the real binary; expected bytes come from the clause text and
// the settled help/report strings, never from running fossil.
// ==========================================================================

/// The verbatim `zit addremove` help — an independent transcription of
/// `main.zig`'s `addremove_help` (B-addremove-9/cli-help-verbatim-stdout-exit-0),
/// so a drift between the two is caught here.
const addremove_help_expected =
    \\usage: zit addremove [--dry-run] [--dotfiles]
    \\
    \\Reconcile the tracked set with the working tree in one pass: mark every
    \\untracked file to include in your next check-in, and stop tracking every
    \\file you deleted from disk. This is the set `zit extras` lists, added, plus
    \\the tracked files now missing, removed — fossil and hg spell it `addremove`;
    \\git spells it `add -A`.
    \\
    \\addremove changes only what zit tracks; it never edits, creates, or deletes
    \\a working file, and the next `zit commit` records the result. Each action is
    \\reported, sorted and relative to the checkout root:
    \\
    \\  ADDED <path>     a newly tracked file
    \\  REMOVED <path>   a tracked file no longer on disk
    \\
    \\A tracked file that is merely modified is left alone (content does not affect
    \\membership), and zit's control files are never tracked. Names beginning with
    \\a dot are skipped unless --dotfiles is given. With nothing to do, the output
    \\is empty.
    \\
    \\options:
    \\  --dry-run     report what would change, one action per line; change nothing
    \\  --dotfiles    also track names beginning with a dot (also settable with
    \\                `zit settings dotfiles on`)
    \\  -h, --help    show this help and exit
    \\
    \\exit status:
    \\  0  the tracked set was reconciled (with or without changes), or a --dry-run preview
    \\  1  a path could not be tracked, or this is not a zit repository
    \\  2  usage error (an unknown option or an unexpected argument)
    \\
;

test "cli addremove B-addremove-1: adds two untracked, removes one missing, sorted report, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // Track `gone` then delete it (a missing-managed removal); leave two untracked
    // files in reverse-of-sorted order (an ADD each).
    try touch(io, tmp.dir, "gone");
    try addManaged(gpa, io, tmp.dir, &.{"gone"});
    try tmp.dir.deleteFile(io, "gone");
    try touch(io, tmp.dir, "zebra");
    try touch(io, tmp.dir, "apple");

    var res = try runZit(gpa, io, tmp.dir, &.{"addremove"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // ADDED lines (sorted ascending) then the REMOVED line; diagnostics-free.
    try std.testing.expectEqualStrings("ADDED apple\nADDED zebra\nREMOVED gone\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    // The on-disk oracle: the set is exactly the two adds (gone forgotten).
    try expectManaged(gpa, io, tmp.dir, &.{ "apple", "zebra" });

    // `zit status` then shows the transitions and `zit extras` shows nothing.
    var st = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &st);
    try expectExit(st.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "ADDED      apple") != null);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "ADDED      zebra") != null);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "gone") == null);

    var ext = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &ext);
    try expectExit(ext.term, 0);
    try std.testing.expectEqualStrings("", ext.stdout);
}

test "cli addremove B-addremove-2: the ADDED paths are `zit extras` minus symlinks — same sweep, subdirs included, control files excluded" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // A nested file, a symlink (listed, never followed), and two root files. The
    // control artifacts `.zit`/`.zit-checkout` are on disk but never swept.
    try tmp.dir.createDirPath(io, "sub/dir");
    try touch(io, tmp.dir, "sub/dir/name");
    try tmp.dir.symLink(io, "target", "link", .{});
    try touch(io, tmp.dir, "zebra");
    try touch(io, tmp.dir, "apple");

    // Capture what extras prints at this instant (the add-set oracle).
    var ext = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &ext);
    try expectExit(ext.term, 0);
    try std.testing.expectEqualStrings("apple\nlink\nsub/dir/name\nzebra\n", ext.stdout);

    var res = try runZit(gpa, io, tmp.dir, &.{"addremove"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // The ADDED paths are the extras set MINUS symlinks (an `ADDED ` prefix per
    // line, no REMOVED lines — nothing was tracked to go missing). Still ONE
    // sweep: a second walker would diverge on the subdir or the control names.
    // `link` is absent since F-B3 — 0.1.0 does not version symlinks, and
    // tracking one here is exactly what wedged `commit` (blocker B3). The
    // withholding is silent: addremove's report is a ledger of transitions, and
    // a skipped symlink is a non-transition (`zit extras` still shows it).
    try std.testing.expectEqualStrings(
        "ADDED apple\nADDED sub/dir/name\nADDED zebra\n",
        res.stdout,
    );
    try std.testing.expectEqualStrings("", res.stderr);
    // The three regular files are tracked, the symlink is not, and no control
    // name leaked in.
    try expectManaged(gpa, io, tmp.dir, &.{ "apple", "sub/dir/name", "zebra" });
    // And it is still an extra — withheld from tracking, not hidden.
    var ext2 = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &ext2);
    try expectExit(ext2.term, 0);
    try std.testing.expectEqualStrings("link\n", ext2.stdout);
}

test "cli addremove B-addremove-3: the rm set is the missing-managed set; a present-but-modified file is never removed or re-added" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // `keep` is tracked and stays on disk but is MODIFIED in place; `gone` is
    // tracked and deleted. Only `gone` is a removal; `keep` is untouched.
    try writeFile(io, tmp.dir, "keep", "original");
    try touch(io, tmp.dir, "gone");
    try addManaged(gpa, io, tmp.dir, &.{ "keep", "gone" });
    try writeFile(io, tmp.dir, "keep", "changed contents");
    try tmp.dir.deleteFile(io, "gone");

    var res = try runZit(gpa, io, tmp.dir, &.{"addremove"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // Exactly one removal; `keep` is neither removed nor re-added (content is
    // irrelevant to membership).
    try std.testing.expectEqualStrings("REMOVED gone\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{"keep"});
    // The modified working file is byte-identical after the run (never touched).
    const keep_bytes = try tmp.dir.readFileAlloc(io, "keep", gpa, .limited(1 << 16));
    defer gpa.free(keep_bytes);
    try std.testing.expectEqualStrings("changed contents", keep_bytes);
}

test "cli addremove B-addremove-4: --dry-run previews exactly and leaves .zit-managed byte-unchanged" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "gone");
    try addManaged(gpa, io, tmp.dir, &.{"gone"});
    try tmp.dir.deleteFile(io, "gone");
    try touch(io, tmp.dir, "zebra");
    try touch(io, tmp.dir, "apple");

    // The exact `.zit-managed` bytes before the dry run.
    const before = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(before);

    var dry = try runZit(gpa, io, tmp.dir, &.{ "addremove", "--dry-run" });
    defer freeResult(gpa, &dry);
    try expectExit(dry.term, 0);
    try std.testing.expectEqualStrings("ADDED apple\nADDED zebra\nREMOVED gone\n", dry.stdout);
    try std.testing.expectEqualStrings("", dry.stderr);

    // `.zit-managed` is byte-identical — the dry run persisted nothing.
    const after = try readManagedImage(gpa, io, tmp.dir);
    defer gpa.free(after);
    try std.testing.expectEqualStrings(before, after);

    // Dry-run output equals the real run's report on the same tree.
    var real = try runZit(gpa, io, tmp.dir, &.{"addremove"});
    defer freeResult(gpa, &real);
    try expectExit(real.term, 0);
    try std.testing.expectEqualStrings(dry.stdout, real.stdout);
}

test "cli addremove B-addremove-5: a clean tree (no extras, no missing) prints nothing and exits 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // One tracked file that is present, nothing untracked: nothing to do.
    try touch(io, tmp.dir, "keep");
    try addManaged(gpa, io, tmp.dir, &.{"keep"});

    var res = try runZit(gpa, io, tmp.dir, &.{"addremove"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{"keep"});
}

test "cli addremove B-addremove-6/-8: a control-looking file is never added and the working tree is byte-identical afterward" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // `.zit-cache` is a never-tracked control name (B-add-7 set): present on disk
    // but the sweep excludes it, so addremove never tracks it — the sibling `real`
    // is still added, and the run succeeds.
    try touch(io, tmp.dir, ".zit-cache");
    try writeFile(io, tmp.dir, "real", "payload");

    var res = try runZit(gpa, io, tmp.dir, &.{"addremove"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("ADDED real\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{"real"});

    // B-addremove-8: working files are NEVER touched — every file still present
    // and byte-identical (no deletion, no write).
    try std.testing.expect(fileExists(io, tmp.dir, ".zit-cache"));
    const real_bytes = try tmp.dir.readFileAlloc(io, "real", gpa, .limited(1 << 16));
    defer gpa.free(real_bytes);
    try std.testing.expectEqualStrings("payload", real_bytes);
}

test "cli addremove B-addremove-8: a deleted-managed file is forgotten, never recreated on disk" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "gone");
    try addManaged(gpa, io, tmp.dir, &.{"gone"});
    try tmp.dir.deleteFile(io, "gone");

    var res = try runZit(gpa, io, tmp.dir, &.{"addremove"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("REMOVED gone\n", res.stdout);
    // addremove touches only `.zit-managed`: the deleted file is NOT resurrected.
    try std.testing.expect(!fileExists(io, tmp.dir, "gone"));
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli addremove B-addremove-9: -h and --help print verbatim help to stdout, exit 0, help wins over a trailing token" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const invocations = [_][]const []const u8{
        &.{ "addremove", "--help" },
        &.{ "addremove", "-h" },
        &.{ "addremove", "--help", "foo" },
        &.{ "addremove", "-h", "--dry-run" },
    };
    for (invocations) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(addremove_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
}

test "cli addremove B-addremove-10: an unknown option or a positional is a usage error, exit 2, stderr, empty stdout, no mutation" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // Pre-state: a missing-managed file that a real run WOULD remove — so the
    // "no mutation" assertion has teeth.
    try touch(io, tmp.dir, "gone");
    try addManaged(gpa, io, tmp.dir, &.{"gone"});
    try tmp.dir.deleteFile(io, "gone");
    try touch(io, tmp.dir, "extra");

    const cases = [_]struct { argv: []const []const u8, tok: []const u8, kind: []const u8 }{
        .{ .argv = &.{ "addremove", "--force" }, .tok = "--force", .kind = "unknown option" },
        .{ .argv = &.{ "addremove", "-n" }, .tok = "-n", .kind = "unknown option" },
        .{ .argv = &.{ "addremove", "foo" }, .tok = "foo", .kind = "unexpected argument" },
        .{ .argv = &.{ "addremove", "--", "foo" }, .tok = "foo", .kind = "unexpected argument" },
    };
    for (cases) |c| {
        var res = try runZit(gpa, io, tmp.dir, c.argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        const expected = try std.fmt.allocPrint(
            gpa,
            "zit addremove: {s} '{s}'\ntry 'zit addremove --help'\n",
            .{ c.kind, c.tok },
        );
        defer gpa.free(expected);
        try std.testing.expectEqualStrings(expected, res.stderr);
    }
    // No usage error mutated the set: `gone` is still tracked, `extra` still not.
    try expectManaged(gpa, io, tmp.dir, &.{"gone"});
}

test "cli addremove B-addremove-11: outside a repository, exit 1, not_a_repo on stderr, empty stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try touch(io, tmp.dir, "a"); // a file, but no repo anywhere above

    const invocations = [_][]const []const u8{
        &.{"addremove"},
        &.{ "addremove", "--dry-run" },
    };
    for (invocations) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expectEqualStrings(
            "zit: no zit checkout here (looked for .zit-checkout in this directory and its parents)\n" ++
                "if this is a bare repository, name it with -R, or run 'zit open' to create a working tree\n",
            res.stderr,
        );
    }
}

test "cli addremove B-addremove-12: action lines to stdout, diagnostics to stderr, no interleaving under redirection" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "gone");
    try addManaged(gpa, io, tmp.dir, &.{"gone"});
    try tmp.dir.deleteFile(io, "gone");
    try touch(io, tmp.dir, "apple");

    // The child's stdout and stderr are dup2'd onto independent OS pipes (the same
    // fd split any shell redirection uses). Only the ADDED/REMOVED report reaches
    // stdout; stderr is empty on a clean run — proving no interleaving. An in-tree
    // `> file` cannot be used here: the redirect target would itself be swept as an
    // extra and pollute the report.
    var res = try runZit(gpa, io, tmp.dir, &.{"addremove"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("ADDED apple\nREMOVED gone\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli addremove B-addremove-13: 'addremove' is listed in the command help, routes to its own help, and is suggested for a near-miss" {
    // The verb joins the single-source Verb enum, so the command list names it,
    // `zit help addremove` routes to the verb's own verbatim help, and the
    // did-you-mean suggester finds a near-miss.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var help = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &help);
    try expectExit(help.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "addremove") != null);

    var topic = try runZit(gpa, io, tmp.dir, &.{ "help", "addremove" });
    defer freeResult(gpa, &topic);
    try expectExit(topic.term, 0);
    try std.testing.expectEqualStrings(addremove_help_expected, topic.stdout);

    var typo = try runZit(gpa, io, tmp.dir, &.{"addremvoe"});
    defer freeResult(gpa, &typo);
    try expectExit(typo.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, typo.stderr, "did you mean 'addremove'?") != null);
}

test "cli add B-extras-5: 'zit add .zit-cache' and 'zit add .zit-tmp-pack' are refused as control files" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // The two transient artifacts join the never-tracked set: add/rm delegate to
    // the same predicate `extras` uses, so the boundary cannot drift.
    try touch(io, tmp.dir, ".zit-cache");
    try touch(io, tmp.dir, ".zit-tmp-pack");

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", ".zit-cache", ".zit-tmp-pack" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit add: '.zit-cache' is a zit control file and cannot be tracked\n" ++
            "zit add: '.zit-tmp-pack' is a zit control file and cannot be tracked\n",
        res.stderr,
    );
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

// --- amend (contracts/amend.md B-amend-1..17) -------------------------------

/// An independent transcription of `main.zig`'s `amend_help` block (must match
/// byte-for-byte; drift fails B-amend-13).
const amend_help_expected =
    \\zit amend NAME [--comment TEXT] [--user NAME] [--date ISO] [--branch NAME]
    \\
    \\Attach display overrides to a check-in by minting one control artifact.
    \\History is never rewritten; the original check-in is untouched.
    \\
    \\  --comment TEXT   override the displayed comment
    \\  --user NAME      override the displayed author
    \\  --date ISO       override the displayed date (YYYY-MM-DDTHH:MM:SS)
    \\  --branch NAME    move the check-in (and its descendants) to branch NAME
    \\  -h, --help       show this help
    \\
    \\At least one override flag is required.
    \\
    \\exit status:
    \\  0  success
    \\  1  operational failure (not a repository, unreadable state, a refused
    \\     action) — the diagnostic on stderr names it
    \\  2  usage error (an unknown option or unexpected argument)
    \\
;

/// Extracts the 64-hex control-artifact hash from an `amend <hash>\n` success
/// line, asserting the fixed structure (never a wall clock).
fn amendHash(stdout: []const u8) ![]const u8 {
    const prefix = "amend ";
    try std.testing.expect(std.mem.startsWith(u8, stdout, prefix));
    try std.testing.expect(std.mem.endsWith(u8, stdout, "\n"));
    const hash = stdout[prefix.len .. stdout.len - 1];
    try std.testing.expectEqual(@as(usize, 64), hash.len);
    for (hash) |ch| try std.testing.expect(std.ascii.isHex(ch) and !std.ascii.isUpper(ch));
    return hash;
}

/// Seeds a repository with one root check-in (comment "orig", file "f") and
/// returns its 64-hex hash (caller frees). The shared setup for the amend e2e.
fn seedRootCheckin(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]u8 {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "f", "v1\n");
    try addManaged(gpa, io, dir, &.{"f"});
    var root = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", "orig" });
    defer freeResult(gpa, &root);
    try expectExit(root.term, 0);
    return gpa.dupe(u8, try checkinHash(root.stdout));
}

test "cli amend B-amend-1: --comment mints a control that overrides the displayed comment" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    var am = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "amender" }, &.{ "amend", root_hash, "--comment", "overridden text" });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 0);
    try std.testing.expectEqualStrings("", am.stderr); // B-amend-16 stream discipline
    const control_hash = try gpa.dupe(u8, try amendHash(am.stdout));
    defer gpa.free(control_hash);
    // A NEW, distinct artifact (not the check-in) carries the override.
    try std.testing.expect(!std.mem.eql(u8, control_hash, root_hash));

    // The minted artifact is a CONTROL (§5.2 D,T,U,Z) carrying `+comment` targeted
    // at the check-in, never `*` (§5.4).
    {
        var mc = try parseCheckin(gpa, io, tmp.dir, control_hash);
        defer mc.deinit();
        try std.testing.expectEqual(artifact.ArtifactType.control, mc.m.kind);
        try std.testing.expect(mc.m.d != null and mc.m.u != null and mc.m.z != null);
        try std.testing.expect(mc.m.c == null and mc.m.f.len == 0 and mc.m.p.len == 0);
        var found = false;
        for (mc.m.t) |t| {
            if (t.sign == .add and std.mem.eql(u8, t.name, "comment")) {
                found = true;
                try std.testing.expectEqualStrings("overridden text", t.value.?);
                try std.testing.expect(t.target != null);
                try std.testing.expectEqualStrings(root_hash, t.target.?);
            }
        }
        try std.testing.expect(found);
    }

    // Option B observables: timeline comment column, info `comment:` line, and
    // the raw tag view all reflect the override after crosslink.
    var tl = try runZit(gpa, io, tmp.dir, &.{"timeline"});
    defer freeResult(gpa, &tl);
    try expectExit(tl.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "overridden text") != null);

    var info = try runZit(gpa, io, tmp.dir, &.{ "info", root_hash });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "comment: overridden text") != null);

    var tg = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "--raw", root_hash });
    defer freeResult(gpa, &tg);
    try expectExit(tg.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, tg.stdout, "comment overridden text") != null);
}

test "cli amend B-amend-2: --user/--date surface in info; a malformed --date is a usage error" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    var am = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "amender" }, &.{ "amend", root_hash, "--user", "alice", "--date", "2020-06-07T08:09:10" });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 0);
    _ = try amendHash(am.stdout);

    var info = try runZit(gpa, io, tmp.dir, &.{ "info", root_hash });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "user:") != null);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "alice") != null);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "date:") != null);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "2020-06-07T08:09:10") != null);

    // A malformed --date is rejected (exit 2) before anything is minted (§4.6.4).
    var bad = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "amender" }, &.{ "amend", root_hash, "--date", "2020-06-07" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 2);
    try std.testing.expectEqualStrings("", bad.stdout);
    try std.testing.expect(std.mem.indexOf(u8, bad.stderr, "invalid date") != null);
    try std.testing.expect(std.mem.indexOf(u8, bad.stderr, "try 'zit amend --help'") != null);
}

test "cli amend B-amend-3: --branch moves the check-in to the new branch (repo-state §6.2)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    // A later mtime (higher epoch) so the branch override supersedes the commit's
    // own `branch=trunk` tag (repo-state §5.3 last-wins).
    var am = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "amender", .epoch = "2000000000" }, &.{ "amend", root_hash, "--branch", "feature" });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 0);
    const control_hash = try gpa.dupe(u8, try amendHash(am.stdout));
    defer gpa.free(control_hash);

    // The §6.2 branch-move tag set, retargeted at the check-in.
    {
        var mc = try parseCheckin(gpa, io, tmp.dir, control_hash);
        defer mc.deinit();
        try std.testing.expect(hasTag(mc.m, .propagate, "branch"));
        try std.testing.expect(hasTag(mc.m, .propagate, "sym-feature"));
        try std.testing.expect(hasTag(mc.m, .cancel, "sym-trunk"));
        for (mc.m.t) |t| try std.testing.expect(t.target != null); // never `*`
    }

    var info = try runZit(gpa, io, tmp.dir, &.{ "info", root_hash });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "branch:  feature") != null);

    var br = try runZit(gpa, io, tmp.dir, &.{"branch"});
    defer freeResult(gpa, &br);
    try expectExit(br.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, br.stdout, "feature") != null);
}

test "cli amend B-amend-4: several flags mint ONE control artifact" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    var am = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "amender", .epoch = "2000000000" }, &.{ "amend", root_hash, "--comment", "c", "--user", "u2", "--branch", "feature" });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 0);
    // Exactly one success line ⇒ one control artifact (never one per flag).
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, am.stdout, "\n"));
    const control_hash = try gpa.dupe(u8, try amendHash(am.stdout));
    defer gpa.free(control_hash);

    var mc = try parseCheckin(gpa, io, tmp.dir, control_hash);
    defer mc.deinit();
    try std.testing.expectEqual(artifact.ArtifactType.control, mc.m.kind);
    // All the overrides ride in this single artifact's T run.
    try std.testing.expect(hasTag(mc.m, .add, "comment"));
    try std.testing.expect(hasTag(mc.m, .add, "user"));
    try std.testing.expect(hasTag(mc.m, .propagate, "branch"));
    try std.testing.expect(hasTag(mc.m, .propagate, "sym-feature"));
}

test "cli amend B-amend-5: history is append-only — the manifest is untouched, no new check-in" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    var am = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "amender" }, &.{ "amend", root_hash, "--comment", "shown" });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 0);

    // The original manifest still parses byte-for-byte the same: same kind, same
    // C-card comment "orig" (the override lives on a separate control artifact).
    {
        var mm = try parseCheckin(gpa, io, tmp.dir, root_hash);
        defer mm.deinit();
        try std.testing.expectEqual(artifact.ArtifactType.manifest, mm.m.kind);
        try std.testing.expectEqualStrings("orig", mm.m.c.?.comment);
    }

    // The timeline gains no new check-in — a control artifact is not a check-in.
    var tl = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &tl);
    try expectExit(tl.term, 0);
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, tl.stdout, "\n"));

    // A subsequent commit on the checkout still behaves — history moved forward.
    try writeFile(io, tmp.dir, "f", "v2\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "next" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
}

test "cli amend B-amend-6: amend is repeatable and last-wins by event mtime" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    // The greater-mtime control wins regardless of ingest order — mint the later
    // one (higher epoch) first, then an earlier one.
    var late = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "amender", .epoch = "2000000000" }, &.{ "amend", root_hash, "--comment", "second" });
    defer freeResult(gpa, &late);
    try expectExit(late.term, 0);
    var early = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "amender", .epoch = "1500000000" }, &.{ "amend", root_hash, "--comment", "first" });
    defer freeResult(gpa, &early);
    try expectExit(early.term, 0);
    // Two distinct control artifacts.
    try std.testing.expect(!std.mem.eql(u8, try amendHash(late.stdout), try amendHash(early.stdout)));

    var info = try runZit(gpa, io, tmp.dir, &.{ "info", root_hash });
    defer freeResult(gpa, &info);
    try expectExit(info.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "comment: second") != null);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "comment: first") == null);
}

test "cli amend B-amend-8: no override flag is a usage error, nothing minted" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    var am = try runZit(gpa, io, tmp.dir, &.{ "amend", root_hash });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 2);
    try std.testing.expectEqualStrings("", am.stdout);
    try std.testing.expect(std.mem.indexOf(u8, am.stderr, "no override given") != null);
    try std.testing.expect(std.mem.indexOf(u8, am.stderr, "try 'zit amend --help'") != null);
}

test "cli amend B-amend-9: an unresolvable NAME fails with exit 1, nothing minted" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    var am = try runZit(gpa, io, tmp.dir, &.{ "amend", "d" ** 64, "--comment", "x" });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 1);
    try std.testing.expectEqualStrings("", am.stdout);
    try std.testing.expect(std.mem.indexOf(u8, am.stderr, "not found") != null);
}

test "cli amend B-amend-10: a NAME that names a control artifact is refused by its type" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    // First amend mints a control artifact; feeding ITS hash back to amend must
    // refuse — T cards target check-ins, not control artifacts.
    var first = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "amender" }, &.{ "amend", root_hash, "--comment", "x" });
    defer freeResult(gpa, &first);
    try expectExit(first.term, 0);
    const control_hash = try gpa.dupe(u8, try amendHash(first.stdout));
    defer gpa.free(control_hash);

    var am = try runZit(gpa, io, tmp.dir, &.{ "amend", control_hash, "--comment", "y" });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 1);
    try std.testing.expectEqualStrings("", am.stdout);
    try std.testing.expect(std.mem.indexOf(u8, am.stderr, "not a check-in") != null);
    try std.testing.expect(std.mem.indexOf(u8, am.stderr, "tag") != null); // §5.1 external name
}

test "cli amend B-amend-11: an empty value and a repeated flag are usage errors" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    var empty = try runZit(gpa, io, tmp.dir, &.{ "amend", root_hash, "--comment", "" });
    defer freeResult(gpa, &empty);
    try expectExit(empty.term, 2);
    try std.testing.expectEqualStrings("", empty.stdout);
    try std.testing.expect(std.mem.indexOf(u8, empty.stderr, "non-empty") != null);

    var rep = try runZit(gpa, io, tmp.dir, &.{ "amend", root_hash, "--comment", "a", "--comment", "b" });
    defer freeResult(gpa, &rep);
    try expectExit(rep.term, 2);
    try std.testing.expectEqualStrings("", rep.stdout);
    try std.testing.expect(std.mem.indexOf(u8, rep.stderr, "more than once") != null);
}

test "cli amend B-amend-12: amend dispatches to the real handler; a surviving reflex still teaches" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    // `zit amend NAME --comment` succeeds — it is a real verb, not the reflex lesson.
    var am = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "amender" }, &.{ "amend", root_hash, "--comment", "real" });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, am.stderr, "is not a zit command") == null);

    // A surviving reflex entry (rebase) still teaches the immutable-history lesson.
    var rb = try runZit(gpa, io, tmp.dir, &.{"rebase"});
    defer freeResult(gpa, &rb);
    try expectExit(rb.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, rb.stderr, "is not a zit command") != null);
}

test "cli amend B-amend-13: -h/--help writes the verbatim help to stdout, exit 0, mints nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    // Help resolves nothing, so it works even outside a repository.
    var h = try runZit(gpa, io, tmp.dir, &.{ "amend", "--help" });
    defer freeResult(gpa, &h);
    try expectExit(h.term, 0);
    try std.testing.expectEqualStrings(amend_help_expected, h.stdout);
    try std.testing.expectEqualStrings("", h.stderr);

    // -h wins over an otherwise-invalid trailing token.
    var h2 = try runZit(gpa, io, tmp.dir, &.{ "amend", "--bogus", "-h" });
    defer freeResult(gpa, &h2);
    try expectExit(h2.term, 0);
    try std.testing.expectEqualStrings(amend_help_expected, h2.stdout);
}

test "cli amend B-amend-14/16: usage taxonomy and stream discipline" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const root_hash = try seedRootCheckin(gpa, io, tmp.dir);
    defer gpa.free(root_hash);

    // A missing NAME.
    var no_name = try runZit(gpa, io, tmp.dir, &.{"amend"});
    defer freeResult(gpa, &no_name);
    try expectExit(no_name.term, 2);
    try std.testing.expectEqualStrings("", no_name.stdout);
    try std.testing.expect(std.mem.indexOf(u8, no_name.stderr, "no name given") != null);

    // An unknown option.
    var unknown = try runZit(gpa, io, tmp.dir, &.{ "amend", root_hash, "--nope" });
    defer freeResult(gpa, &unknown);
    try expectExit(unknown.term, 2);
    try std.testing.expectEqualStrings("", unknown.stdout);
    try std.testing.expect(std.mem.indexOf(u8, unknown.stderr, "unknown option '--nope'") != null);

    // A second positional.
    var extra = try runZit(gpa, io, tmp.dir, &.{ "amend", root_hash, "--comment", "x", "also" });
    defer freeResult(gpa, &extra);
    try expectExit(extra.term, 2);
    try std.testing.expectEqualStrings("", extra.stdout);
    try std.testing.expect(std.mem.indexOf(u8, extra.stderr, "unexpected argument 'also'") != null);
}

test "cli amend B-amend-15: outside any repository, exit 1 with the standard diagnostic" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    // No `zit init` here.
    var am = try runZit(gpa, io, tmp.dir, &.{ "amend", "abc", "--comment", "x" });
    defer freeResult(gpa, &am);
    try expectExit(am.term, 1);
    try std.testing.expectEqualStrings("", am.stdout);
    try std.testing.expect(std.mem.indexOf(u8, am.stderr, "no zit checkout here") != null);
}

test "cli amend B-amend-17: `zit help amend` teaches amend's help; the command list names it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var h = try runZit(gpa, io, tmp.dir, &.{ "help", "amend" });
    defer freeResult(gpa, &h);
    try expectExit(h.term, 0);
    try std.testing.expectEqualStrings(amend_help_expected, h.stdout);

    var top = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &top);
    try expectExit(top.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, top.stdout, "amend") != null);
}

// --- bisect (contracts/bisect.md B-bisect-1..20) ------------------------------

/// Commits the current tree at a caller-chosen `epoch` (SOURCE_DATE_EPOCH), so a
/// bisect chain has strictly increasing, deterministic event mtimes — the probe
/// selection orders by ascending mtime, so distinct epochs pin every midpoint.
fn bisectCommit(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, epoch: []const u8, msg: []const u8) ![]const u8 {
    var res = try runCommitEnv(gpa, io, dir, .{ .epoch = epoch }, &.{ "commit", "-m", msg });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const h = try checkinHash(res.stdout);
    return gpa.dupe(u8, h);
}

test "cli bisect B-bisect-1/-3/-4/-7/-8: a full linear bisection narrows, names the culprit, resets" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // A five-check-in chain c0<-c1<-c2<-c3<-c4 with strictly increasing mtimes.
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "v0\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const c0 = try bisectCommit(gpa, io, tmp.dir, "1000000000", "c0");
    defer gpa.free(c0);
    try writeFile(io, tmp.dir, "poem", "v1\n");
    const c1 = try bisectCommit(gpa, io, tmp.dir, "1000000100", "c1");
    defer gpa.free(c1);
    try writeFile(io, tmp.dir, "poem", "v2\n");
    const c2 = try bisectCommit(gpa, io, tmp.dir, "1000000200", "defect lands here");
    defer gpa.free(c2);
    try writeFile(io, tmp.dir, "poem", "v3\n");
    const c3 = try bisectCommit(gpa, io, tmp.dir, "1000000300", "c3");
    defer gpa.free(c3);
    try writeFile(io, tmp.dir, "poem", "v4\n");
    const c4 = try bisectCommit(gpa, io, tmp.dir, "1000000400", "c4");
    defer gpa.free(c4);
    // The checkout is at c4 (the head) — the bisection's restore point.

    // B-bisect-1 first half: a lone bad is recorded WITHOUT moving the checkout.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "bad", c4 });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, c4[0..12]) != null);
        try std.testing.expectEqualStrings("", r.stderr);
        try std.testing.expect(sectionExists(gpa, io, tmp.dir, .bisect));
        try expectCheckoutVersion(io, tmp.dir, c4);
    }
    // B-bisect-1 second half: good completes the span and moves to the probe c2.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "good", c0 });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, c2[0..12]) != null);
        try std.testing.expectEqualStrings("", r.stderr);
        try expectCheckoutVersion(io, tmp.dir, c2);
    }
    // B-bisect-7: status reports the probe WITHOUT moving; stream to stdout.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "status" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, c2[0..12]) != null);
        try std.testing.expectEqualStrings("", r.stderr);
        try expectCheckoutVersion(io, tmp.dir, c2);
    }
    // next re-announces the current probe (idempotent), checkout unchanged.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "next" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, c2[0..12]) != null);
        try expectCheckoutVersion(io, tmp.dir, c2);
    }
    // B-bisect-3: mark the probe bad → the span narrows and moves to c1.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "bad", c2 });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try expectCheckoutVersion(io, tmp.dir, c1);
    }
    // B-bisect-4: mark c1 good → span exhausted; culprit c2 named with its
    // comment on stdout; the checkout is moved to the culprit.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "good", c1 });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "first bad check-in") != null);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, c2[0..12]) != null);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "defect lands here") != null);
        try std.testing.expectEqualStrings("", r.stderr);
        try expectCheckoutVersion(io, tmp.dir, c2);
    }
    // B-bisect-8: reset restores the starting check-in (c4) and clears the state.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "reset" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try expectCheckoutVersion(io, tmp.dir, c4);
        try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .bisect));
    }
    // B-bisect-7: status with no session is exit 1, nothing on stdout.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "status" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 1);
        try std.testing.expectEqualStrings("", r.stdout);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "no bisect in progress") != null);
    }
}

test "cli bisect B-bisect-16/-17: help to stdout, and the usage-error taxonomy" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // B-bisect-16: -h writes the help block to stdout, exit 0, stderr empty.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "-h" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "usage: zit bisect") != null);
        try std.testing.expectEqualStrings("", r.stderr);
    }
    // B-bisect-17: each offense is exit 2, nothing on stdout, a stderr diagnostic.
    const bad_cases = [_][]const []const u8{
        &.{"bisect"}, // no subcommand
        &.{ "bisect", "frobnicate" }, // unknown subcommand
        &.{ "bisect", "bad" }, // missing NAME
        &.{ "bisect", "good" }, // missing NAME
        &.{ "bisect", "next", "extra" }, // extra argument
        &.{ "bisect", "status", "extra" }, // extra argument
        &.{ "bisect", "--wat" }, // unknown option
    };
    for (bad_cases) |argv| {
        var r = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &r);
        try expectExit(r.term, 2);
        try std.testing.expectEqualStrings("", r.stdout);
        try std.testing.expect(r.stderr.len != 0);
    }
}

test "cli bisect B-bisect-13/-18: no-session refusals and run outside a repository" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // B-bisect-18: outside any repository, every subcommand is the standard
    // not-a-zit-repository refusal on stderr, exit 1, stdout empty.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "status" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 1);
        try std.testing.expectEqualStrings("", r.stdout);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "no zit checkout here") != null);
    }

    try initRepo(gpa, io, tmp.dir);
    // B-bisect-13: next / reset with no session refuse exit 1.
    for ([_][]const u8{ "next", "reset" }) |sub| {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", sub });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 1);
        try std.testing.expectEqualStrings("", r.stdout);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "no bisect in progress") != null);
    }

    // B-bisect-13: next with only one endpoint marked names what is missing.
    try writeFile(io, tmp.dir, "poem", "v0\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const c0 = try bisectCommit(gpa, io, tmp.dir, "1000000000", "c0");
    defer gpa.free(c0);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "bad", c0 });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 0);
    }
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "next" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "good") != null);
    }
}

test "cli bisect B-bisect-9/-10: dirty-tree and inverted-marks refusals mutate nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Inverted marks: bad is an ancestor of good → refuse, state unchanged.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "poem", "v0\n");
        try addManaged(gpa, io, tmp.dir, &.{"poem"});
        const c0 = try bisectCommit(gpa, io, tmp.dir, "1000000000", "c0");
        defer gpa.free(c0);
        try writeFile(io, tmp.dir, "poem", "v1\n");
        const c1 = try bisectCommit(gpa, io, tmp.dir, "1000000100", "c1");
        defer gpa.free(c1);
        // checkout at c1.
        {
            var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "bad", c0 });
            defer freeResult(gpa, &r);
            try expectExit(r.term, 0);
        }
        {
            var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "good", c1 });
            defer freeResult(gpa, &r);
            try expectExit(r.term, 1);
            try std.testing.expectEqualStrings("", r.stdout);
            try std.testing.expect(std.mem.indexOf(u8, r.stderr, "inverted") != null);
            try expectCheckoutVersion(io, tmp.dir, c1); // no move
        }
        // State unchanged: only the bad endpoint is recorded (good not persisted).
        {
            var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "status" });
            defer freeResult(gpa, &r);
            try expectExit(r.term, 0);
            try std.testing.expect(std.mem.indexOf(u8, r.stdout, c0[0..12]) != null);
            try std.testing.expect(std.mem.indexOf(u8, r.stdout, "mark a good") != null);
        }
    }

    // Dirty working tree: a move-completing mark refuses, checkout unchanged.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try initRepo(gpa, io, tmp.dir);
        try writeFile(io, tmp.dir, "poem", "v0\n");
        try addManaged(gpa, io, tmp.dir, &.{"poem"});
        const c0 = try bisectCommit(gpa, io, tmp.dir, "1000000000", "c0");
        defer gpa.free(c0);
        try writeFile(io, tmp.dir, "poem", "v1\n");
        const c1 = try bisectCommit(gpa, io, tmp.dir, "1000000100", "c1");
        defer gpa.free(c1);
        try writeFile(io, tmp.dir, "poem", "v2\n");
        const c2 = try bisectCommit(gpa, io, tmp.dir, "1000000200", "c2");
        defer gpa.free(c2);
        // checkout at c2.
        {
            var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "bad", c2 });
            defer freeResult(gpa, &r);
            try expectExit(r.term, 0);
        }
        // Dirty the tree, then complete the span → refuse over the dirty tree.
        try writeFile(io, tmp.dir, "poem", "uncommitted edit\n");
        {
            var r = try runZit(gpa, io, tmp.dir, &.{ "bisect", "good", c0 });
            defer freeResult(gpa, &r);
            try expectExit(r.term, 1);
            try std.testing.expectEqualStrings("", r.stdout);
            try std.testing.expect(std.mem.indexOf(u8, r.stderr, "not clean") != null);
            try expectCheckoutVersion(io, tmp.dir, c2); // no move
        }
    }
}

// ===========================================================================
// `zit annotate PATH` (contracts/annotate.md B-annotate-1..16). Per-line
// provenance of one tracked file at the current check-out, driven through the
// real binary over real multi-commit histories (a linear chain and a merge
// DAG built by commit → update → commit → merge → commit).
// ===========================================================================

/// The verbatim `zit annotate` help — must match `main.zig`'s `annotate_help`
/// byte-for-byte (drift fails B-annotate-12).
const annotate_help_expected =
    \\usage: zit annotate PATH [VERSION] [--full] [-n N] [-l]
    \\
    \\Show, for each line of PATH, the check-in that introduced that line and
    \\when. PATH is an exact repo-relative path to a tracked file; each output
    \\line is the introducing check-in's 12-character artifact-ID prefix, the
    \\ISO date it was recorded (UTC), then the file line.
    \\
    \\VERSION selects the annotated check-in the same way 'zit info' does; with
    \\it omitted, the current checkout is annotated. With -R and a VERSION no
    \\working tree is required.
    \\
    \\Provenance is computed over the file's whole version history: a line is
    \\attributed to the oldest check-in that carries it. A line that arrived from
    \\a merged branch is credited to its origin on that branch, never to the merge
    \\check-in itself. Only committed content is annotated — uncommitted edits in
    \\the working file are not shown.
    \\
    \\options:
    \\  -n, --limit N look back at most N check-ins; a line older than the
    \\                window is credited to the oldest check-in shown
    \\  -l, --log     list the check-ins consulted (newest first) before
    \\                the lines, separated by a '---' line
    \\      --full    emit the full artifact ID per line instead of the
    \\                10-character prefix (stable as an index key)
    \\  -R, --repo <path>
    \\                read this repository instead of the one found from the
    \\                current directory; <path> is the directory holding '.zit'
    \\                (or the '.zit' file itself). No working tree is required.
    \\  -h, --help    show this help and exit
    \\
    \\exit status:
    \\  0  the annotation was written
    \\  1  PATH is not tracked at the annotated check-in, its content is
    \\     binary, VERSION does not resolve, or this is not a zit repository
    \\  2  usage error (a missing PATH, an unknown option, or a third positional)
    \\
    \\aliases: blame, praise
    \\
;

/// Commits the working tree at a pinned epoch/author and returns the new
/// check-in's 64-hex hash on `gpa` (caller frees). The epoch fixes both the
/// event mtime (deterministic chain order) and the displayed ISO date.
fn annotateCommit(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    epoch: []const u8,
    msg: []const u8,
) ![]const u8 {
    var res = try runCommitEnv(gpa, io, dir, .{ .user = "u", .epoch = epoch }, &.{ "commit", "-m", msg });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    return gpa.dupe(u8, try checkinHash(res.stdout));
}

/// `annotateCommit` with `--allow-fork` — the annotate fixtures' second-head
/// commits (the fork guard refuses a deliberate fork without it).
fn annotateForkCommit(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    epoch: []const u8,
    msg: []const u8,
) ![]const u8 {
    var res = try runCommitEnv(gpa, io, dir, .{ .user = "u", .epoch = epoch }, &.{ "commit", "-m", msg, "--allow-fork" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    return gpa.dupe(u8, try checkinHash(res.stdout));
}

test "cli annotate B-annotate-1/-2/-5: linear add/edit/append history, oldest-origin per line, deterministic" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const h0 = try annotateCommit(gpa, io, tmp.dir, "1000000000", "root"); // 2001-09-09
    defer gpa.free(h0);
    try writeFile(io, tmp.dir, "poem", "A\nB1\nC\n");
    const h1 = try annotateCommit(gpa, io, tmp.dir, "1100000000", "edit"); // 2004-11-09
    defer gpa.free(h1);
    try writeFile(io, tmp.dir, "poem", "A\nB1\nC\nD\n");
    const h2 = try annotateCommit(gpa, io, tmp.dir, "1200000000", "append"); // 2008-01-10
    defer gpa.free(h2);

    // A,C unchanged since root → h0 ; B1 introduced at h1 ; D appended at h2.
    const expected = try std.fmt.allocPrint(
        gpa,
        "{s} 2001-09-09     1: A\n{s} 2004-11-09     2: B1\n{s} 2001-09-09     3: C\n{s} 2008-01-10     4: D\n",
        .{ h0[0..12], h1[0..12], h0[0..12], h2[0..12] },
    );
    defer gpa.free(expected);

    var res = try runZit(gpa, io, tmp.dir, &.{ "annotate", "poem" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr); // B-annotate-15 stream discipline

    // B-annotate-5: a second run is byte-identical.
    var res2 = try runZit(gpa, io, tmp.dir, &.{ "annotate", "poem" });
    defer freeResult(gpa, &res2);
    try std.testing.expectEqualStrings(res.stdout, res2.stdout);
}

test "cli annotate B-annotate-11: uncommitted working-tree edits are invisible" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const h0 = try annotateCommit(gpa, io, tmp.dir, "1000000000", "root");
    defer gpa.free(h0);

    const expected = try std.fmt.allocPrint(
        gpa,
        "{s} 2001-09-09     1: A\n{s} 2001-09-09     2: B\n{s} 2001-09-09     3: C\n",
        .{ h0[0..12], h0[0..12], h0[0..12] },
    );
    defer gpa.free(expected);

    // Edit the working file WITHOUT committing — annotate reads checked-in bytes.
    try writeFile(io, tmp.dir, "poem", "totally\ndifferent\nuncommitted\ncontent\n");
    var res = try runZit(gpa, io, tmp.dir, &.{ "annotate", "poem" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(expected, res.stdout);
}

test "cli annotate B-annotate-3/-4: merge pass-through and merge-only resolution lines" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // base A,B,C. current edits C→C1 (primary side). other edits A→A1 (merge
    // side). Merge is clean (disjoint), then a resolution edit inserts R (in no
    // parent). Expected: A1→other, B→base(oldest), R→merge, C1→current.
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const base = try annotateCommit(gpa, io, tmp.dir, "1000000000", "base"); // 2001-09-09
    defer gpa.free(base);

    try writeFile(io, tmp.dir, "poem", "A\nB\nC1\n");
    const current = try annotateCommit(gpa, io, tmp.dir, "1100000000", "current"); // 2004-11-09
    defer gpa.free(current);

    var up = try runZit(gpa, io, tmp.dir, &.{ "update", base });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);

    try writeFile(io, tmp.dir, "poem", "A1\nB\nC\n");
    const other = try annotateForkCommit(gpa, io, tmp.dir, "1200000000", "other"); // 2008-01-10
    defer gpa.free(other);

    var back = try runZit(gpa, io, tmp.dir, &.{ "update", current });
    defer freeResult(gpa, &back);
    try expectExit(back.term, 0);

    var merged = try runZit(gpa, io, tmp.dir, &.{ "merge", other });
    defer freeResult(gpa, &merged);
    try expectExit(merged.term, 0); // clean, disjoint edits

    // A resolution edit present in NEITHER parent: R.
    try writeFile(io, tmp.dir, "poem", "A1\nB\nR\nC1\n");
    const merge_ci = try annotateCommit(gpa, io, tmp.dir, "1300000000", "merge"); // 2011-03-13
    defer gpa.free(merge_ci);

    const expected = try std.fmt.allocPrint(
        gpa,
        "{s} 2008-01-10     1: A1\n{s} 2001-09-09     2: B\n{s} 2011-03-13     3: R\n{s} 2004-11-09     4: C1\n",
        .{ other[0..12], base[0..12], merge_ci[0..12], current[0..12] },
    );
    defer gpa.free(expected);

    var res = try runZit(gpa, io, tmp.dir, &.{ "annotate", "poem" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli annotate topology: a child with an EARLIER mtime than its parent still attributes by ancestry" {
    // Non-monotone clocks: the child commit is BACKDATED before its parent. The
    // shared line L was introduced by the parent and must stay charged to the
    // parent (older by topology), not the too-recent child. The old mtime-ascending
    // chain order processed the child first and misattributed L to the child.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "L\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const parent = try annotateCommit(gpa, io, tmp.dir, "1200000000", "parent"); // 2008-01-10, LATER
    defer gpa.free(parent);

    try writeFile(io, tmp.dir, "poem", "L\nM\n");
    const child = try annotateCommit(gpa, io, tmp.dir, "1000000000", "child"); // 2001-09-09, EARLIER
    defer gpa.free(child);

    // L → parent (older by topology, despite its LATER mtime) ; M → child.
    const expected = try std.fmt.allocPrint(
        gpa,
        "{s} 2008-01-10     1: L\n{s} 2001-09-09     2: M\n",
        .{ parent[0..12], child[0..12] },
    );
    defer gpa.free(expected);

    var res = try runZit(gpa, io, tmp.dir, &.{ "annotate", "poem" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli annotate topology: a merge BACKDATED before its parents credits the primary parent's line, not the merge" {
    // Both branch tips independently add the identical line X; the merge keeps it.
    // The merge check-in is BACKDATED before every parent (non-monotone). Attribution
    // must pass X through the PRIMARY parent (current), never charge it to the merge.
    // The old mtime-ascending order processed the earliest-mtime merge FIRST — before
    // its parents were resolved — and wrongly charged X (and A) to the merge itself.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const base = try annotateCommit(gpa, io, tmp.dir, "1000000000", "base"); // 2001-09-09
    defer gpa.free(base);

    // Primary branch inserts X.
    try writeFile(io, tmp.dir, "poem", "A\nX\nB\n");
    const current = try annotateCommit(gpa, io, tmp.dir, "1200000000", "current"); // 2008-01-10
    defer gpa.free(current);

    // Other branch independently inserts the identical X, plus a distinct Z (so the
    // merge actually changes the working tree — otherwise there is nothing to commit).
    var up = try runZit(gpa, io, tmp.dir, &.{ "update", base });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);
    try writeFile(io, tmp.dir, "poem", "A\nX\nB\nZ\n");
    const other = try annotateForkCommit(gpa, io, tmp.dir, "1300000000", "other"); // 2011-03-13
    defer gpa.free(other);

    // Merge other into current, then commit the merge BACKDATED before all parents.
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", current });
    defer freeResult(gpa, &back);
    try expectExit(back.term, 0);
    var merged = try runZit(gpa, io, tmp.dir, &.{ "merge", other });
    defer freeResult(gpa, &merged);
    try expectExit(merged.term, 0); // X agrees on both sides, Z only on other ⇒ clean
    const merge_ci = try annotateCommit(gpa, io, tmp.dir, "500000000", "merge"); // 1985-11-05, EARLIEST
    defer gpa.free(merge_ci);

    // A,B → base ; X → current (the PRIMARY parent, preferred over other) ; Z → other.
    // Nothing is charged to the backdated merge itself.
    const expected = try std.fmt.allocPrint(
        gpa,
        "{s} 2001-09-09     1: A\n{s} 2008-01-10     2: X\n{s} 2001-09-09     3: B\n{s} 2011-03-13     4: Z\n",
        .{ base[0..12], current[0..12], base[0..12], other[0..12] },
    );
    defer gpa.free(expected);

    var res = try runZit(gpa, io, tmp.dir, &.{ "annotate", "poem" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli annotate B-annotate-9: a binary file refuses with exit 1, nothing on stdout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "blob.bin", "before\x00after\n"); // a NUL ⇒ binary
    try addManaged(gpa, io, tmp.dir, &.{"blob.bin"});
    const h0 = try annotateCommit(gpa, io, tmp.dir, "1000000000", "add binary");
    defer gpa.free(h0);

    var res = try runZit(gpa, io, tmp.dir, &.{ "annotate", "blob.bin" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "binary") != null);
}

test "cli annotate B-annotate-10: untracked-but-present vs nonexistent, both exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const h0 = try annotateCommit(gpa, io, tmp.dir, "1000000000", "root");
    defer gpa.free(h0);

    // Present on disk but never added → untracked.
    try writeFile(io, tmp.dir, "scratch.txt", "not added\n");
    var untracked = try runZit(gpa, io, tmp.dir, &.{ "annotate", "scratch.txt" });
    defer freeResult(gpa, &untracked);
    try expectExit(untracked.term, 1);
    try std.testing.expectEqualStrings("", untracked.stdout);
    try std.testing.expect(std.mem.indexOf(u8, untracked.stderr, "not tracked") != null);

    // Neither on disk nor tracked → nonexistent.
    var missing = try runZit(gpa, io, tmp.dir, &.{ "annotate", "ghost.txt" });
    defer freeResult(gpa, &missing);
    try expectExit(missing.term, 1);
    try std.testing.expectEqualStrings("", missing.stdout);
    try std.testing.expect(std.mem.indexOf(u8, missing.stderr, "no such tracked file") != null);
}

test "cli annotate B-annotate-12: -h and --help print the verbatim help, exit 0" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    for ([_][]const u8{ "-h", "--help" }) |flag| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "annotate", flag });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(annotate_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
    // Help wins even over an otherwise-invalid trailing token.
    var win = try runZit(gpa, io, tmp.dir, &.{ "annotate", "--help", "-z" });
    defer freeResult(gpa, &win);
    try expectExit(win.term, 0);
    try std.testing.expectEqualStrings(annotate_help_expected, win.stdout);
}

test "cli annotate B-annotate-13: missing PATH, unknown option, or a third positional is a usage error, exit 2" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const cases = [_][]const []const u8{
        &.{"annotate"}, // missing PATH
        &.{ "annotate", "-x" }, // unknown option
        &.{ "annotate", "a", "b", "c" }, // third positional (the second is VERSION)
    };
    for (cases) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "try 'zit annotate --help'") != null);
    }
}

test "cli annotate B-annotate-14: outside a repository fails with the not-a-repo diagnostic, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{}); // NOT initialized
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{ "annotate", "poem" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no zit checkout here") != null);
}

test "cli annotate B-annotate-16: the top-level help names the annotate verb" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "annotate") != null);
}

// --- open: clone → working tree (P3-F3) ------------------------------------
//
// `zit clone` builds a repository file and nothing else: the checkout it leaves
// behind is the same UNBORN record `init` writes (`version == ""`, branch
// `trunk`), with no managed set and no files. `zit open` is the second half of
// fossil's own `clone` + `open` UX — it resolves the tip of the main branch and
// materializes it through the shared update engine.
//
// The proof chain below is the missing end-to-end the pre-release assessment
// called out: the existing clone e2e asserts artifact-digest-set equality only,
// which proves the wire converged but says nothing about whether a cloned repo
// can become a usable working tree.

const open_help_expected =
    \\usage: zit open [CHECKIN | URL] [--store=PATH] [--manifest[=r,u,t]]
    \\                [--cone=PATH] [--cone-not=PATH] [--cone-file=FILE]
    \\                [-h|--help]
    \\
    \\Materialize a check-in in the working tree of a not-yet-open checkout.
    \\Given a URL instead, this is 'zit clone URL': the repository is cloned
    \\into a directory named from the URL and opened there.
    \\This is the second half of 'zit clone URL DIR': clone builds DIR and its
    \\'.zit', open fills DIR with the files of a check-in. Run it from inside
    \\the checkout directory.
    \\
    \\Without CHECKIN, the tip of this repository's main branch is opened.
    \\CHECKIN accepts every form 'zit help names' lists — IDs, prefixes,
    \\branches, tags, reserved forms, and timestamps — so a fresh clone can
    \\open straight at a release tag without materializing the tip first.
    \\
    \\A working tree that is already open at a check-in is left untouched; use
    \\'zit update' to move an open tree to a different check-in. A repository
    \\with no check-in yet is already in its correct (empty) state, which open
    \\reports and leaves alone — though a CHECKIN named explicitly must
    \\resolve, so in an empty repository it fails as not found.
    \\
    \\--manifest asks this checkout for the files some build systems read to
    \\stamp a version into their output: 'manifest' (the check-in artifact, r),
    \\'manifest.uuid' (its hash, u) and 'manifest.tags' (its symbolic tags, t).
    \\The bare flag or 'on' means 'ru'; 'off' writes none, whatever the
    \\project's 'manifest' setting says. The choice is remembered, so every
    \\later 'zit update' rewrites the files for the check-in it moves to.
    \\Without the flag the project's 'manifest' setting decides, and a file of
    \\one of those names that the check-in itself tracks is never replaced.
    \\
    \\--store=PATH opens a second working tree of an EXISTING repository: run
    \\it in an empty directory and name the checkout (or the '.zit' file) that
    \\holds the store. The new tree keeps its own checkout state but reads and
    \\writes the one shared store, so a commit in either tree is visible to
    \\the other immediately; concurrent commits serialize on the store's own
    \\lock. The directory must not already be a checkout.
    \\
    \\--store on a tree that is ALREADY open re-points it instead: after the
    \\repository moves, run open --store=NEWPATH in the stranded tree and it
    \\resumes where it was, uncommitted work intact. The new PATH must hold
    \\the same repository -- one containing this tree's current check-in --
    \\so a tree cannot be re-pointed at an unrelated store.
    \\
    \\options:
    \\  --store=PATH        create a detached working tree of the repository
    \\                      at PATH before opening
    \\  --manifest[=r,u,t]  write manifest, manifest.uuid, manifest.tags
    \\  --cone=PATH         materialize and track only PATH's subtree
    \\                      (repeatable) — a sparse checkout from the start
    \\  --cone-not=PATH     exclude PATH's subtree from an enclosing --cone
    \\                      member (repeatable); the most specific member
    \\                      decides coverage
    \\  --cone-file=FILE    read cone members from FILE, one per line
    \\                      ('#' comments and blank lines are skipped;
    \\                      a '!' prefix marks an exclusion)
    \\  -h, --help          show this help and exit
    \\
    \\exit status:
    \\  0  success
    \\  1  operational failure (not a repository, unreadable state, a refused
    \\     action) — the diagnostic on stderr names it
    \\  2  usage error (an unknown option or unexpected argument)
    \\
;

test "cli open P3-F3: clone → open → byte-identical tree → update → commit round-trips" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: two check-ins, the second adding a nested path and rewriting the
    // first file — so materializing the TIP is distinguishable from materializing
    // the root, and directory creation is exercised.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);

    try writeFile(io, tmp_a.dir, "hello.txt", "first revision\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    const root = try commitHashDup(gpa, io, tmp_a.dir, "root");
    defer gpa.free(root);

    try writeFile(io, tmp_a.dir, "hello.txt", "second revision, longer than the first\n");
    try tmp_a.dir.createDirPath(io, "sub");
    try writeFile(io, tmp_a.dir, "sub/nested.txt", "nested payload\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "hello.txt", "sub/nested.txt" });
    const tip = try commitHashDup(gpa, io, tmp_a.dir, "tip");
    defer gpa.free(tip);

    // Serve A and clone it into a fresh directory B/repo over real HTTP.
    const open_chain_port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, open_chain_port);
    defer server.kill(io);
    try waitForListen(io, open_chain_port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{open_chain_port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    // The state clone leaves: an UNBORN checkout record and no working files.
    try expectCheckoutVersion(io, b_dir, "");
    try std.testing.expect(!fileExists(io, b_dir, "hello.txt"));

    // OPEN. The tip of trunk is resolved from the cloned repository's own state
    // — `update` takes only literal 40/64-hex IDs, so this resolution is open's
    // own load-bearing work, not something the caller can supply.
    {
        var op = try runZit(gpa, io, b_dir, &.{"open"});
        defer freeResult(gpa, &op);
        try expectExit(op.term, 0);
        try expectNoDiagnostic(op.stderr); // the receipt lives on stderr
        try std.testing.expect(std.mem.indexOf(u8, op.stdout, tip) != null);
        try std.testing.expect(std.mem.indexOf(u8, op.stdout, "trunk") != null);
    }
    try expectCheckoutVersion(io, b_dir, tip);

    // THE PROOF: the files on disk in B byte-match the source tree in A. Digest-
    // set equality (what the clone e2e asserts) cannot see this.
    for ([_][]const u8{ "hello.txt", "sub/nested.txt" }) |path| {
        const a_bytes = try readFile(gpa, io, tmp_a.dir, path);
        defer gpa.free(a_bytes);
        const b_bytes = try readFile(gpa, io, b_dir, path);
        defer gpa.free(b_bytes);
        try std.testing.expectEqualSlices(u8, a_bytes, b_bytes);
    }
    try expectManaged(gpa, io, b_dir, &.{ "hello.txt", "sub/nested.txt" });

    // `update` works in the opened tree: moving back to the root drops the
    // nested file and restores the earlier bytes.
    {
        var up = try runZit(gpa, io, b_dir, &.{ "update", root });
        defer freeResult(gpa, &up);
        try expectExit(up.term, 0);
    }
    try expectCheckoutVersion(io, b_dir, root);
    {
        const bytes = try readFile(gpa, io, b_dir, "hello.txt");
        defer gpa.free(bytes);
        try std.testing.expectEqualStrings("first revision\n", bytes);
    }
    try std.testing.expect(!fileExists(io, b_dir, "sub/nested.txt"));

    // …and back to the tip, so the commit below has the tip as its parent.
    {
        var up = try runZit(gpa, io, b_dir, &.{ "update", "--no-sync", tip });
        defer freeResult(gpa, &up);
        try expectExit(up.term, 0);
    }

    // A `commit` in the opened tree round-trips: it mints a real check-in whose
    // P card names the tip we opened at, and the checkout advances onto it.
    try writeFile(io, b_dir, "hello.txt", "third revision, authored in the cloned tree\n");
    const child = try commitHashDup(gpa, io, b_dir, "from the clone");
    defer gpa.free(child);
    try expectCheckoutVersion(io, b_dir, child);
    var parsed = try parseCheckin(gpa, io, b_dir, child);
    defer parsed.deinit();
    try std.testing.expectEqual(@as(usize, 1), parsed.m.p.len);
    try std.testing.expectEqual(@as(usize, 1), parsed.m.p[0].count);
    try std.testing.expectEqualStrings(tip, parsed.m.p[0].parents.?);
}

test "cli open: an explicit CHECKIN opens at that check-in, resolved like info" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: two check-ins so the root is distinguishable from the tip.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "hello.txt", "first revision\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    const root = try commitHashDup(gpa, io, tmp_a.dir, "root");
    defer gpa.free(root);
    try writeFile(io, tmp_a.dir, "hello.txt", "second revision\n");
    const tip = try commitHashDup(gpa, io, tmp_a.dir, "tip");
    defer gpa.free(tip);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    // A name that resolves to nothing fails and leaves the checkout unborn —
    // no partial materialization on the way to a refusal.
    {
        var bogus = try runZit(gpa, io, b_dir, &.{ "open", "no-such-name" });
        defer freeResult(gpa, &bogus);
        try expectExit(bogus.term, 1);
        try std.testing.expectEqualStrings("", bogus.stdout);
        try std.testing.expectEqualStrings(
            "zit open: check-in 'no-such-name' not found\n",
            bogus.stderr,
        );
        try expectCheckoutVersion(io, b_dir, "");
        try std.testing.expect(!fileExists(io, b_dir, "hello.txt"));
    }

    // Opening at the ROOT by 12-char prefix materializes the root, not the tip
    // — the whole point of the argument: no intermediate tip materialization.
    {
        var op = try runZit(gpa, io, b_dir, &.{ "open", root[0..12] });
        defer freeResult(gpa, &op);
        try expectExit(op.term, 0);
        try std.testing.expectEqualStrings("", op.stderr);
        try std.testing.expect(std.mem.indexOf(u8, op.stdout, root) != null);
    }
    try expectCheckoutVersion(io, b_dir, root);
    const got = try readFile(gpa, io, b_dir, "hello.txt");
    defer gpa.free(got);
    try std.testing.expectEqualStrings("first revision\n", got);
}

test "cli open P3-F3: a plain clone's unborn checkout is a state 'update' tolerates" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // The "interrupted / never-run open" state is EXACTLY what a plain clone
    // produces — an unborn checkout record, no managed set, no files. Setting it
    // up any other way (hand-writing the record) would keep passing even if
    // clone's bootstrap drifted away from what the shared layer tolerates, so
    // the setup here is nothing but `zit clone`.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "only.txt", "recoverable\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"only.txt"});
    const tip = try commitHashDup(gpa, io, tmp_a.dir, "seed");
    defer gpa.free(tip);

    const open_unborn_port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, open_unborn_port);
    defer server.kill(io);
    try waitForListen(io, open_unborn_port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{open_unborn_port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    // `update` straight onto the clone-produced unborn record: the record is
    // present but names no check-in, and the managed set is absent (which reads
    // as empty) — so the tree is clean and the update must proceed.
    {
        var up = try runZit(gpa, io, b_dir, &.{ "update", "--no-sync", tip });
        defer freeResult(gpa, &up);
        try expectExit(up.term, 0);
        try expectNoDiagnostic(up.stderr); // the receipt lives on stderr
    }
    try expectCheckoutVersion(io, b_dir, tip);
    const bytes = try readFile(gpa, io, b_dir, "only.txt");
    defer gpa.free(bytes);
    try std.testing.expectEqualStrings("recoverable\n", bytes);
}

// The one materialization failure a user genuinely reaches on this verb: clone
// into a directory, leave a stray untracked file whose path a checked-in path
// needs, then `open`. Every other `ApplyError` arm needs a damaged repository or
// a failing device; this one needs only a file the user put there. It is also
// the arm that pins open's diagnostics to the real engine — a mis-mapped
// diagnostic table survives every success-path test.
test "cli open P3-F3: an untracked file in the way fails the open and materializes nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "only.txt", "from the repository\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"only.txt"});
    const tip = try commitHashDup(gpa, io, tmp_a.dir, "seed");
    defer gpa.free(tip);

    const open_collision_port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, open_collision_port);
    defer server.kill(io);
    try waitForListen(io, open_collision_port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{open_collision_port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    // The stray file: same path as the check-in's, untracked, different bytes.
    try writeFile(io, b_dir, "only.txt", "written by the user, never added\n");

    var res = try runZit(gpa, io, b_dir, &.{"open"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    // `open` resolves its own target, so it names the check-in it resolved
    // before reporting what went wrong: without that line a reader cannot tell
    // "materializing the expected check-in failed" from "resolved a check-in
    // other than the tip I believe in". Asserted whole rather than by
    // substring, so a future change to either line has to come through here.
    const expected_stderr = try std.fmt.allocPrint(
        gpa,
        "zit open: materializing check-in {s} on trunk\n" ++
            "zit open: an untracked file or directory is in the way\n",
        .{tip},
    );
    defer gpa.free(expected_stderr);
    try std.testing.expectEqualStrings(expected_stderr, res.stderr);

    // Nothing materialized and nothing clobbered: the record is still unborn and
    // the user's bytes are exactly as they were left.
    try expectCheckoutVersion(io, b_dir, "");
    const bytes = try readFile(gpa, io, b_dir, "only.txt");
    defer gpa.free(bytes);
    try std.testing.expectEqualStrings("written by the user, never added\n", bytes);
}

test "cli open P3-F3: re-opening an already-open tree is refused and changes nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "line\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const head = try commitHashDup(gpa, io, tmp.dir, "seed");
    defer gpa.free(head);

    // A dirty edit proves the refusal happens BEFORE any materialization: a
    // gate that ran the update anyway would either overwrite this or fail with
    // update's own not-clean diagnostic.
    try writeFile(io, tmp.dir, "poem", "edited but not committed\n");

    var res = try runZit(gpa, io, tmp.dir, &.{"open"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "already open") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, head) != null);

    const bytes = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(bytes);
    try std.testing.expectEqualStrings("edited but not committed\n", bytes);
}

/// A complete check-in manifest of a DIFFERENT repository, as file bytes.
///
/// `cards` is everything before the Z card; the checksum is computed here
/// because a wrong one makes the blob unclassifiable, which would silently
/// turn either test below into a tautology -- no foreign check-in would exist
/// and both would pass for the wrong reason.
///
/// The F card names an artifact no store holds, which is what makes the
/// check-in unmaterializable. `D 2099-01-01` puts it ahead of any check-in the
/// test itself makes, so mtime resolution (repo-state 8.2) prefers it wherever
/// it is eligible at all.
fn plantForeign(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    cards: []const u8,
) !void {
    var digest: [16]u8 = undefined;
    std.crypto.hash.Md5.hash(cards, &digest, .{});
    const hex = std.fmt.bytesToHex(digest, .lower);
    const payload = try std.fmt.allocPrint(gpa, "{s}Z {s}\n", .{ cards, &hex });
    defer gpa.free(payload);
    try writeFile(io, dir, "payload", payload);
    try writeFile(io, dir, "own.txt", "real content\n");
    try addManaged(gpa, io, dir, &.{ "payload", "own.txt" });
}

/// The store of `from`, dropped over the freshly-initialized repository in
/// `to`, whose checkout record is still unborn. This is the state `zit clone`
/// leaves, reached without a server: `open` resolving its own tip is the
/// subject of both tests, and a transport in front of it would only add ways
/// for them to fail for unrelated reasons.
fn adoptStore(gpa: std.mem.Allocator, io: std.Io, from: std.Io.Dir, to: std.Io.Dir) !void {
    const store = try readFile(gpa, io, from, ".zit");
    defer gpa.free(store);
    try writeFile(io, to, ".zit", store);
}

test "cli open P3-F3: a self-tagged foreign check-in is refused, naming the class" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);

    // `T *branch * trunk` is self-targeted and value-carrying; `T *sym-trunk *`
    // is bare and propagating. Together they make this manifest a genuine
    // carrier of trunk's tags, so it is eligible for tip resolution and wins on
    // mtime. (`T *branch trunk` would instead put a branch name in the UUID
    // field, rejecting the manifest entirely and making it unclassifiable.)
    try plantForeign(gpa, io, tmp_a.dir, "C foreign\n" ++
        "D 2099-01-01T00:00:00\n" ++
        "F absent.txt " ++ ("1" ** 64) ++ "\n" ++
        "T *branch * trunk\n" ++
        "T *sym-trunk *\n" ++
        "U nobody\n");
    const head = try commitHashDup(gpa, io, tmp_a.dir, "plant");
    defer gpa.free(head);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    try initRepo(gpa, io, tmp_b.dir);
    try adoptStore(gpa, io, tmp_a.dir, tmp_b.dir);

    var res = try runZit(gpa, io, tmp_b.dir, &.{"open"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);

    // The foreign check-in won the tip: the diagnostic names it and not the
    // check-in this test actually made. Without that, `open` would have
    // succeeded and the refusal below would be untested.
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, head) == null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "absent.txt") != null);

    // The diagnostic names the CLASS. A bare missing-file error sends the
    // reader to the wrong subsystem, and the store is intact, so it must not
    // allege corruption -- `verify` would contradict it.
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "ANOTHER repository") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "corrupt") == null);

    // Fail-closed: nothing materialized, the record still unborn. This is the
    // property most likely to be traded away by a later optimization of the
    // preflight, and it is the reason this test spawns the real binary.
    try expectCheckoutVersion(io, tmp_b.dir, "");
    try std.testing.expect(!fileExists(io, tmp_b.dir, "own.txt"));
    try std.testing.expect(!fileExists(io, tmp_b.dir, "payload"));
}

test "cli open P3-F3: an untagged foreign check-in is a leaf but never a tip" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);

    // Same manifest as above minus its T cards, and the same 2099 date. It is
    // still a check-in and still joins the branch for display, but it carries
    // no propagating branch tag, so the default-branch fallback attributes it
    // without making it resolvable (repo-state 6.1 L920-926). The later date
    // is what makes that distinction load-bearing: if attribution DID confer
    // resolvability, this manifest would take the tip and `open` would fail.
    try plantForeign(gpa, io, tmp_a.dir, "C untagged\n" ++
        "D 2099-01-01T00:00:00\n" ++
        "F absent.txt " ++ ("1" ** 64) ++ "\n" ++
        "U nobody\n");
    const head = try commitHashDup(gpa, io, tmp_a.dir, "plant");
    defer gpa.free(head);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    try initRepo(gpa, io, tmp_b.dir);
    try adoptStore(gpa, io, tmp_a.dir, tmp_b.dir);

    var res = try runZit(gpa, io, tmp_b.dir, &.{"open"});
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 0, res);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, head) != null);
    try expectCheckoutVersion(io, tmp_b.dir, head);

    // The real check-in materialized in full, foreign leaf notwithstanding.
    const bytes = try readFile(gpa, io, tmp_b.dir, "own.txt");
    defer gpa.free(bytes);
    try std.testing.expectEqualStrings("real content\n", bytes);

    // And it is visible: refusing to resolve it is not the same as hiding it,
    // and a preflight that made foreign check-ins disappear from `leaves`
    // would pass every assertion above.
    var leaves = try runZit(gpa, io, tmp_b.dir, &.{"leaves"});
    defer freeResult(gpa, &leaves);
    try expectExitDiag(leaves.term, 0, leaves);
    var lines: usize = 0;
    var it = std.mem.tokenizeScalar(u8, leaves.stdout, '\n');
    while (it.next()) |_| lines += 1;
    try std.testing.expectEqual(@as(usize, 2), lines);
}

test "cli open P3-F3: a repository with no check-in is reported, not failed" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"open"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "no check-in") != null);
    // The unborn record is the correct end state for an empty repository; open
    // leaves it exactly as it found it.
    try expectCheckoutVersion(io, tmp.dir, "");
}

test "cli open P3-F3: outside a repository fails with the not-a-repo diagnostic, exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{}); // NOT initialized
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{"open"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no zit checkout here") != null);
}

test "cli open P3-F3: an unknown option or a positional is a usage error, exit 2" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const cases = [_][]const []const u8{
        &.{ "open", "-x" }, // unknown option
        &.{ "open", "a", "b" }, // one positional is CHECKIN; a second is extra
    };
    for (cases) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "try 'zit open --help'") != null);
    }

    // A single positional is CHECKIN — not a usage error. In this empty
    // repository it resolves to nothing: an operational failure, exit 1.
    var named = try runZit(gpa, io, tmp.dir, &.{ "open", "somewhere" });
    defer freeResult(gpa, &named);
    try expectExit(named.term, 1);
    try std.testing.expectEqualStrings("", named.stdout);
    try std.testing.expectEqualStrings("zit open: check-in 'somewhere' not found\n", named.stderr);
}

test "cli open P3-F3: the verb is in the top-level help and teaches one help text" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var top = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &top);
    try expectExit(top.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, top.stdout, "  open ") != null);

    // `zit open --help` and `zit help open` teach byte-identical text (DX D4).
    var flag = try runZit(gpa, io, tmp.dir, &.{ "open", "--help" });
    defer freeResult(gpa, &flag);
    try expectExit(flag.term, 0);
    try std.testing.expectEqualStrings(open_help_expected, flag.stdout);

    var topic = try runZit(gpa, io, tmp.dir, &.{ "help", "open" });
    defer freeResult(gpa, &topic);
    try expectExit(topic.term, 0);
    try std.testing.expectEqualStrings(open_help_expected, topic.stdout);
}

// --------------------------------------------------------------------------
// `open --manifest[=r,u,t]`: the working-tree manifest files, as an explicit
// per-checkout opt-in. The three characters select the check-in artifact
// (`r` -> `manifest`), its hash (`u` -> `manifest.uuid`) and its symbolic tags
// (`t` -> `manifest.tags`); the bare flag means `ru`. The opt-in is recorded in
// the checkout record, so every later `update` refreshes the files. No flag
// writes nothing, which is the default and stays the default.
// --------------------------------------------------------------------------

/// The line fossil appends to the on-disk `manifest`, so the file is not itself
/// a well-formed artifact. Pinned here as a literal, not imported: it is a
/// cross-implementation compatibility fact, and a test that read it from the
/// implementation could not notice the implementation changing it.
const manifest_sterilizer = "# Remove this line to create a well-formed Fossil manifest.\n";

test "cli open: --manifest writes the manifest files and a later update refreshes them" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "hello.txt", "first revision\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    const root = try commitHashDup(gpa, io, tmp_a.dir, "root");
    defer gpa.free(root);
    try writeFile(io, tmp_a.dir, "hello.txt", "second revision, longer than the first\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    const tip = try commitHashDup(gpa, io, tmp_a.dir, "tip");
    defer gpa.free(tip);

    const open_manifest_port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, open_manifest_port);
    defer server.kill(io);
    try waitForListen(io, open_manifest_port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{open_manifest_port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    {
        var op = try runZit(gpa, io, b_dir, &.{ "open", "--manifest=rut" });
        defer freeResult(gpa, &op);
        try expectExit(op.term, 0);
        try std.testing.expectEqualStrings("", op.stderr);
    }

    // `u`: the check-in hash and a newline, verbatim.
    {
        const uuid = try readFile(gpa, io, b_dir, "manifest.uuid");
        defer gpa.free(uuid);
        const want = try std.fmt.allocPrint(gpa, "{s}\n", .{tip});
        defer gpa.free(want);
        try std.testing.expectEqualStrings(want, uuid);
    }
    // `r`: the check-in artifact ITSELF, plus fossil's sterilizing line —
    // asserted by hashing the leading bytes back to `tip`. A weaker oracle (the
    // file exists, or it mentions a card name) would pass on approximately-right
    // bytes; this one cannot.
    {
        const man = try readFile(gpa, io, b_dir, "manifest");
        defer gpa.free(man);
        try std.testing.expect(std.mem.endsWith(u8, man, manifest_sterilizer));
        try std.testing.expect(artifact.verify(tip, man[0 .. man.len - manifest_sterilizer.len]));
        // POSITION, not merely presence: the marker follows the final card.
        // Fossil appends it, and a reader strips a TRAILING occurrence, so a
        // refactor that moved it to line 1 would still sterilize, still satisfy
        // an endsWith-free check, and silently stop matching Fossil byte for
        // byte.
        try std.testing.expect(!std.mem.startsWith(u8, man, manifest_sterilizer));
    }
    // `t`: fossil's layout — the branch line, then one line per symbolic tag.
    {
        const tags = try readFile(gpa, io, b_dir, "manifest.tags");
        defer gpa.free(tags);
        try std.testing.expectEqualStrings("branch trunk\ntag trunk\n", tags);
    }

    // The opt-in is per checkout, not per command: `update` refreshes all three
    // against the new check-in without the flag being repeated.
    {
        var up = try runZit(gpa, io, b_dir, &.{ "update", root });
        defer freeResult(gpa, &up);
        try expectExit(up.term, 0);
    }
    {
        const uuid = try readFile(gpa, io, b_dir, "manifest.uuid");
        defer gpa.free(uuid);
        const want = try std.fmt.allocPrint(gpa, "{s}\n", .{root});
        defer gpa.free(want);
        try std.testing.expectEqualStrings(want, uuid);
        const man = try readFile(gpa, io, b_dir, "manifest");
        defer gpa.free(man);
        try std.testing.expect(artifact.verify(root, man[0 .. man.len - manifest_sterilizer.len]));
    }
}

test "cli open: --manifest never overwrites a tracked file of the same name" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    // A repository that TRACKS a file called `manifest`. Fossil refuses to
    // clobber it and so must zit: the user's versioned content outranks a
    // convenience file.
    try writeFile(io, tmp_a.dir, "manifest", "a tracked file, not the check-in\n");
    try writeFile(io, tmp_a.dir, "hello.txt", "payload\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "manifest", "hello.txt" });
    const tip = try commitHashDup(gpa, io, tmp_a.dir, "seed");
    defer gpa.free(tip);

    const open_manifest_guard_port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, open_manifest_guard_port);
    defer server.kill(io);
    try waitForListen(io, open_manifest_guard_port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{open_manifest_guard_port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    {
        var op = try runZit(gpa, io, b_dir, &.{ "open", "--manifest=ru" });
        defer freeResult(gpa, &op);
        try expectExit(op.term, 0);
        try std.testing.expectEqualStrings("", op.stderr);
    }

    // The tracked bytes survive; the un-tracked sibling is still written.
    const kept = try readFile(gpa, io, b_dir, "manifest");
    defer gpa.free(kept);
    try std.testing.expectEqualStrings("a tracked file, not the check-in\n", kept);
    const uuid = try readFile(gpa, io, b_dir, "manifest.uuid");
    defer gpa.free(uuid);
    const want = try std.fmt.allocPrint(gpa, "{s}\n", .{tip});
    defer gpa.free(want);
    try std.testing.expectEqualStrings(want, uuid);
}

test "cli open: without --manifest no manifest file is written" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "line\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const head = try commitHashDup(gpa, io, tmp.dir, "seed");
    defer gpa.free(head);

    for ([_][]const u8{ "manifest", "manifest.uuid", "manifest.tags" }) |name| {
        try std.testing.expect(!fileExists(io, tmp.dir, name));
    }
}

test "cli open: a versioned-only manifest setting is read out of the check-in at open" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDirPath(io, ".fossil-settings");
    try writeFile(io, tmp_a.dir, ".fossil-settings/manifest", "# stamps for the build\nu\n");
    try writeFile(io, tmp_a.dir, "hello.txt", "one\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ ".fossil-settings/manifest", "hello.txt" });
    const tip = try commitHashDup(gpa, io, tmp_a.dir, "root");
    defer gpa.free(tip);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    {
        var op = try runZit(gpa, io, b_dir, &.{"open"});
        defer freeResult(gpa, &op);
        try expectExitDiag(op.term, 0, op);
    }
    // No repository or global value exists: only the check-in's own file
    // could have asked for `manifest.uuid`, and it did.
    const uuid = try readFile(gpa, io, b_dir, "manifest.uuid");
    defer gpa.free(uuid);
    const want = try std.fmt.allocPrint(gpa, "{s}\n", .{tip});
    defer gpa.free(want);
    try std.testing.expectEqualStrings(want, uuid);
    try std.testing.expect(!fileExists(io, b_dir, "manifest"));
}

test "cli open: --manifest=off is remembered, so the project setting cannot re-enable stamps at update" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDirPath(io, ".zit-settings");
    try writeFile(io, tmp_a.dir, ".zit-settings/manifest", "on\n");
    try writeFile(io, tmp_a.dir, "hello.txt", "one\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ ".zit-settings/manifest", "hello.txt" });
    const root = try commitHashDup(gpa, io, tmp_a.dir, "root");
    defer gpa.free(root);
    try writeFile(io, tmp_a.dir, "hello.txt", "two\n");
    const tip = try commitHashDup(gpa, io, tmp_a.dir, "tip");
    defer gpa.free(tip);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    {
        var op = try runZit(gpa, io, b_dir, &.{ "open", "--manifest=off" });
        defer freeResult(gpa, &op);
        try expectExitDiag(op.term, 0, op);
    }
    try std.testing.expect(!fileExists(io, b_dir, "manifest"));
    try std.testing.expect(!fileExists(io, b_dir, "manifest.uuid"));
    {
        var up = try runZit(gpa, io, b_dir, &.{ "update", root });
        defer freeResult(gpa, &up);
        try expectExitDiag(up.term, 0, up);
    }
    // The versioned `on` is live in this tree now, and still loses to the
    // checkout's explicit refusal.
    try std.testing.expect(!fileExists(io, b_dir, "manifest"));
    try std.testing.expect(!fileExists(io, b_dir, "manifest.uuid"));
    // And `on` at open is fossil's spelling for `ru`.
    var tmp_c = std.testing.tmpDir(.{});
    defer tmp_c.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_c.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var c_dir = try tmp_c.dir.openDir(io, "repo", .{});
    defer c_dir.close(io);
    {
        var op = try runZit(gpa, io, c_dir, &.{ "open", "--manifest=on" });
        defer freeResult(gpa, &op);
        try expectExitDiag(op.term, 0, op);
    }
    try std.testing.expect(fileExists(io, c_dir, "manifest"));
    try std.testing.expect(fileExists(io, c_dir, "manifest.uuid"));
    try std.testing.expect(!fileExists(io, c_dir, "manifest.tags"));
}

// ==========================================================================
// `zit add` / `zit rm` directory recursion (P3-F4). `add <dir>` scopes ONE
// `extras.sweep` of the checkout root by a directory prefix — the same walk,
// the same exclusions, no second walker; `rm <dir>` expands the argument over
// the TRACKED keys (`ManagedSet.iterate`) and never touches the disk, so a
// directory the user already deleted is still forgettable. Both verbs consume
// exactly one enumeration each.
// ==========================================================================

/// Creates the directory chain `path` under `dir`, component by component.
/// `createDir` is single-level; the recursion tests need nested trees.
fn mkDirPath(io: std.Io, dir: std.Io.Dir, path: []const u8) !void {
    var it = std.mem.splitScalar(u8, path, '/');
    var cur = try dir.openDir(io, ".", .{});
    defer cur.close(io);
    while (it.next()) |seg| {
        if (seg.len == 0) continue;
        cur.createDir(io, seg, .default_dir) catch |e| switch (e) {
            error.PathAlreadyExists => {},
            else => return e,
        };
        const child = try cur.openDir(io, seg, .{});
        cur.close(io);
        cur = child;
    }
}

/// Creates every `dir/name` file, making the parent directories first.
fn touchTree(io: std.Io, dir: std.Io.Dir, paths: []const []const u8) !void {
    for (paths) |p| {
        if (std.mem.lastIndexOfScalar(u8, p, '/')) |i| try mkDirPath(io, dir, p[0..i]);
        try touch(io, dir, p);
    }
}

test "cli add P3-F4: 'add <dir>' adds every untracked file beneath it, recursively and scoped" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{ "src/a", "src/deep/b", "src/deep/deeper/c", "other/d", "top" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "src" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // Ascending byte order (the sweep's own order), scoped to `src`: the
    // sibling tree and the root file are untouched.
    try std.testing.expectEqualStrings(
        "ADDED  src/a\nADDED  src/deep/b\nADDED  src/deep/deeper/c\n",
        res.stdout,
    );
    try std.testing.expectEqualStrings("", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{ "src/a", "src/deep/b", "src/deep/deeper/c" });
}

test "cli add P3-F4: a directory prefix never leaks across a shared name boundary" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // `src2/…` shares src's bytes but is NOT under `src/` — a plain
    // startsWith would wrongly swallow it.
    try touchTree(io, tmp.dir, &.{ "src/a", "src2/b", "srcx" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "src" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("ADDED  src/a\n", res.stdout);
    try expectManaged(gpa, io, tmp.dir, &.{"src/a"});
}

test "cli add P3-F4: 'add .' at the root adds the whole tree; root control names are never added" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{ "a", "sub/b" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "." });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("ADDED  a\nADDED  sub/b\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    // `.zit`, `.zit-checkout` (and any other root control name) are excluded by
    // the sweep itself, so `add .` can never track them.
    try expectManaged(gpa, io, tmp.dir, &.{ "a", "sub/b" });
}

test "cli add P3-F4: the control-name exclusion is depth-0 only — a nested '.zit-cache' IS added" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    // Below the root zit's OWN names carry no special meaning; --dotfiles is
    // what makes them visible at all now that dot-names are hidden by default.
    try touchTree(io, tmp.dir, &.{ "sub/.zit-cache", "sub/.zit" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", ".", "--dotfiles" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("ADDED  sub/.zit\nADDED  sub/.zit-cache\n", res.stdout);
    try expectManaged(gpa, io, tmp.dir, &.{ "sub/.zit", "sub/.zit-cache" });
}

test "cli add P3-F4: 'add .' inside a subdirectory scopes to that subdirectory" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{ "sub/a", "sub/deep/b", "other/c" });

    var sub = try tmp.dir.openDir(io, "sub", .{});
    defer sub.close(io);
    var res = try runZit(gpa, io, sub, &.{ "add", "." });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // Keys stay root-relative even though the cwd is the subdirectory.
    try std.testing.expectEqualStrings("ADDED  sub/a\nADDED  sub/deep/b\n", res.stdout);
    try expectManaged(gpa, io, tmp.dir, &.{ "sub/a", "sub/deep/b" });
}

test "cli add P3-F4: a partially-tracked directory adds exactly the untracked remainder" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{ "sub/a", "sub/b", "sub/c" });
    try addManaged(gpa, io, tmp.dir, &.{"sub/b"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "sub" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // The tracked entry is excluded by the sweep — neither re-inserted nor
    // reported as "already tracked" (it was never named).
    try std.testing.expectEqualStrings("ADDED  sub/a\nADDED  sub/c\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{ "sub/a", "sub/b", "sub/c" });
}

test "cli add P3-F4: --dry-run over a directory previews without persisting" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{ "sub/a", "sub/deep/b" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "-n", "sub" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("would add sub/a\nwould add sub/deep/b\n", res.stdout);
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli add P3-F4: a directory holding nothing untracked adds nothing and succeeds" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{"sub/a"});
    try addManaged(gpa, io, tmp.dir, &.{"sub/a"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "sub" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{"sub/a"});
}

test "cli rm P3-F4: 'rm <dir>' forgets every tracked key beneath it, scoped, files kept on disk" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{ "src/a", "src/deep/b", "src2/c", "top" });
    try addManaged(gpa, io, tmp.dir, &.{ "src/a", "src/deep/b", "src2/c", "top" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "src" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "DELETED  src/a\nDELETED  src/deep/b\n",
        res.stdout,
    );
    try std.testing.expectEqualStrings("", res.stderr);
    // `src2/c` shares src's bytes but is not beneath `src/`; `top` is untouched.
    try expectManaged(gpa, io, tmp.dir, &.{ "src2/c", "top" });
    try std.testing.expect(fileExists(io, tmp.dir, "src/a"));
}

test "cli rm P3-F4: a directory already deleted from disk is still forgettable (membership, not existence)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{ "sub/a", "sub/b" });
    try addManaged(gpa, io, tmp.dir, &.{ "sub/a", "sub/b" });
    // The user removes the whole directory themselves — the primary reason to
    // reach for `zit rm <dir>`. A filesystem walk could not serve this.
    try tmp.dir.deleteFile(io, "sub/a");
    try tmp.dir.deleteFile(io, "sub/b");
    try tmp.dir.deleteDir(io, "sub");

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "sub" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "DELETED  sub/a\nDELETED  sub/b\n",
        res.stdout,
    );
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli rm P3-F4: 'rm .' at the root forgets the whole tracked tree" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{ "a", "sub/b" });
    try addManaged(gpa, io, tmp.dir, &.{ "a", "sub/b" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "." });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "DELETED  a\nDELETED  sub/b\n",
        res.stdout,
    );
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli rm P3-F4: a directory with no tracked keys beneath it is 'not tracked', exit 1" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{ "sub/a", "keep" });
    try addManaged(gpa, io, tmp.dir, &.{"keep"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "sub" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    // No new diagnostic class: an argument matching neither a tracked entry nor
    // any tracked prefix keeps the existing message.
    try std.testing.expectEqualStrings("zit rm: 'sub' is not tracked\n", res.stderr);
    try expectManaged(gpa, io, tmp.dir, &.{"keep"});
}

test "cli rm P3-F4: 'rm --hard <dir>' deletes the working files only after the write" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{ "sub/a", "sub/deep/b", "keep" });
    try addManaged(gpa, io, tmp.dir, &.{ "sub/a", "sub/deep/b", "keep" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "sub" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "DELETED  sub/a (deleted from disk)\nDELETED  sub/deep/b (deleted from disk)\n",
        res.stdout,
    );
    try expectManaged(gpa, io, tmp.dir, &.{"keep"});
    try std.testing.expect(!fileExists(io, tmp.dir, "sub/a"));
    try std.testing.expect(!fileExists(io, tmp.dir, "sub/deep/b"));
    try std.testing.expect(fileExists(io, tmp.dir, "keep"));
}

test "cli rm P3-F4: a partially-tracked directory forgets only its tracked keys" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touchTree(io, tmp.dir, &.{ "sub/a", "sub/b" });
    try addManaged(gpa, io, tmp.dir, &.{"sub/a"}); // sub/b was never tracked

    var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "-n", "sub" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("would remove sub/a\n", res.stdout);
    // A dry run persists nothing.
    try expectManaged(gpa, io, tmp.dir, &.{"sub/a"});
}

// --- REGRESSION B1 --------------------------------------------------------
//
// Same-second commits resolve to the WRONG check-in, silently (blocker B1/B2,
// docs/plans/CLI-VERB-BREAKDOWN.md §B1/B2). `repostate.zig`'s selection sites
// break event.mtime ties by artifact-hash order rather than DAG ancestry, so a
// three-commit chain minted inside one second can resolve `tip`/`trunk` to an
// ANCESTOR of the newest check-in. These tests must NOT sleep between commits
// — same-second IS the repro.

/// Mints the exact CLI repro from the breakdown doc: three same-second commits
/// c1/c2/c3 rewriting `f.txt` v1 → v2 → v3 on a linear trunk. Returns c3's
/// hash (caller frees); the checkout is left at c3 with `f.txt` == "v3\n".
fn buildSameSecondChain(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]const u8 {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "f.txt", "v1\n");
    try addManaged(gpa, io, dir, &.{"f.txt"});
    const c1 = try commitHashDup(gpa, io, dir, "c1");
    defer gpa.free(c1);
    try writeFile(io, dir, "f.txt", "v2\n");
    const c2 = try commitHashDup(gpa, io, dir, "c2");
    defer gpa.free(c2);
    try writeFile(io, dir, "f.txt", "v3\n");
    return commitHashDup(gpa, io, dir, "c3");
}

test "cli REGRESSION B1: same-second commits resolve tip and trunk to the newest check-in" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const c3 = try buildSameSecondChain(gpa, io, tmp.dir);
    defer gpa.free(c3);

    // Ground truth: the working file holds v3, and `leaves` — which reads the
    // leaf table rather than the mtime-tie selection — names c3.
    const on_disk = try readFile(gpa, io, tmp.dir, "f.txt");
    defer gpa.free(on_disk);
    try std.testing.expectEqualStrings("v3\n", on_disk);

    // `cat f.txt tip` must be the newest content, not an ancestor's.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "f.txt", "tip" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("v3\n", res.stdout);
    }

    // `cat f.txt trunk` must agree with `tip` — two symbolic names for the
    // same check-in disagreeing in one binary is the disqualifying symptom.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "f.txt", "trunk" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("v3\n", res.stdout);
    }

    // `info tip` must name c3 and report it as a leaf. SF5's tripwire: `tip`
    // resolving to a non-leaf is self-contradictory in a linear history.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "info", "tip" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.startsWith(u8, res.stdout, c3));
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "leaf:") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "leaf:    no") == null);
    }

    // Cross-verb agreement (§5.3's invariant): `info tip` and `leaves` must
    // name the same check-in.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"leaves"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, c3[0..12]) != null);
    }

    // `timeline` must order newest-first: c3, then c2, then c1.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"timeline"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        const at_c3 = std.mem.indexOf(u8, res.stdout, " c3").?;
        const at_c2 = std.mem.indexOf(u8, res.stdout, " c2").?;
        const at_c1 = std.mem.indexOf(u8, res.stdout, " c1").?;
        try std.testing.expect(at_c3 < at_c2);
        try std.testing.expect(at_c2 < at_c1);
    }
}

test "cli REGRESSION B1 (SF1): update accepts a symbolic name and lands on the newest check-in" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const c3 = try buildSameSecondChain(gpa, io, tmp.dir);
    defer gpa.free(c3);

    // Move the checkout off the tip so `update trunk` has somewhere to travel.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "f.txt", "tip" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    // SF1: `update` rejects symbolic names today ("invalid check-in ID
    // 'trunk'"); it goes through the same resolution surface B1 touches.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    // CONTRACT PARTNER chain (§5.2.3): update → status → commit → open. The
    // checkout must sit at c3 with a clean tree.
    try expectCheckoutVersion(io, tmp.dir, c3);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"status"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, cli.shortHash(c3)) != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "nothing to report") != null);
    }

    // A commit on top of the updated checkout must record c3 as its parent —
    // proof that `update trunk` resolved to the newest check-in, not an
    // ancestor.
    try writeFile(io, tmp.dir, "f.txt", "v4\n");
    const c4 = try commitHashDup(gpa, io, tmp.dir, "c4");
    defer gpa.free(c4);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "info", c4 });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, c3) != null);
    }
}

test "cli REGRESSION B1: open materializes the newest same-second check-in into a fresh clone" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    const c3 = try buildSameSecondChain(gpa, io, tmp_a.dir);
    defer gpa.free(c3);

    const b1_open_port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, b1_open_port);
    defer server.kill(io);
    try waitForListen(io, b1_open_port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{b1_open_port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    // `open` resolves the tip of trunk from the cloned repository's own state
    // — the same mtime-tie selection B1 breaks.
    {
        var res = try runZit(gpa, io, b_dir, &.{"open"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, c3) != null);
    }
    try expectCheckoutVersion(io, b_dir, c3);

    // B2: the materialized bytes must be v3, not an ancestor's v1/v2.
    const opened = try readFile(gpa, io, b_dir, "f.txt");
    defer gpa.free(opened);
    try std.testing.expectEqualStrings("v3\n", opened);
}

test "cli REGRESSION B1: a same-second two-branch merge picks the correct base and is never up to date" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // CONTRACT PARTNER (§5.2.3): `mergeBaseOf` shares the mtime-tie selection
    // being rewritten. A same-second diamond's two leaves share exactly one
    // common ancestor; a wrong base silently re-applies or drops a side.
    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
    defer fx.deinit(gpa);

    var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    // Never a false "already up to date": the two sides are genuinely divergent.
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "up to date") == null);

    // Correct base content: base A/B/C, current A1/B/C, other A/B/C1 → the
    // three-way result takes BOTH disjoint edits. A base selected off the
    // ancestor chain would conflict or drop one side.
    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expectEqualStrings("A1\nB\nC1\n", poem);

    // status must surface the pending merge parent.
    try expectMergeState(io, tmp.dir, fx.current, fx.other);
}

// --- REGRESSION B5 --------------------------------------------------------
//
// A merge whose only change is an ADDED file is silently lost (blocker B5,
// docs/plans/CLI-VERB-BREAKDOWN.md §B5). `merge` writes the incoming file to
// the working tree but never inserts it into `.zit-managed`, so the file is
// simultaneously present on disk and untracked: `status` reports "nothing to
// report", `extras` lists the merged file as junk, and `commit` refuses with
// "nothing to commit (no changes)" — dropping the merge parent that `merge`
// recorded in `.zit-checkout`. The user's only escape is `undo`.
//
// The symmetric half of the same omission: a merge that DELETES a file leaves
// the path managed, so `status` reports a phantom "missing" and `commit`
// refuses with "cannot commit … missing from the working tree". Both ride the
// same `applyMerge` entry stream, so both are encoded here.
//
// ORDERING: the defect is a managed-set mutation, so each repro is built in
// both orderings that could hide it — the merge direction (feature→trunk and
// trunk→feature) and the added path's position in the sorted managed set
// (`feat.txt` inserts AFTER `base.txt`; `aaa.txt` inserts BEFORE it). Identical
// failures in both is what proves the mechanism rather than luck.

/// Builds the §B5 repro shape: root(`base.txt`) on trunk, then a `feature`
/// branch whose ONLY change is an added `feat.txt`. Leaves the checkout back on
/// trunk at the root, clean, ready for `zit merge feature`. Returns
/// {base, other, current} where `current` == `base` (trunk never advanced).
///
/// Hashes are captured from each `commit`'s own stdout, never from `info tip`:
/// with two same-mtime leaves `tip` legitimately picks either one, and reading
/// it here would make the fixture — not the defect — decide the outcome.
fn buildAddOnlyBranch(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !MergeDiamond {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "base.txt", "base\n");
    try addManaged(gpa, io, dir, &.{"base.txt"});
    const base = try commitHashDup(gpa, io, dir, "root");
    errdefer gpa.free(base);

    try writeFile(io, dir, "feat.txt", "feat\n");
    try addManaged(gpa, io, dir, &.{"feat.txt"});
    var branch = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "--branch", "feature", "-m", "addfeat" });
    defer freeResult(gpa, &branch);
    try expectExit(branch.term, 0);
    const other = try gpa.dupe(u8, try checkinHash(branch.stdout));
    errdefer gpa.free(other);

    var up = try runZit(gpa, io, dir, &.{ "update", base });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);

    const current = try gpa.dupe(u8, base);
    return .{ .base = base, .other = other, .current = current };
}

/// Asserts `zit extras` reports an empty working tree — the on-disk oracle that
/// a merged-in file is TRACKED, not junk.
fn expectNoExtras(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !void {
    var res = try runZit(gpa, io, dir, &.{"extras"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stdout);
}

test "cli REGRESSION B5: an add-only merge registers the file, status shows it added, commit lands two parents" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildAddOnlyBranch(gpa, io, tmp.dir);
    defer fx.deinit(gpa);

    // The merge itself already works: it writes feat.txt and records the parent.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "feature" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "1 file(s) changed") != null);
    }
    const merged = try readFile(gpa, io, tmp.dir, "feat.txt");
    defer gpa.free(merged);
    try std.testing.expectEqualStrings("feat\n", merged);
    try expectMergeState(io, tmp.dir, fx.current, fx.other);

    // THE DEFECT: the merged-in file must be managed. `feat.txt` sorts AFTER
    // `base.txt`, so this is the append end of the sorted set.
    try expectManaged(gpa, io, tmp.dir, &.{ "base.txt", "feat.txt" });

    // …and therefore must not be reported as junk.
    try expectNoExtras(gpa, io, tmp.dir);

    // status must surface BOTH the pending merge (by naming its parent) and
    // the merge-added file. "nothing to report" over a pending merge that
    // changed a file is the silent half of the blocker.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"status"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "nothing to report") == null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, fx.other) != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "ADDED      feat.txt") != null);
    }

    // commit must succeed and mint a real merge check-in.
    const m = try commitHashDup(gpa, io, tmp.dir, "m");
    defer gpa.free(m);

    // Two parents: the primary is the pre-merge trunk tip, the merge parent is
    // the feature tip. A dropped merge parent is a lost branch.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "info", m });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        var buf: [96]u8 = undefined;
        const want_parent = try std.fmt.bufPrint(&buf, "parent:  {s}\n", .{fx.current});
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, want_parent) != null);
        var buf2: [96]u8 = undefined;
        const want_merge = try std.fmt.bufPrint(&buf2, "merge:   {s}\n", .{fx.other});
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, want_merge) != null);
    }

    // The pending state is consumed, not left dangling.
    try expectMergeState(io, tmp.dir, m, "");
    try expectNoExtras(gpa, io, tmp.dir);
}

test "cli REGRESSION B5: an add-only merge in the reverse direction registers the same way" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // SECOND ORDERING. The first test merges feature INTO a trunk checkout and
    // inserts at the tail of the sorted managed set. This one merges the trunk
    // tip INTO a `feature` checkout and inserts `aaa.txt` at the HEAD, before
    // both existing entries. Same failure in both ⇒ the mechanism is the
    // missing managed-set mutation, not the direction or the insert position.
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "base.txt", "base\n");
    try addManaged(gpa, io, tmp.dir, &.{"base.txt"});
    const root = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(root);

    try writeFile(io, tmp.dir, "mark.txt", "mark\n");
    try addManaged(gpa, io, tmp.dir, &.{"mark.txt"});
    var branch = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "--branch", "feature", "-m", "featwork" });
    defer freeResult(gpa, &branch);
    try expectExit(branch.term, 0);
    const feature_tip = try gpa.dupe(u8, try checkinHash(branch.stdout));
    defer gpa.free(feature_tip);

    {
        var up = try runZit(gpa, io, tmp.dir, &.{ "update", root });
        defer freeResult(gpa, &up);
        try expectExit(up.term, 0);
    }
    try writeFile(io, tmp.dir, "aaa.txt", "aaa\n");
    try addManaged(gpa, io, tmp.dir, &.{"aaa.txt"});
    const trunk_tip = try commitHashDup(gpa, io, tmp.dir, "trunkadd");
    defer gpa.free(trunk_tip);

    {
        var up = try runZit(gpa, io, tmp.dir, &.{ "update", feature_tip });
        defer freeResult(gpa, &up);
        try expectExit(up.term, 0);
    }

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", trunk_tip });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "1 file(s) changed") != null);
    }

    // Head-of-set insertion.
    try expectManaged(gpa, io, tmp.dir, &.{ "aaa.txt", "base.txt", "mark.txt" });
    try expectNoExtras(gpa, io, tmp.dir);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"status"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "nothing to report") == null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, trunk_tip) != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "ADDED      aaa.txt") != null);
    }

    const m = try commitHashDup(gpa, io, tmp.dir, "m");
    defer gpa.free(m);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "info", m });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        var buf: [96]u8 = undefined;
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, try std.fmt.bufPrint(&buf, "parent:  {s}\n", .{feature_tip})) != null);
        var buf2: [96]u8 = undefined;
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, try std.fmt.bufPrint(&buf2, "merge:   {s}\n", .{trunk_tip})) != null);
    }
}

test "cli REGRESSION B5: a merge that deletes a file deregisters it instead of reporting it missing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // The symmetric half of the same omission, on the same `applyMerge` entry
    // stream: `.delete` entries remove the file from disk but leave it in the
    // managed set, so `status` invents a "missing" file that the user never
    // deleted and `commit` refuses over it.
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "base.txt", "base\n");
    try writeFile(io, tmp.dir, "gone.txt", "gone\n");
    try addManaged(gpa, io, tmp.dir, &.{ "base.txt", "gone.txt" });
    const root = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(root);

    {
        var rm = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "gone.txt" });
        defer freeResult(gpa, &rm);
        try expectExit(rm.term, 0);
    }
    var branch = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "--branch", "feature", "-m", "delfeat" });
    defer freeResult(gpa, &branch);
    try expectExit(branch.term, 0);
    const feature_tip = try gpa.dupe(u8, try checkinHash(branch.stdout));
    defer gpa.free(feature_tip);

    {
        var up = try runZit(gpa, io, tmp.dir, &.{ "update", root });
        defer freeResult(gpa, &up);
        try expectExit(up.term, 0);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", feature_tip });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    // gone.txt is off disk, so it must be off the managed set too.
    try std.testing.expect(!fileExists(io, tmp.dir, "gone.txt"));
    try expectManaged(gpa, io, tmp.dir, &.{"base.txt"});
    try expectNoExtras(gpa, io, tmp.dir);

    // No phantom "missing" — the user deleted nothing.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"status"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "MISSING    gone.txt") == null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, feature_tip) != null);
    }

    // …and commit lands the merge rather than refusing over the phantom.
    const m = try commitHashDup(gpa, io, tmp.dir, "m");
    defer gpa.free(m);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "info", m });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        var buf: [96]u8 = undefined;
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, try std.fmt.bufPrint(&buf, "merge:   {s}\n", .{feature_tip})) != null);
    }
}

test "cli REGRESSION B5: zit add of a merge-added file is idempotent" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // CONTRACT PARTNER: merge gains a write path into the same `ManagedSet`
    // that `add` owns. Once merge registers the file, `add` must see it as
    // already tracked — two mechanisms writing the same set must agree, or a
    // user typing the obvious recovery command double-registers it.
    var fx = try buildAddOnlyBranch(gpa, io, tmp.dir);
    defer fx.deinit(gpa);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "feature" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "feat.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // The notice is a stderr diagnostic, not stdout output — the stream `add`
    // has always used for it (see the `zit add` idempotence tests above).
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "already tracked") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "ADDED      feat.txt") == null);
    try expectManaged(gpa, io, tmp.dir, &.{ "base.txt", "feat.txt" });
}

test "cli REGRESSION B5: cherrypick of an add-only revision registers the added file" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // CONTRACT PARTNER: cherrypick shares `applyMerge`, so an add-only revision
    // cherry-picked onto trunk loses the file exactly the same way.
    var fx = try buildAddOnlyBranch(gpa, io, tmp.dir);
    defer fx.deinit(gpa);

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", fx.other });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "1 file(s) changed") != null);
    }

    try expectManaged(gpa, io, tmp.dir, &.{ "base.txt", "feat.txt" });
    try expectNoExtras(gpa, io, tmp.dir);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"status"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "ADDED      feat.txt") != null);
    }

    // A cherrypick is NOT a merge: it commits with one parent and a Q card, so
    // only the registration and the commit path are asserted here.
    const m = try commitHashDup(gpa, io, tmp.dir, "cp");
    defer gpa.free(m);
}

test "cli REGRESSION B5: a convergent-content merge still commits its merge parent" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Both sides made the SAME edit, so the merge changes zero files but is
    // still a real merge with a real parent to record. commit's "nothing to
    // commit (no changes)" refusal must not fire over a pending merge: the
    // merge parent IS the change, and refusing wedges the tree with `undo` as
    // the only escape.
    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A\nB\nC1\n");
    defer fx.deinit(gpa);

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "up to date") == null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "0 file(s) changed") != null);
    }
    try expectMergeState(io, tmp.dir, fx.current, fx.other);

    const m = try commitHashDup(gpa, io, tmp.dir, "m");
    defer gpa.free(m);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "info", m });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        var buf: [96]u8 = undefined;
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, try std.fmt.bufPrint(&buf, "parent:  {s}\n", .{fx.current})) != null);
        var buf2: [96]u8 = undefined;
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, try std.fmt.bufPrint(&buf2, "merge:   {s}\n", .{fx.other})) != null);
    }
    try expectMergeState(io, tmp.dir, m, "");
}

test "cli REGRESSION B5: a fresh open materializes the merge-added file" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    var fx = try buildAddOnlyBranch(gpa, io, tmp_a.dir);
    defer fx.deinit(gpa);
    {
        var res = try runZit(gpa, io, tmp_a.dir, &.{ "merge", "feature" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    // The end-to-end proof the blocker is about: the merged file must reach the
    // REPOSITORY, not just this working tree. If commit refuses, feat.txt only
    // ever existed on one disk and the branch is lost.
    const m = try commitHashDup(gpa, io, tmp_a.dir, "m");
    defer gpa.free(m);

    const b5_open_port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, b5_open_port);
    defer server.kill(io);
    try waitForListen(io, b5_open_port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{b5_open_port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    {
        var res = try runZit(gpa, io, b_dir, &.{"open"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    const opened = try readFile(gpa, io, b_dir, "feat.txt");
    defer gpa.free(opened);
    try std.testing.expectEqualStrings("feat\n", opened);
    try expectManaged(gpa, io, b_dir, &.{ "base.txt", "feat.txt" });
}

test "cli REGRESSION B5: undo of a pending add-only merge leaves nothing registered" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // CONTRACT-PARTNER GUARD — structurally incapable of being RED pre-fix.
    // The abort path's job is to UNDO merge's managed-set mutation. Pre-fix
    // merge performs no such mutation, so "the set is back to {base.txt}" is
    // vacuously true and this scenario cannot exercise the defect. It becomes
    // load-bearing only once the fix introduces the write, and it must stay
    // green through the fix: a registration that undo does not reverse leaves a
    // phantom managed entry for a file undo just deleted.
    var fx = try buildAddOnlyBranch(gpa, io, tmp.dir);
    defer fx.deinit(gpa);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", "feature" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    {
        var un = try runZit(gpa, io, tmp.dir, &.{"undo"});
        defer freeResult(gpa, &un);
        try expectExit(un.term, 0);
    }

    try std.testing.expect(!fileExists(io, tmp.dir, "feat.txt"));
    try expectManaged(gpa, io, tmp.dir, &.{"base.txt"});
    try expectNoExtras(gpa, io, tmp.dir);
    try expectMergeState(io, tmp.dir, fx.current, "");
}

// --- REGRESSION B6 --------------------------------------------------------
//
// `tag` is read-only: only `tag list` exists, so a repository cannot be given
// a name for a check-in at all (blocker B6, docs/plans/CLI-VERB-BREAKDOWN.md
// line 27 / line 132: "tag | PARTIAL | read-only — `tag list` only, no
// create"). The fix mints a CONTROL artifact carrying `T +sym-NAME <hash>` —
// a SINGLETON (`+`), not a propagating (`*`) tag: repostate's `resolveName`
// (src/repostate.zig:891) picks the max-event.mtime check-in among ALL active
// sym rows, so a propagating sym would be inherited by every descendant and
// silently resolve to the newest one — a B1-class wrong answer. `applyTagOps`
// (src/repostate.zig:727) gates propagation on `op.type != .singleton`, so a
// singleton card flows through the existing machinery unchanged.
//
// Plan §4 requires a ROUND TRIP, not just local visibility: create, see it in
// the tag renderings, then confirm the T-card survives `pack` and a clone — a
// synced artifact that does not survive sync is a worse lie than a missing
// verb. Note for the implementer: neither `info` nor `timeline` renders tags
// today (src/main.zig:4980 renderInfo prints id/branch/leaf/parents only;
// runTimeline never calls `tagsOf`), so `tag list CHECKIN` IS the per-check-in
// render surface these tests assert against — extending info/timeline is not
// in B6's minimal scope.

/// Mints a one-commit trunk and returns that check-in's 64-hex hash (caller
/// frees). `a.txt` holds "root\n"; the checkout is left on it.
fn b6BuildRoot(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]const u8 {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "a.txt", "root\n");
    try addManaged(gpa, io, dir, &.{"a.txt"});
    return commitHashDup(gpa, io, dir, "root");
}

test "cli REGRESSION B6: tag add creates a tag that tag list and tag list CHECKIN report" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try b6BuildRoot(gpa, io, tmp.dir);
    defer gpa.free(root);

    // THE MISSING CAPABILITY. Pre-fix this is `bad_subcommand` -> exit 2.
    {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "tag", "add", "v1", root });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("", res.stderr);
    }

    // §5.1/§2 no-arg listing: only the new singleton symbolic release name
    // appears; the propagating trunk machinery is excluded.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("v1\n", res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }

    // §5.2 CHECKIN listing: the singleton tag is on the named check-in.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", root });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("v1\n", res.stdout);
    }

    // §8.1/§8.2 resolution: the name now names that check-in everywhere.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "info", "v1" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.startsWith(u8, res.stdout, root));
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "a.txt", "v1" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("root\n", res.stdout);
    }
}

test "cli REGRESSION B6: a created tag pins to its check-in and is not inherited by a descendant (tag before commit)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try b6BuildRoot(gpa, io, tmp.dir);
    defer gpa.free(root);

    // ORDERING A: tag first, then mint the descendant. A propagating (`*`)
    // sym card would be inherited by everything committed AFTER it.
    {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "tag", "add", "v1", root });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    try writeFile(io, tmp.dir, "a.txt", "child\n");
    const child = try commitHashDup(gpa, io, tmp.dir, "child");
    defer gpa.free(child);
    try std.testing.expect(!std.mem.eql(u8, root, child));

    try b6ExpectPinned(gpa, io, tmp.dir, root, child);
}

test "cli REGRESSION B6: a created tag pins to its check-in and is not inherited by a descendant (commit before tag)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try b6BuildRoot(gpa, io, tmp.dir);
    defer gpa.free(root);

    // ORDERING B: mint the descendant FIRST, then tag the ancestor. The
    // propagation walk in `applyTagOps` runs over already-crosslinked
    // descendants here, a different code path from ordering A's inheritance
    // at commit time. Identical behavior in both orderings is what proves the
    // singleton mechanism rather than luck.
    try writeFile(io, tmp.dir, "a.txt", "child\n");
    const child = try commitHashDup(gpa, io, tmp.dir, "child");
    defer gpa.free(child);
    try std.testing.expect(!std.mem.eql(u8, root, child));
    {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "tag", "add", "v1", root });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    try b6ExpectPinned(gpa, io, tmp.dir, root, child);
}

/// The pinning oracle shared by both orderings: `v1` is on `root` only, the
/// descendant does NOT list it, and the name still resolves to `root` even
/// though `child` is strictly newer (max-mtime would pick `child` if the row
/// had propagated).
fn b6ExpectPinned(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    root: []const u8,
    child: []const u8,
) !void {
    {
        var res = try runZit(gpa, io, dir, &.{ "tag", "list", root });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "v1\n") != null);
    }
    {
        var res = try runZit(gpa, io, dir, &.{ "tag", "list", child });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "v1\n") == null);
    }
    {
        var res = try runZit(gpa, io, dir, &.{ "info", "v1" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.startsWith(u8, res.stdout, root));
    }
    // The content oracle: the tagged revision's bytes, not the descendant's.
    {
        var res = try runZit(gpa, io, dir, &.{ "cat", "a.txt", "v1" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("root\n", res.stdout);
    }
}

test "cli REGRESSION B6: a created tag survives pack" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try b6BuildRoot(gpa, io, tmp.dir);
    defer gpa.free(root);
    {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "tag", "add", "v1", root });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"pack"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    // The T-card is permanent repository content: packing must not drop it.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("v1\n", res.stdout);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "info", "v1" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.startsWith(u8, res.stdout, root));
    }
}

test "cli REGRESSION B6: a created tag survives a clone" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    const root = try b6BuildRoot(gpa, io, tmp_a.dir);
    defer gpa.free(root);
    {
        var res = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "tag", "add", "v1", root });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    const b6_clone_port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, b6_clone_port);
    defer server.kill(io);
    try waitForListen(io, b6_clone_port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{b6_clone_port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    // A tag that does not survive sync is a worse lie than a missing verb.
    {
        var res = try runZit(gpa, io, b_dir, &.{ "tag", "list" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("v1\n", res.stdout);
    }
    {
        var res = try runZit(gpa, io, b_dir, &.{ "tag", "list", root });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "v1\n") != null);
    }
    {
        var res = try runZit(gpa, io, b_dir, &.{ "info", "v1" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.startsWith(u8, res.stdout, root));
    }
}

// --- REGRESSION B3 --------------------------------------------------------
//
// A symlink anywhere in the tree permanently wedges `commit` (blocker B3,
// docs/plans/CLI-VERB-BREAKDOWN.md §B3). `extras.zig` classifies symlinks as
// extras and the recursive `add`/`addremove` sweep reuses that walk without
// re-applying the exclusion, so `add .` TRACKS exactly what explicit `zit add
// sym.txt` and `commit` both REFUSE. `status` then calls a file that is on
// disk "missing", and the recovery (`zit rm sym.txt`) is undiscoverable.
//
// Contract of record (BLOCKER-FIX-PLAN.md §2 F-B3): consistent refusal —
// 0.1.0 does not version symlinks. The recursive paths skip them (add reports
// each skip on stderr, addremove withholds them silently), explicit `add`
// keeps refusing with the symlink wording, and `commit`'s refusal for an
// already-wedged repo names `zit rm PATH`.

test "cli REGRESSION B3: `add .` skips a symlink, so commit succeeds, status is clean and extras lists it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // The breakdown doc's repro verbatim:
    //   zit init -q && echo a > a.txt && ln -s a.txt sym.txt
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try tmp.dir.symLink(io, "a.txt", "sym.txt", .{});

    // `zit add .` — must NOT track sym.txt, and must SAY it skipped it.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "add", "." });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("ADDED  a.txt\n", res.stdout);
        try std.testing.expect(std.mem.indexOf(
            u8,
            res.stderr,
            "skipped 'sym.txt': symlink (not versioned in 0.1.0)",
        ) != null);
    }
    try expectManaged(gpa, io, tmp.dir, &.{"a.txt"});

    // `zit commit -m t` — must succeed. Today it dies with
    // "cannot commit 'sym.txt': not a regular file" and the repo is stuck.
    {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "t" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    // `zit status` — must NOT report sym.txt missing; it is on disk.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"status"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "MISSING    sym.txt") == null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "sym.txt") == null);
    }

    // `zit extras` — must list sym.txt: untracked is where a symlink belongs.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("sym.txt\n", res.stdout);
    }
}

test "cli REGRESSION B3: `addremove` withholds symlinks in both sweep orderings" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // BOTH orderings relative to the regular file the sweep also yields: the
    // extras walk is sorted by path, so `aaa_sym` is visited BEFORE `m.txt`
    // and `zzz_sym` AFTER it. A skip that only works on one side of a regular
    // entry (e.g. a filter that latches off the previous item's kind) fails
    // exactly one of these two assertions.
    try writeFile(io, tmp.dir, "m.txt", "m\n");
    try tmp.dir.symLink(io, "m.txt", "aaa_sym", .{});
    try tmp.dir.symLink(io, "m.txt", "zzz_sym", .{});

    {
        var res = try runZit(gpa, io, tmp.dir, &.{"addremove"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        // Neither symlink is ADDED, in either ordering.
        try std.testing.expectEqualStrings("ADDED m.txt\n", res.stdout);
    }

    // Neither symlink is tracked, and both remain extras.
    try expectManaged(gpa, io, tmp.dir, &.{"m.txt"});
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("aaa_sym\nzzz_sym\n", res.stdout);
    }

    // The whole point: commit is no longer wedged.
    {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "t" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
}

test "cli REGRESSION B3: explicit `add sym.txt` refuses with the symlink wording, tracking nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try tmp.dir.symLink(io, "a.txt", "sym.txt", .{});

    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "sym.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    // Today: "zit add: 'sym.txt' is not a regular file" — a fifo and a symlink
    // are not the same refusal and must not read the same.
    try std.testing.expect(std.mem.indexOf(
        u8,
        res.stderr,
        "symlink (not versioned in 0.1.0)",
    ) != null);
    try std.testing.expect(!sectionExists(gpa, io, tmp.dir, .managed));
}

test "cli REGRESSION B3: a wedged repo recovers via the `zit rm` the commit error names" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Build the wedge the way an F4-era `add .` left real repos: `sym.txt` is
    // MANAGED and is a symlink on disk. Tracking it while it is still a
    // regular file is the only construction that survives the fix (post-fix
    // neither `add .` nor `add sym.txt` will register it) — the wedged repos
    // this recovery exists for were created before the fix.
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try writeFile(io, tmp.dir, "sym.txt", "placeholder\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "sym.txt" });
    try tmp.dir.deleteFile(io, "sym.txt");
    try tmp.dir.symLink(io, "a.txt", "sym.txt", .{});

    // commit refuses — correctly — but must name the way out. Today the
    // message is "cannot commit 'sym.txt': not a regular file" and stops
    // there, which is why the blocker was near-undiagnosable in the field.
    {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "t" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "zit rm sym.txt") != null);
    }

    // Follow the recovery the message gives, verbatim.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "rm", "sym.txt" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    {
        var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "t" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"status"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "nothing to report") != null);
    }
    // The symlink is still on disk and is now, correctly, an extra.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("sym.txt\n", res.stdout);
    }
}

test "cli REGRESSION B3: `clean --dry-run` still lists a symlink as an extra" {
    // CONTRACT-PARTNER GUARD, not a repro. clean is a pure READER of the
    // extras sweep — it never inserts into the managed set — so it is
    // structurally incapable of exhibiting the B3 wedge and was green before
    // the fix. It is here because the fix widens `extras.Extras` entries to
    // carry an is_symlink tag and touches clean's call site: clean must keep
    // IGNORING that tag (symlinks stay deletable extras) while add/addremove
    // filter on it. It must stay green through the fix.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try tmp.dir.symLink(io, "a.txt", "sym.txt", .{});
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    var res = try runZit(gpa, io, tmp.dir, &.{ "clean", "--dry-run" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("sym.txt\n", res.stdout);
}

// ==========================================================================
// REGRESSION B4 — non-atomic `mv` (records the rename before moving the file,
// with no rollback when the move fails).
//
// Repro of record (docs/plans/CLI-VERB-BREAKDOWN.md "B4 — non-atomic mv"),
// reproduced verbatim against a fresh trunk build before these tests were
// written:
//
//   $ zit mv sub/a2.txt nodir/x.txt
//     recorded 'sub/a2.txt' as 'nodir/x.txt' but could not move the working
//     file: input/output error
//   $ zit status                      # missing nodir/x.txt  (file still at sub/a2.txt)
//   $ zit mv nodir/x.txt sub/a2.txt   # 'sub/a2.txt' already exists on disk
//
// A refused mv must be a ZERO state change, and the refusal chain is not
// observable through `status` alone: the managed set mv persists is the same
// set `extras` subtracts from and `commit` walks, so each of the three is
// asserted. The two failure causes below (missing dest parent → ENOENT,
// unwritable dest parent → EACCES) are the same defect reached two ways;
// identical wrong state in both is what shows the mechanism is the WRITE/MOVE
// ORDERING and not one errno.
// ==========================================================================

/// Sets up a repository with a single committed file at `sub/a2.txt` — the
/// baseline all four B4 tests start from. A committed baseline (rather than a
/// merely-added one) is what makes `commit`'s "nothing to commit" the honest
/// oracle for "nothing was recorded".
fn b4Baseline(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !void {
    try initRepo(gpa, io, dir);
    try dir.createDir(io, "sub", .default_dir);
    try writeFile(io, dir, "sub/a2.txt", "hello\n");
    try addManaged(gpa, io, dir, &.{"sub/a2.txt"});
    var res = try runCommitEnv(gpa, io, dir, .{}, &.{ "commit", "-m", "base" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
}

/// Asserts the repository is byte-for-byte the b4Baseline state: the file is
/// still at `sub/a2.txt` with its original bytes, the managed set still names
/// exactly that path, `status` reports nothing, `extras` is empty (a stranded
/// working file would show up here), and `commit` has nothing to record.
fn expectB4Untouched(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !void {
    try std.testing.expect(fileExists(io, dir, "sub/a2.txt"));
    const bytes = try readFile(gpa, io, dir, "sub/a2.txt");
    defer gpa.free(bytes);
    try std.testing.expectEqualStrings("hello\n", bytes);
    try expectManaged(gpa, io, dir, &.{"sub/a2.txt"});

    {
        var res = try runZit(gpa, io, dir, &.{"status"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "nothing to report") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "missing") == null);
    }
    {
        var res = try runZit(gpa, io, dir, &.{"extras"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("", res.stdout);
    }
    {
        var res = try runCommitEnv(gpa, io, dir, .{ .epoch = "1234567891" }, &.{ "commit", "-m", "after" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expectEqualStrings("zit commit: nothing to commit (no changes)\n", res.stderr);
    }
    // The DIRECT oracle for "nothing was recorded". Everything above is
    // indirect: commit's "no changes" reflects CONTENT only, so a stranded
    // rename record passes all of it silently and then surfaces at the next
    // commit that does carry a change, as a §4.7.6 old-name F card claiming a
    // former name that no manifest ever held. Read the record itself.
    {
        var set = try managed.ManagedSet.read(gpa, io, dir);
        defer set.deinit();
        try std.testing.expect(set.pendingRenames().len == 0);
    }
}

test "cli REGRESSION B4: a failed mv into a missing directory records nothing" {
    // REGRESSION B4: the repro of record. `nodir` does not exist, so the
    // rename cannot succeed; today mv persists the rename FIRST and then
    // reports the failure, leaving the set naming a path that is not on disk.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try b4Baseline(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "sub/a2.txt", "nodir/x.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    // A refusal, not a report of a half-done rename.
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "recorded") == null);
    // SF3 (mv-scoped): ENOENT must not be rendered as "input/output error",
    // and the message must name the directory that is missing.
    try std.testing.expectEqualStrings(
        "zit mv: cannot move into 'nodir': no such directory\n",
        res.stderr,
    );

    try std.testing.expect(!fileExists(io, tmp.dir, "nodir/x.txt"));
    try expectB4Untouched(gpa, io, tmp.dir);
}

test "cli REGRESSION B4: a failed mv into an unwritable directory records nothing" {
    // REGRESSION B4: the SAME defect reached by a different errno. `ro` exists
    // (so a dest-parent-exists pre-check cannot catch it) but is not writable,
    // so the rename itself fails with EACCES. Identical wrong state to the
    // ENOENT case is what proves the defect is the record-before-move ordering
    // rather than one specific failure cause — and this is the case that will
    // still reach the rename handler after the fix, so it is the one that
    // pins the rollback path.
    if (comptime !File.Permissions.has_executable_bit) return error.SkipZigTest;
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try b4Baseline(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, "ro", .default_dir);
    // Restored before tmp.cleanup, which
    // needs the directory writable again.
    defer tmp.dir.setFilePermissions(io, "ro", File.Permissions.fromMode(0o700), .{ .follow_symlinks = false }) catch {};
    try tmp.dir.setFilePermissions(io, "ro", File.Permissions.fromMode(0o500), .{ .follow_symlinks = false });

    var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "sub/a2.txt", "ro/x.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "recorded") == null);
    // SF3 (mv-scoped): EACCES is neither "input/output error" nor a raw Zig
    // error name — it is the same human phrase `clean` and `init` already print.
    try std.testing.expectEqualStrings(
        "zit mv: cannot move 'sub/a2.txt' to 'ro/x.txt': permission denied\n",
        res.stderr,
    );

    try std.testing.expect(!fileExists(io, tmp.dir, "ro/x.txt"));
    try expectB4Untouched(gpa, io, tmp.dir);
}

test "cli REGRESSION B4: a failed mv leaves the source tracked and still movable" {
    // REGRESSION B4: the user-visible cost of the stranded record. The
    // breakdown's documented recovery (`zit mv NEW OLD`) fails after a failed
    // mv because the record and the filesystem disagree; the honest fix is
    // that no recovery is needed at all — the source is untouched, so an
    // ordinary mv of it just works. Today it does not: the source is no
    // longer tracked, so mv refuses it.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try b4Baseline(gpa, io, tmp.dir);

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "sub/a2.txt", "nodir/x.txt" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "sub/a2.txt", "sub/c.txt" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("RENAME sub/a2.txt sub/c.txt\n", res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }
    try expectManaged(gpa, io, tmp.dir, &.{"sub/c.txt"});
    try std.testing.expect(fileExists(io, tmp.dir, "sub/c.txt"));
    try std.testing.expect(!fileExists(io, tmp.dir, "sub/a2.txt"));
}

test "cli REGRESSION B4: `zit mv NEW OLD` reverses a successful rename exactly" {
    // CONTRACT-PARTNER GUARD, not a repro. Both renames here SUCCEED, so no
    // rename-failure path is entered and the record/move ordering is
    // unobservable — this scenario is structurally incapable of exhibiting the
    // B4 defect and was green before the fix. It is here because the fix
    // reorders mvCommit around the same recordRename bookkeeping: `mv b a`
    // after `mv a b` must net the rename record back out, or the reversal
    // would leave a stray old-name F card and `commit` would see a change.
    // It must stay green through the fix — it is what makes `mv --help`'s
    // "to reverse a rename, run `zit mv NEW OLD`" promise true.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try b4Baseline(gpa, io, tmp.dir);

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "sub/a2.txt", "sub/b.txt" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "mv", "sub/b.txt", "sub/a2.txt" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    try std.testing.expect(!fileExists(io, tmp.dir, "sub/b.txt"));
    // Exactly the baseline again — including a commit that finds no change,
    // which is the assertion that no stray rename record survived the round trip.
    try expectB4Untouched(gpa, io, tmp.dir);
}

// ---------------------------------------------------------------------------
// `zit stat` — the store's own byte accounting and artifact roll.
// ---------------------------------------------------------------------------

/// Extracts the u64 value of a `key value\n` line from `zit stat` output.
/// Fails the test if the key is absent, so a dropped field cannot pass as a
/// zero.
fn statField(out: []const u8, key: []const u8) !u64 {
    var lines = std.mem.splitScalar(u8, out, '\n');
    while (lines.next()) |line| {
        var it = std.mem.splitScalar(u8, line, ' ');
        const k = it.next() orelse continue;
        if (!std.mem.eql(u8, k, key)) continue;
        const v = it.next() orelse return error.TestUnexpectedResult;
        return std.fmt.parseInt(u64, v, 10);
    }
    std.debug.print("`zit stat` output has no '{s}' field:\n{s}\n", .{ key, out });
    return error.TestUnexpectedResult;
}

/// Independent oracles for the two measured byte fields, sourced from the
/// store's own per-rid index rather than from `storestat.walk` — which is what
/// the verb totals through. Sharing no arithmetic with the code under test is
/// the whole point: a `content-bytes` the verb computed some other way has to
/// match a number assembled here from `payloadLenOf`, frame by frame.
const StatOracle = struct {
    /// Sum of the on-disk payload length of every non-config record.
    content: u64,
    /// Sum of each of those frames' header + padding (`frameSpanOf` less the
    /// payload), which is what `frame-overhead-bytes` must equal.
    overhead: u64,
    artifacts: u64,
};

fn statOracle(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !StatOracle {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_only });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);

    const rids = try log.ridsInPersistedOrder(gpa);
    defer gpa.free(rids);
    var o = StatOracle{ .content = 0, .overhead = 0, .artifacts = 0 };
    for (rids) |rid| {
        if (log.kindOf(rid) == .config) continue;
        const payload = log.payloadLenOf(rid);
        o.content += payload;
        o.overhead += log.frameSpanOf(rid) - payload;
        o.artifacts += 1;
    }
    return o;
}

/// Builds a repository with two committed generations of one file plus a
/// second file, so the store holds manifests, full-form blobs, and at least
/// one delta frame.
fn statCorpus(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !void {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "poem.txt", "the sea is calm tonight\n" ** 40);
    try writeFile(io, dir, "notes.txt", "alpha beta gamma\n" ** 20);
    try addManaged(gpa, io, dir, &.{ "poem.txt", "notes.txt" });
    const h1 = try commitHashDup(gpa, io, dir, "first");
    gpa.free(h1);
    try writeFile(io, dir, "poem.txt", "the sea is calm tonight\n" ** 39 ++ "the tide is full\n");
    const h2 = try commitHashDup(gpa, io, dir, "second");
    gpa.free(h2);
}

test "e2e stat: content bytes are measured by the frame walk and never the file size" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try statCorpus(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{"stat"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr);

    // store-bytes is the file; nothing else may claim to be it.
    var f = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_only });
    defer f.close(io);
    const st = try f.stat(io);
    try std.testing.expectEqual(st.size, try statField(res.stdout, "store-bytes"));

    // THE honesty property, against an oracle the verb does not share code
    // with: content-bytes must equal the sum of the stored frames' payload
    // lengths, taken from the store's own index (`statOracle`). A figure
    // derived from the `.zit` size fails here — `st.size` itself, `st.size`
    // less the header, the total on-disk frame span, any of them — because the
    // payload total is assembled frame by frame and never touches the size.
    //
    // What this cannot distinguish, and no test on the printed value can: a
    // figure that subtracts the walk's OWN overhead and config totals from the
    // size. Exact accounting makes that arithmetically identical to the
    // measurement — it IS the measurement, routed through a subtraction. The
    // property under test is the number, not the expression that produced it.
    const oracle = try statOracle(gpa, io, tmp.dir);
    const content = try statField(res.stdout, "content-bytes");
    try std.testing.expect(oracle.content > 0); // the corpus really has content
    try std.testing.expectEqual(oracle.content, content);
    try std.testing.expect(content < st.size);

    // frame-overhead-bytes is independently sourced the same way, so the
    // overhead cannot be back-computed as "whatever makes the file add up"
    // while content is wrong by the same amount.
    const overhead = try statField(res.stdout, "frame-overhead-bytes");
    try std.testing.expect(oracle.overhead > 0);
    try std.testing.expectEqual(oracle.overhead, overhead);

    // Exact accounting, reproduced at the CLI surface: every byte of the file
    // is either the header, a frame payload, or frame overhead. With content
    // and overhead both pinned above, this pins config-bytes too.
    const header = try statField(res.stdout, "header-bytes");
    const config = try statField(res.stdout, "config-bytes");
    try std.testing.expectEqual(st.size, header + content + overhead + config);
    try std.testing.expect(config > 0); // the config record is a real frame

    // The artifact census comes from the same walk as the bytes.
    const digests = try collectArtifactDigests(gpa, io, tmp.dir);
    defer freeDigests(gpa, digests);
    try std.testing.expectEqual(@as(u64, digests.len), try statField(res.stdout, "artifacts"));
    try std.testing.expectEqual(oracle.artifacts, @as(u64, digests.len));
    const deltas = try statField(res.stdout, "deltas");
    const full = try statField(res.stdout, "full-form");
    try std.testing.expectEqual(@as(u64, digests.len), deltas + full);
    try std.testing.expect(deltas > 0); // the second poem.txt generation deltas
}

test "e2e stat: --artifacts enumerates every stored hash, one per line, sorted" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try statCorpus(gpa, io, tmp.dir);

    var res = try runZit(gpa, io, tmp.dir, &.{ "stat", "--artifacts" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("", res.stderr);

    const digests = try collectArtifactDigests(gpa, io, tmp.dir);
    defer freeDigests(gpa, digests);

    var expected: std.ArrayListUnmanaged(u8) = .empty;
    defer expected.deinit(gpa);
    for (digests) |d| {
        var hex: [2 * logstore.max_hash_len + 1]u8 = undefined;
        const line = try std.fmt.bufPrint(&hex, "{x}\n", .{d});
        try expected.appendSlice(gpa, line);
    }
    // Byte-for-byte: same set, same sorted order, one hash per line.
    try std.testing.expectEqualStrings(expected.items, res.stdout);
}

test "e2e stat: --type=delta selects the delta-form artifacts only" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try statCorpus(gpa, io, tmp.dir);

    var all = try runZit(gpa, io, tmp.dir, &.{ "stat", "--artifacts" });
    defer freeResult(gpa, &all);
    try expectExit(all.term, 0);

    var only_delta = try runZit(gpa, io, tmp.dir, &.{ "stat", "--artifacts", "--type=delta" });
    defer freeResult(gpa, &only_delta);
    try expectExit(only_delta.term, 0);

    var only_full = try runZit(gpa, io, tmp.dir, &.{ "stat", "--artifacts", "--type=full" });
    defer freeResult(gpa, &only_full);
    try expectExit(only_full.term, 0);

    const n = std.mem.count(u8, all.stdout, "\n");
    const nd = std.mem.count(u8, only_delta.stdout, "\n");
    const nf = std.mem.count(u8, only_full.stdout, "\n");
    try std.testing.expect(nd > 0);
    try std.testing.expect(nf > 0);
    try std.testing.expectEqual(n, nd + nf); // the filter partitions, it does not drop
    // The counts agree with the summary from the same walk.
    var summary = try runZit(gpa, io, tmp.dir, &.{"stat"});
    defer freeResult(gpa, &summary);
    try std.testing.expectEqual(@as(u64, nd), try statField(summary.stdout, "deltas"));
    try std.testing.expectEqual(@as(u64, nf), try statField(summary.stdout, "full-form"));
}

/// The verbatim `zit stat` help — an independent transcription of `main.zig`'s
/// `stat_help`, the same house convention every other verb's help is held to,
/// so a drift between the two is caught here. The field list is the verb's
/// documented contract; editing one side alone must not be silent.
const stat_help_expected =
    \\usage: zit stat [--artifacts] [--type=full|delta] [-R <path>] [-h|--help]
    \\
    \\Report what the repository store actually holds. Every number is read
    \\from the frames in `.zit` — nothing is estimated, and nothing is derived
    \\from the file's size.
    \\
    \\Output is one 'field value' line per fact, so it reads by machine:
    \\
    \\  project-code          this repository's shared sync identity, the
    \\                        value a peer matches on and the first input to
    \\                        an authenticated push's derived secret. Empty on
    \\                        a repository that predates project codes.
    \\  hash-policy           which names this repository mints
    \\  store-bytes           the `.zit` file itself, in bytes
    \\  header-bytes          the store header
    \\  content-bytes         artifact payload bytes, summed frame by frame
    \\  frame-overhead-bytes  frame headers and 8-byte padding for those frames
    \\  config-bytes          the repository's config record
    \\  shun-bytes            the repository's shun table
    \\  shun-rows             rows in that table
    \\  user-bytes            the repository's user table
    \\  user-frames           frames in that table -- rows plus any
    \\                        supersede markers a pack has not yet reclaimed,
    \\                        so this can exceed what `zit users` prints
    \\  artifacts             stored artifacts (the config record is not one)
    \\  full-form             artifacts stored whole
    \\  full-form-bytes       content-bytes held by those artifacts
    \\  deltas                artifacts stored against a base
    \\  delta-bytes           content-bytes held by those artifacts
    \\
    \\-R names a repository with no working tree, so a bare store answers all
    \\of the above without a checkout, a port, or a child process.
    \\
    \\content-bytes and store-bytes are different measurements and must not be
    \\substituted for one another: the store also carries the header, a header
    \\per frame, and padding, so it is always the larger number. The five byte
    \\fields plus header-bytes account for the file exactly.
    \\
    \\full-form-bytes and delta-bytes PARTITION content-bytes; they are not
    \\further components of the file and must not be added to the accounting
    \\above. Read them against full-form/deltas rather than instead of them: a
    \\store is routinely a minority full-form by artifact and a large majority
    \\full-form by byte, because an artifact is stored whole exactly when
    \\delta did not pay -- the large, poorly-compressible ones.
    \\
    \\options:
    \\  -h, --help    show this help and exit
    \\  --artifacts   list the stored artifact hashes instead, one per line,
    \\                in ascending hash order
    \\  --type=KIND   with --artifacts, list only 'full' or 'delta' artifacts
    \\
    \\exit status:
    \\  0  the report was written
    \\  1  this is not a zit repository, or `.zit` could not be read
    \\  2  usage error (an unknown option, a repeated --type, a --type without
    \\     --artifacts, or an unexpected argument)
    \\
;

test "e2e stat: --help prints the verbatim help to stdout, exit 0, outside a repository too" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Help precedes every other reading of the command line: before the
    // repository lookup, and ahead of a trailing bad token or a real option.
    const invocations = [_][]const []const u8{
        &.{ "stat", "--help" },
        &.{ "stat", "-h" },
        &.{ "stat", "--help", "foo" },
        &.{ "stat", "-h", "--all" },
        &.{ "stat", "--artifacts", "--help" },
    };
    for (invocations) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings(stat_help_expected, res.stdout);
        try std.testing.expectEqualStrings("", res.stderr);
    }

    // `zit help stat` routes to the same block, not a second copy of it.
    try initRepo(gpa, io, tmp.dir);
    var topic = try runZit(gpa, io, tmp.dir, &.{ "help", "stat" });
    defer freeResult(gpa, &topic);
    try expectExit(topic.term, 0);
    try std.testing.expectEqualStrings(stat_help_expected, topic.stdout);
}

test "e2e stat: a repeated --type is a usage error, not a silent last-wins" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try statCorpus(gpa, io, tmp.dir);

    // Two --type values cannot both be honoured, and answering the second one
    // silently would report a roll the user did not ask for.
    var res = try runZit(gpa, io, tmp.dir, &.{ "stat", "--artifacts", "--type=full", "--type=delta" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 2);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "--type") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "more than once") != null);

    // The same value twice is still two --type options; it is not a special case.
    var same = try runZit(gpa, io, tmp.dir, &.{ "stat", "--artifacts", "--type=full", "--type=full" });
    defer freeResult(gpa, &same);
    try expectExit(same.term, 2);
    try std.testing.expectEqualStrings("", same.stdout);
}

// --- `-n N` entry limit and `-R` repository override -------------------------
//
// Two options over one shared set of read verbs. `-n` follows fossil, including
// its deliberate per-verb overload (`timeline -n 20` limits, `add -n` is a dry
// run); `-R` follows `fossil -R`, naming a repository that need not have a
// working tree attached.

/// Builds `n` check-ins in `dir`, each touching the same file. The commits land
/// as fast as the machine allows, so several routinely share a one-second
/// timestamp — which is the case the limit must still order correctly.
fn seedHistory(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, n: usize) !void {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "f.txt", "0\n");
    {
        var res = try runZit(gpa, io, dir, &.{ "add", "-q", "f.txt" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    var i: usize = 0;
    while (i < n) : (i += 1) {
        var buf: [64]u8 = undefined;
        const body = try std.fmt.bufPrint(&buf, "line {d}\n", .{i});
        try writeFile(io, dir, "f.txt", body);
        var msg_buf: [32]u8 = undefined;
        const msg = try std.fmt.bufPrint(&msg_buf, "c{d}", .{i});
        var res = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-q", "-m", msg });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
}

fn countLines(s: []const u8) usize {
    return std.mem.count(u8, s, "\n");
}

test "cli timeline -n: the limit is a strict PREFIX of the unlimited listing, ties included" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedHistory(gpa, io, tmp.dir, 12);

    var full = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &full);
    try expectExit(full.term, 0);
    try std.testing.expectEqual(@as(usize, 12), countLines(full.stdout));

    // The load-bearing assertion. Comparing only the ROW COUNT would pass even
    // if the limit selected an arbitrary N — the defect the ordering fix
    // existed to remove. Requiring the output to be the first N BYTES of the
    // unlimited listing can only hold if the cap is applied after the total
    // order, so this line fails for any wrong-N-check-ins implementation.
    for ([_]usize{ 1, 5, 11, 12 }) |n| {
        var num_buf: [8]u8 = undefined;
        const num = try std.fmt.bufPrint(&num_buf, "{d}", .{n});
        var res = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-n", num });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqual(n, countLines(res.stdout));

        var end: usize = 0;
        var seen: usize = 0;
        while (seen < n) : (seen += 1) end = std.mem.indexOfScalarPos(u8, full.stdout, end, '\n').? + 1;
        try std.testing.expectEqualStrings(full.stdout[0..end], res.stdout);
    }
}

test "cli timeline -n: 0 is unlimited, a cap past the history shows everything" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedHistory(gpa, io, tmp.dir, 4);

    inline for (.{ "0", "99" }) |n| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-n", n });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqual(@as(usize, 4), countLines(res.stdout));
    }
    // `--limit` is the long alias for the same option.
    var alias = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "--limit", "2" });
    defer freeResult(gpa, &alias);
    try expectExit(alias.term, 0);
    try std.testing.expectEqual(@as(usize, 2), countLines(alias.stdout));
}

test "cli timeline/log -n: a non-numeric or negative value is refused, never silently defaulted" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedHistory(gpa, io, tmp.dir, 2);

    inline for (.{ "abc", "-3" }) |bad| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-n", bad });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2); // a usage error, not a silent full listing
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "invalid -n value") != null);
    }
}

test "cli log -n: limits the ancestry walk to the N most recent check-ins" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedHistory(gpa, io, tmp.dir, 6);

    // The tip's full ID, read off `leaves` (hash column).
    var leaves = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &leaves);
    try expectExit(leaves.term, 0);
    // The leaf line is Fossil's `(1) time [hash] …`; the twelve-character
    // name inside the brackets resolves like any prefix.
    const lb = std.mem.indexOfScalar(u8, leaves.stdout, '[').? + 1;
    const rb = std.mem.indexOfScalarPos(u8, leaves.stdout, lb, ']').?;
    const tip = leaves.stdout[lb..rb];

    var full = try runZit(gpa, io, tmp.dir, &.{ "log", tip });
    defer freeResult(gpa, &full);
    try expectExit(full.term, 0);
    try std.testing.expectEqual(@as(usize, 6), countLines(full.stdout));

    var lim = try runZit(gpa, io, tmp.dir, &.{ "log", "-n", "3", tip });
    defer freeResult(gpa, &lim);
    try expectExit(lim.term, 0);
    try std.testing.expectEqual(@as(usize, 3), countLines(lim.stdout));
    // Again a prefix, not merely a count: the walk order must be unchanged.
    try std.testing.expect(std.mem.startsWith(u8, full.stdout, lim.stdout));

    // `-n 0` walks the whole ancestry.
    var all = try runZit(gpa, io, tmp.dir, &.{ "log", "-n", "0", tip });
    defer freeResult(gpa, &all);
    try expectExit(all.term, 0);
    try std.testing.expectEqualStrings(full.stdout, all.stdout);
}

test "cli -n stays --dry-run on the working-tree verbs: the overload is per-verb" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "x\n");

    // `add -n f.txt` is a DRY RUN: it must not consume "f.txt" as a limit
    // value, and must leave the managed set empty.
    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "-n", "f.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try expectManaged(gpa, io, tmp.dir, &.{});
}

test "cli -R: a read verb runs against a store from outside any checkout" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var store = std.testing.tmpDir(.{});
    defer store.cleanup();
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    try seedHistory(gpa, io, store.dir, 5);

    // `away` is a plain directory with no `.zit` anywhere: today's discovery
    // walk finds nothing there, which is exactly what `-R` overrides.
    {
        var bare = try runZit(gpa, io, away.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
        defer freeResult(gpa, &bare);
        try expectExit(bare.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, bare.stderr, "no zit checkout here") != null);
    }

    const cwd = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd);
    const store_abs = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &store.sub_path });
    defer gpa.free(store_abs);

    var in_place = try runZit(gpa, io, store.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &in_place);
    try expectExit(in_place.term, 0);

    // Same bytes from outside as from inside — the override changes WHERE the
    // store is found and nothing else.
    var via_r = try runZit(gpa, io, away.dir, &.{ "timeline", "-F", "oneline", "-R", store_abs });
    defer freeResult(gpa, &via_r);
    try expectExit(via_r.term, 0);
    try std.testing.expectEqualStrings(in_place.stdout, via_r.stdout);
    try std.testing.expectEqual(@as(usize, 5), countLines(via_r.stdout));

    // `-R` composes with `-n` in one invocation (the two options land together
    // on the same verbs, so they are exercised together).
    var both = try runZit(gpa, io, away.dir, &.{ "timeline", "-F", "oneline", "-R", store_abs, "-n", "2" });
    defer freeResult(gpa, &both);
    try expectExit(both.term, 0);
    try std.testing.expectEqual(@as(usize, 2), countLines(both.stdout));
    try std.testing.expect(std.mem.startsWith(u8, in_place.stdout, both.stdout));
}

test "cli -R: the long spelling, the .zit file itself, and a path holding no store" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var store = std.testing.tmpDir(.{});
    defer store.cleanup();
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    try seedHistory(gpa, io, store.dir, 3);

    const cwd = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd);
    const store_abs = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &store.sub_path });
    defer gpa.free(store_abs);
    const file_abs = try std.fs.path.join(gpa, &.{ store_abs, ".zit" });
    defer gpa.free(file_abs);
    const away_abs = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &away.sub_path });
    defer gpa.free(away_abs);

    // `--repo <dir>` and `-R <dir>/.zit` both name the same store.
    var long = try runZit(gpa, io, away.dir, &.{ "timeline", "-F", "oneline", "--repo", store_abs });
    defer freeResult(gpa, &long);
    try expectExit(long.term, 0);
    var byfile = try runZit(gpa, io, away.dir, &.{ "timeline", "-F", "oneline", "-R", file_abs });
    defer freeResult(gpa, &byfile);
    try expectExit(byfile.term, 0);
    try std.testing.expectEqualStrings(long.stdout, byfile.stdout);
    try std.testing.expectEqual(@as(usize, 3), countLines(long.stdout));

    // A directory with no `.zit` is reported, never silently resolved to the
    // cwd. It used to report the DISCOVERY message, which advises naming the
    // repository with -R -- to someone who had just done that. The
    // property this test exists for is unchanged; the message now names which
    // of the two `-R` mistakes was made.
    var miss = try runZit(gpa, io, away.dir, &.{ "timeline", "-F", "oneline", "-R", away_abs });
    defer freeResult(gpa, &miss);
    try expectExit(miss.term, 1);
    try std.testing.expectEqualStrings("", miss.stdout);
    try std.testing.expect(std.mem.indexOf(u8, miss.stderr, "no repository at") != null);
    try std.testing.expect(std.mem.indexOf(u8, miss.stderr, "no .zit there") != null);
}

test "cli -R: every read verb accepts it; a bad value is refused rather than guessed" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var store = std.testing.tmpDir(.{});
    defer store.cleanup();
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    try seedHistory(gpa, io, store.dir, 3);

    const cwd = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd);
    const store_abs = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &store.sub_path });
    defer gpa.free(store_abs);

    // The operand-free read verbs all reach the store from outside a checkout.
    inline for (.{ "timeline", "leaves", "branch" }) |verb| {
        var res = try runZit(gpa, io, away.dir, &.{ verb, "-R", store_abs });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(res.stdout.len > 0);
    }

    // A dangling `-R`, an empty value, and a repeated option are all usage
    // errors: none may fall back to discovery.
    {
        var res = try runZit(gpa, io, away.dir, &.{ "timeline", "-R" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
    }
    {
        var res = try runZit(gpa, io, away.dir, &.{ "timeline", "-R", "" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "non-empty value") != null);
    }
    {
        var res = try runZit(gpa, io, away.dir, &.{ "timeline", "-R", store_abs, "--repo", store_abs });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "more than once") != null);
    }
}

test "cli -R: checkout-dependent verbs REJECT it by name rather than ignoring it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var store = std.testing.tmpDir(.{});
    defer store.cleanup();
    var work = std.testing.tmpDir(.{});
    defer work.cleanup();
    try seedHistory(gpa, io, store.dir, 2);
    try initRepo(gpa, io, work.dir);
    try writeFile(io, work.dir, "w.txt", "w\n");

    const cwd = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd);
    const store_abs = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &store.sub_path });
    defer gpa.free(store_abs);

    // Run these from a REAL checkout (`work`), so a verb that wrongly ignored
    // `-R` would succeed against the wrong repository instead of erroring for
    // an unrelated reason. That is what makes the exit-2 assertion meaningful.
    // `diff` left this list: its --from/--to mode honors -R, and
    // its worktree mode refuses in-verb (exit 1) — pinned by the
    // "cli diff -R" test.
    inline for (.{ "status", "extras", "clean", "addremove", "undo", "redo" }) |verb| {
        var res = try runZit(gpa, io, work.dir, &.{ verb, "-R", store_abs });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not supported here") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "-R/--repo") != null);
    }
    // And the ones that take operands, including the write path.
    {
        var res = try runZit(gpa, io, work.dir, &.{ "add", "-R", store_abs, "w.txt" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not supported here") != null);
    }
    {
        var res = try runZit(gpa, io, work.dir, &.{ "commit", "-R", store_abs, "-m", "x" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not supported here") != null);
    }
    // `amend` writes only the repository, yet it still rewrites HISTORY, so `-R`
    // would mint a check-in in the named store from a directory whose checkout
    // the operator never named. The argv is otherwise VALID — `trunk` resolves
    // in the seeded store and `--comment` is a real override — so the only
    // reason it fails is the `-R` refusal, not a bad operand.
    {
        var res = try runZit(gpa, io, work.dir, &.{ "amend", "-R", store_abs, "trunk", "--comment", "x" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not supported here") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "-R/--repo") != null);
    }
    // The rejection is a REFUSAL, not a no-op: nothing was added to the tree.
    try expectManaged(gpa, io, work.dir, &.{});
}

test "cli uv: misuse is refused before any connection" {
    // Nothing listens on port 1: a usage error that reached the network
    // would fail differently, and slowly.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    const url = "http://127.0.0.1:1/";
    const cases = [_]struct { args: []const []const u8, says: []const u8 }{
        .{ .args = &.{ "uv", "bogus", url }, .says = "bogus" },
        .{ .args = &.{ "uv", "list" }, .says = "" },
        .{ .args = &.{ "uv", "list", url, "extra" }, .says = "takes only the URL" },
        .{ .args = &.{ "uv", "pull", url }, .says = "needs a NAME or --all" },
        .{ .args = &.{ "uv", "pull", url, "a", "--all" }, .says = "not both" },
        .{ .args = &.{ "uv", "push", url }, .says = "needs a FILE" },
        .{ .args = &.{ "uv", "push", url, "a", "b", "--as", "c" }, .says = "--as names one FILE" },
        .{ .args = &.{ "uv", "rm", url, "../escape" }, .says = "not an unversioned file name" },
        .{ .args = &.{ "uv", "push", url, "a", "--mtime", "yesterday" }, .says = "--mtime takes seconds" },
        .{ .args = &.{ "uv", "list", url, "-o", "d" }, .says = "belong to 'uv pull'" },
    };
    for (cases) |c| {
        var res = try runZit(gpa, io, tmp.dir, c.args);
        defer freeResult(gpa, &res);
        expectExit(res.term, 2) catch |e| {
            std.debug.print("\nargs {any}\nstderr: {s}\n", .{ c.args, res.stderr });
            return e;
        };
        if (std.mem.indexOf(u8, res.stderr, c.says) == null) {
            std.debug.print("\nargs {any}\nwanted '{s}' in: {s}\n", .{ c.args, c.says, res.stderr });
            return error.TestUnexpectedResult;
        }
    }
    var help = try runZit(gpa, io, tmp.dir, &.{ "uv", "--help" });
    defer freeResult(gpa, &help);
    try expectExitDiag(help.term, 0, help);
    try std.testing.expect(std.mem.startsWith(u8, help.stdout, "usage: zit uv list URL"));
}

test "cli settings: main-branch reads and sets this repository's main line, and only this repository's" {
    // A forge needs a repository's main branch and cannot guess it from
    // names. Fossil keeps it local, so it never arrives by sync: it has to be
    // readable, and settable where the repository is served.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "r", .default_dir);
    var r = try tmp.dir.openDir(io, "r", .{});
    defer r.close(io);
    try initRepo(gpa, io, r);
    try writeFile(io, r, "a.txt", "a\n");
    try addManaged(gpa, io, r, &.{"a.txt"});
    var root = try runCommitEnv(gpa, io, r, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);

    // Unset: the default, and not listed as changed.
    var def = try runZit(gpa, io, r, &.{ "settings", "main-branch" });
    defer freeResult(gpa, &def);
    try expectExitDiag(def.term, 0, def);
    try std.testing.expectEqualStrings("main-branch trunk\n", def.stdout);
    var changed0 = try runZit(gpa, io, r, &.{ "settings", "--changed" });
    defer freeResult(gpa, &changed0);
    try std.testing.expect(std.mem.indexOf(u8, changed0.stdout, "main-branch") == null);

    try writeFile(io, r, "b.txt", "b\n");
    try addManaged(gpa, io, r, &.{"b.txt"});
    var mainline = try runCommitEnv(gpa, io, r, .{ .user = "u" }, &.{ "commit", "-m", "main", "--branch", "main" });
    defer freeResult(gpa, &mainline);
    try expectExitDiag(mainline.term, 0, mainline);

    var set = try runZit(gpa, io, r, &.{ "settings", "main-branch", "main" });
    defer freeResult(gpa, &set);
    try expectExitDiag(set.term, 0, set);
    try std.testing.expectEqualStrings("", set.stderr);
    var changed1 = try runZit(gpa, io, r, &.{ "settings", "--changed" });
    defer freeResult(gpa, &changed1);
    try std.testing.expect(std.mem.indexOf(u8, changed1.stdout, "main-branch main\n") != null);
    // Readable from a bare store, which is how a forge asks.
    var bare = try runZit(gpa, io, tmp.dir, &.{ "settings", "main-branch", "--value", "-R", "r" });
    defer freeResult(gpa, &bare);
    try expectExitDiag(bare.term, 0, bare);
    try std.testing.expectEqualStrings("main\n", bare.stdout);

    // A fact about one repository: no --global value; a name a branch tag
    // cannot carry is refused; a branch not there yet is a note, not a refusal.
    var global = try runZit(gpa, io, r, &.{ "settings", "--global", "main-branch", "x" });
    defer freeResult(gpa, &global);
    try expectExit(global.term, 2);
    var spaced = try runZit(gpa, io, r, &.{ "settings", "main-branch", "a b" });
    defer freeResult(gpa, &spaced);
    try expectExit(spaced.term, 2);
    var ahead = try runZit(gpa, io, r, &.{ "settings", "main-branch", "next" });
    defer freeResult(gpa, &ahead);
    try expectExitDiag(ahead.term, 0, ahead);
    try std.testing.expect(std.mem.indexOf(u8, ahead.stderr, "no open branch is named 'next'") != null);

    // Empty resets it.
    var reset = try runZit(gpa, io, r, &.{ "settings", "main-branch", "" });
    defer freeResult(gpa, &reset);
    try expectExitDiag(reset.term, 0, reset);
    var after = try runZit(gpa, io, r, &.{ "settings", "main-branch" });
    defer freeResult(gpa, &after);
    try std.testing.expectEqualStrings("main-branch trunk\n", after.stdout);
}

test "cli import-fossil: --submanifest keeps the store's main branch" {
    // The main branch never syncs, so a converted store can only learn it
    // from the store it was converted from.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try initRepo(gpa, io, repo);
    try repo.createDir(io, "a", .default_dir);
    try repo.createDir(io, "b", .default_dir);
    try writeFile(io, repo, "a/x", "1\n");
    try writeFile(io, repo, "b/y", "2\n");
    try writeFile(io, repo, "c", "3\n");
    try addManaged(gpa, io, repo, &.{ "a/x", "b/y", "c" });
    var root = try runCommitEnv(gpa, io, repo, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);
    var set = try runZit(gpa, io, repo, &.{ "settings", "main-branch", "main" });
    defer freeResult(gpa, &set);
    try expectExitDiag(set.term, 0, set);

    var conv = try runZit(gpa, io, tmp.dir, &.{ "import-fossil", "repo", "converted", "--submanifest", "--k", "2" });
    defer freeResult(gpa, &conv);
    try expectExitDiag(conv.term, 0, conv);
    var got = try runZit(gpa, io, tmp.dir, &.{ "settings", "main-branch", "--value", "-R", "converted" });
    defer freeResult(gpa, &got);
    try expectExitDiag(got.term, 0, got);
    try std.testing.expectEqualStrings("main\n", got.stdout);
}

test "cli settings: --changed lists only non-defaults, --value prints bare values, autosync pullonly round-trips, and the editor setting drives commit" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var none = try runZit(gpa, io, tmp.dir, &.{ "settings", "--changed" });
    defer freeResult(gpa, &none);
    try expectExitDiag(none.term, 0, none);
    try std.testing.expectEqualStrings("", none.stdout);

    var po = try runZit(gpa, io, tmp.dir, &.{ "settings", "autosync", "pullonly" });
    defer freeResult(gpa, &po);
    try expectExitDiag(po.term, 0, po);
    var bad = try runZit(gpa, io, tmp.dir, &.{ "settings", "autosync", "sideways" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 2);

    try writeFile(io, tmp.dir, "ed.sh", "#!/bin/sh\necho 'from the editor setting' > \"$1\"\n");
    try chmodFile(io, tmp.dir, "ed.sh", 0o755);
    var ed = try runZit(gpa, io, tmp.dir, &.{ "settings", "editor", "./ed.sh" });
    defer freeResult(gpa, &ed);
    try expectExitDiag(ed.term, 0, ed);

    var changed = try runZit(gpa, io, tmp.dir, &.{ "settings", "--changed" });
    defer freeResult(gpa, &changed);
    try expectExitDiag(changed.term, 0, changed);
    try std.testing.expectEqualStrings("autosync pullonly\neditor ./ed.sh\n", changed.stdout);
    var val = try runZit(gpa, io, tmp.dir, &.{ "settings", "--value", "autosync" });
    defer freeResult(gpa, &val);
    try std.testing.expectEqualStrings("pullonly\n", val.stdout);

    // No -m and no terminal: the configured editor supplies the comment.
    try writeFile(io, tmp.dir, "a", "a\n");
    try addManaged(gpa, io, tmp.dir, &.{"a"});
    var cm = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{"commit"});
    defer freeResult(gpa, &cm);
    try expectExitDiag(cm.term, 0, cm);
    var tl = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-n", "1" });
    defer freeResult(gpa, &tl);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "from the editor setting") != null);
}

test "cli settings -R: a bare store answers reads and takes repository- and global-tier writes" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var store = std.testing.tmpDir(.{});
    defer store.cleanup();
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    // The store carries a checkout beside it, but every command below runs from
    // `away` and reaches the store ONLY through -R, so the checkout is never in
    // play -- exactly a forge's bare store, which the settings verb refused to
    // serve at all until bare stores were supported.
    try initRepo(gpa, io, store.dir);

    const cwd = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd);
    const store_abs = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &store.sub_path });
    defer gpa.free(store_abs);
    const away_abs = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &away.sub_path });
    defer gpa.free(away_abs);

    // A bare read: the full listing, with the default value in effect.
    {
        var r = try runZit(gpa, io, away.dir, &.{ "settings", "-R", store_abs });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "autosync on") != null);
    }
    // A repository-tier write lands in the store's config table -- no checkout.
    {
        var r = try runZit(gpa, io, away.dir, &.{ "settings", "-R", store_abs, "autosync", "off" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expectEqualStrings("autosync off\n", r.stdout);
    }
    // ...and the next read through -R reports it back.
    {
        var r = try runZit(gpa, io, away.dir, &.{ "settings", "-R", store_abs, "autosync" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expectEqualStrings("autosync off\n", r.stdout);
    }
    // A bad -R path is one of the two -R mistakes, named as such -- not the
    // discovery message that would tell an operator to supply the flag they
    // already supplied.
    {
        var r = try runZit(gpa, io, away.dir, &.{ "settings", "-R", away_abs, "autosync" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "no repository at") != null);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "no .zit there") != null);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "name it with -R") == null);
    }
}

test "cli config -R: import applies to a bare store's user area, the file still read from the cwd" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var store = std.testing.tmpDir(.{});
    defer store.cleanup();
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    try initRepo(gpa, io, store.dir);

    const cwd = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd);
    const store_abs = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &store.sub_path });
    defer gpa.free(store_abs);

    // The import FILE is written into `away` (the cwd), while -R names the
    // store: only the store is redirected, the path is not.
    try writeFile(io, away.dir, "users.txt",
        \\# The "user" configuration exported from
        \\# repository "tk.fossil"
        \\config /user 61
        \\1700000000 'alice' pw 'deadbeef' cap 's' info 'Admin' jx '{}'
        \\config /user 45
        \\1700000000 'nobody' pw '' cap 'gjorz' info ''
        \\
    );
    {
        var r = try runZit(gpa, io, away.dir, &.{ "config", "import", "users.txt", "-R", store_abs });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "2 rows imported") != null);
    }
    // The rows are readable back through -R -- the same store table `users`
    // already reached on a bare store.
    {
        var u = try runZit(gpa, io, away.dir, &.{ "users", "-R", store_abs });
        defer freeResult(gpa, &u);
        try expectExitDiag(u.term, 0, u);
        try std.testing.expectEqualStrings(
            "alice\ts\t1700000000\tAdmin\nnobody\tgjorz\t1700000000\t\n",
            u.stdout,
        );
    }
}

// --- help audit -----------------------------------------------------------

/// Whether `help` lists `verb` as a command entry — an indented line whose
/// first token is exactly `verb`. Column-anchored on purpose: a bare substring
/// match would pass vacuously when a verb NAME shows up only inside another
/// verb's prose (e.g. "sync" in serve's one-liner, or "stat" inside "status"),
/// so a dropped command line would slip through unnoticed. Indent-width
/// agnostic — it keys on "leading whitespace, then the verb, then a space".
fn helpListsVerb(help: []const u8, verb: []const u8) bool {
    var lines = std.mem.splitScalar(u8, help, '\n');
    while (lines.next()) |line| {
        const body = std.mem.trimStart(u8, line, " ");
        if (body.len == line.len) continue; // no indent → a heading, not an entry
        if (std.mem.startsWith(u8, body, verb) and
            body.len > verb.len and body[verb.len] == ' ')
        {
            return true;
        }
    }
    return false;
}
/// Converts a Zig enum tag into its public CLI spelling. Command words use
/// hyphens, while Zig enum identifiers use underscores.
fn publicVerbName(tag: []const u8, buffer: []u8) []const u8 {
    std.debug.assert(buffer.len >= tag.len);
    for (tag, 0..) |byte, i| {
        buffer[i] = if (byte == '_') '-' else byte;
    }
    return buffer[0..tag.len];
}

test "cli help audit: 'zit help' names every dispatched verb (anti-rot guard)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var res = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);

    // Structural pin: the candidate set is the dispatched `Verb` enum itself,
    // not a hand-copied list, so a verb added to the dispatcher without a help
    // line fails HERE rather than shipping undiscoverable. `.unknown` is the
    // catch-all sentinel — never a command — so it is the sole exclusion.
    inline for (comptime std.enums.values(cli.Verb)) |v| {
        if (v == .unknown) continue;
        // Plumbing exemption: machine verbs are dispatched but
        // deliberately unlisted — another process is their audience, and the
        // porcelain a person browses should not offer them. Their own help
        // and `zit help <verb>` stay pinned by the e2e, so "unlisted"
        // can never rot into "undocumented".
        if (v == .wiki or v == .ticket or v == .forum or v == .technote or v == .attach) continue;
        var public_verb_buffer: [@tagName(v).len]u8 = undefined;
        const public_verb = publicVerbName(@tagName(v), &public_verb_buffer);
        if (!helpListsVerb(res.stdout, public_verb)) {
            std.debug.print(
                "dispatched verb '{s}' is absent from 'zit help' output\n",
                .{public_verb},
            );
            return error.VerbMissingFromHelp;
        }
    }
}

// --- sub-manifest convergence across pull rounds ---------------------------
//
// draft-fossil-submanifests-00 §7: two zit peers converge a tree whose closure
// arrives in stages, over the REAL binaries and the REAL wire. §7.1 fixes the
// shape this test drives: "An interior sub-manifest's own S references become
// known only when it arrives, so a tree of depth d converges in at most d+1
// pull rounds." A's tree is completed between the two rounds, so B necessarily
// sees the intermediate state — a check-in it holds whose file set it cannot
// yet produce — and §5.2's "MUST NOT present a partially resolved tree as a
// complete file set" is asserted there, not assumed.

/// Appends the §6.2 Z self-checksum card to `body` and returns the whole
/// artifact. The digest is the MD5 of every byte before the `Z`, hex-encoded —
/// computed here rather than hard-coded, so the fixture stays correct if a
/// card above it is ever edited.
fn withZCard(gpa: std.mem.Allocator, body: []const u8) ![]u8 {
    var digest: [16]u8 = undefined;
    std.crypto.hash.Md5.hash(body, &digest, .{});
    return std.fmt.allocPrint(gpa, "{s}Z {x}\n", .{ body, &digest });
}

/// One leaf sub-manifest holding `names` as its F run (submanifests §4.2: a
/// SUBMANIFEST carries F, S, and Z only, and never both F and S). Each F card
/// carries a hash because §4.4 forbids a hashless F card in a SUBMANIFEST.
fn leafSubManifest(gpa: std.mem.Allocator, names: []const []const u8) ![]u8 {
    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(gpa);
    for (names) |name| {
        const blob = try sha3HexAlloc(gpa, name);
        defer gpa.free(blob);
        try body.print(gpa, "F {s} {s}\n", .{ name, blob });
    }
    return withZCard(gpa, body.items);
}

test "cli pull submanifests §7.1/§7.3: two peers converge a tree across pull rounds" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: a real repository with a real check-in, so clone has a project
    // code and a checkout to work from.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "one.txt", "seed\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"one.txt"});
    var commit1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit1);
    try expectExit(commit1.term, 0);

    // The tree: two leaves holding contiguous slices of one sorted namespace
    // (submanifests §3.6), and the check-in manifest that references them. Each
    // S card's filename is the first filename of its subtree (§5.1 steps 4/5)
    // and the run strictly ascends (§3.4).
    const leaf_ab = try leafSubManifest(gpa, &.{ "a.txt", "b.txt" });
    defer gpa.free(leaf_ab);
    const leaf_mn = try leafSubManifest(gpa, &.{ "m.txt", "n.txt" });
    defer gpa.free(leaf_mn);
    var name_buf_ab: [64]u8 = undefined;
    var name_buf_mn: [64]u8 = undefined;
    const hash_ab = artifact.artifactName(.sha3_256, leaf_ab, &name_buf_ab);
    const hash_mn = artifact.artifactName(.sha3_256, leaf_mn, &name_buf_mn);

    const tree_body = try std.fmt.allocPrint(
        gpa,
        "C tree\nD 2026-07-27T12:00:00.000\nS a.txt {s}\nS m.txt {s}\nU u\n",
        .{ hash_ab, hash_mn },
    );
    defer gpa.free(tree_body);
    const tree = try withZCard(gpa, tree_body);
    defer gpa.free(tree);

    // ROUND 0: A holds the tree manifest and NEITHER leaf — the §7.1 phantom
    // state, staged deliberately so B must converge across two pulls.
    const tree_hash = try appendArtifact(gpa, io, tmp_a.dir, tree);
    defer gpa.free(tree_hash);

    const port0 = try freshLoopbackPort(io);
    var server0 = try spawnServe(gpa, io, tmp_a.dir, port0);
    defer server0.kill(io);
    try waitForListen(io, port0);
    const url0 = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{port0});
    defer gpa.free(url0);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url0, "repo" });
    defer freeResult(gpa, &clone);

    // §7.1 ON THE CLONE PATH. A clone converges on the cfile cursor, so it
    // faithfully copies what A holds — INCLUDING A's own incomplete closure.
    // "Storing a structural artifact that carries S cards whose referenced
    // sub-manifests are absent MUST create a phantom for each missing
    // reference"; the reference the phantom stands for is precisely what B does
    // not have, so a clone that reports success here would hand the operator a
    // repository whose gap it recorded nowhere and would never ask for. The
    // repository is still written in full — a partial clone is progress — but
    // the exit status says the closure is open, exactly as `pull` already does
    // for artifacts it did not obtain.
    try std.testing.expect(clone.term != .exited or clone.term.exited != 0);
    try std.testing.expect(std.mem.indexOf(u8, clone.stderr, "sub-manifest") != null);
    try std.testing.expect(std.mem.indexOf(u8, clone.stderr, "2 ") != null); // both leaves, not one

    var b_repo = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_repo.close(io);

    // MID-CONVERGENCE, §5.2: B holds the check-in but not its sub-manifests, so
    // its file set MUST NOT be produced. `ls` fails rather than printing the
    // part of the tree B can already reach — a partial enumeration presented as
    // a file set is the exact failure the section forbids.
    var partial = try runZit(gpa, io, b_repo, &.{ "ls", tree_hash });
    defer freeResult(gpa, &partial);
    try std.testing.expect(partial.term != .exited or partial.term.exited != 0);
    try std.testing.expectEqualStrings("", partial.stdout);
    try std.testing.expect(partial.stderr.len != 0);

    // The same holds on A, which is missing the same two artifacts: the gap is
    // a property of the repository's holdings, not of having synced.
    var partial_a = try runZit(gpa, io, tmp_a.dir, &.{ "ls", tree_hash });
    defer freeResult(gpa, &partial_a);
    try std.testing.expectEqualStrings("", partial_a.stdout);

    // ROUND 1: the sub-manifests arrive at A, and B pulls again.
    const got_ab = try appendArtifact(gpa, io, tmp_a.dir, leaf_ab);
    defer gpa.free(got_ab);
    const got_mn = try appendArtifact(gpa, io, tmp_a.dir, leaf_mn);
    defer gpa.free(got_mn);
    try std.testing.expectEqualStrings(hash_ab, got_ab); // the S card names what arrived
    try std.testing.expectEqualStrings(hash_mn, got_mn);

    const port1 = try freshLoopbackPort(io);
    var server1 = try spawnServe(gpa, io, tmp_a.dir, port1);
    defer server1.kill(io);
    try waitForListen(io, port1);
    const url1 = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{port1});
    defer gpa.free(url1);

    var pull = try runZit(gpa, io, b_repo, &.{ "pull", url1 });
    defer freeResult(gpa, &pull);
    try expectExitDiag(pull.term, 0, pull);

    // THE PROOF: B's holdings equal A's, and the tree now enumerates to its
    // complete file set — the concatenation of the two leaves' F runs in S card
    // order (submanifests §5.1), which is also the sorted namespace.
    const a_digests = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);
    const b_digests = try collectArtifactDigests(gpa, io, b_repo);
    defer freeDigests(gpa, b_digests);
    try expectSameDigestSet(a_digests, b_digests);

    var listed = try runZit(gpa, io, b_repo, &.{ "ls", tree_hash });
    defer freeResult(gpa, &listed);
    try expectExitDiag(listed.term, 0, listed);

    // Built from the same content hashes the leaves' F cards carry, so the
    // expectation is derived from the fixture rather than transcribed from a
    // passing run.
    var expected: std.ArrayList(u8) = .empty;
    defer expected.deinit(gpa);
    for ([_][]const u8{ "a.txt", "b.txt", "m.txt", "n.txt" }) |name| {
        const blob = try sha3HexAlloc(gpa, name);
        defer gpa.free(blob);
        try expected.print(gpa, "{s}\n", .{name}); // names only; `blob` is what --long would add
    }
    try std.testing.expectEqualStrings(expected.items, listed.stdout);
}

// ===========================================================================
// `zit submanifest` — the draft-fossil-submanifests-00 §6.3/§6.4/§8.1 operator
// surface, end to end through the real binary.
// ===========================================================================

/// Reads the two sub-manifest settings out of a repository's `.zit` config
/// frame — the durable state, not the verb's own report, so a claim that
/// "enabling persisted" is proved against storage rather than against stdout.
fn readSubmanifestSettings(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !submanifest.Settings {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_only });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);
    var oc = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult;
    defer oc.deinit(gpa);
    return .of(oc.config);
}

/// Reads the internal cache marker only to prove conservative invalidation. It
/// is not an RFC setting; the surrounding enable/retune behavior is §6.3/§6.4.
fn readSubmanifestSettledCostShape(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
) !?[32]u8 {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_only });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);
    var oc = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult;
    defer oc.deinit(gpa);
    return oc.config.submanifest_settled_cost_shape;
}

/// Overwrite the pinned k directly in the config frame, bypassing the verb.
/// The ONLY way to reach §6.3's out-of-range state: `zit submanifest` refuses
/// to pin one, so a repository can only get there by a hand-edited config or a
/// writer from another implementation — which is exactly the case §6.3's
/// fallback rule exists for.
fn forcePinnedK(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, k: u8) !void {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_write });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.open(gpa, fs.storageFile(), 0);
    defer log.deinit(gpa);
    var oc = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult;
    defer oc.deinit(gpa);
    var next = oc.config;
    next.submanifest_k = k;
    try log.putConfig(next);
    try log.commit();
}

/// Overwrite `submanifest-auto-retune` directly in the config frame, bypassing
/// the verb surface — there is no `zit submanifest` flag for this setting yet
/// (that CLI surface, and its propagation to a clone or peer, is out of scope
/// for the commit-path auto-dispatch this helper exists to test; §6.4's
/// opt-in setting still needs to be reachable by SOME writer for a test
/// to exercise it, the same reasoning `forcePinnedK` states for the
/// out-of-range case above).
fn forceAutoRetune(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, enabled: bool) !void {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_write });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.open(gpa, fs.storageFile(), 0);
    defer log.deinit(gpa);
    var oc = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult;
    defer oc.deinit(gpa);
    var next = oc.config;
    next.submanifest_auto_retune = enabled;
    try log.putConfig(next);
    try log.commit();
}

/// Clear the project code in the config frame, standing in for a repository
/// written before the writer that made it recorded one.
fn clearProjectCode(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !void {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_write });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.open(gpa, fs.storageFile(), 0);
    defer log.deinit(gpa);
    var oc = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult;
    defer oc.deinit(gpa);
    var next = oc.config;
    next.project_code = "";
    try log.putConfig(next);
    try log.commit();
}

test "verify: a repository with no project code says peers will refuse it, without calling it damage" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    const note = "no project code, so sync peers and forges will refuse it";
    var healthy = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &healthy);
    try expectExitDiag(healthy.term, 0, healthy);
    try std.testing.expect(std.mem.indexOf(u8, healthy.stderr, note) == null);

    try clearProjectCode(gpa, io, tmp.dir);
    var res = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &res);
    // Nothing is damaged: the exit code and the verdict stay clean, and the
    // damage advisory does not appear.
    try expectExitDiag(res.term, 0, res);
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "0 faults") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, note) != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "healthy peer") == null);
}

/// Seeds `n` tracked files and commits them, returning the check-in hash.
/// `n = 40` partitions into a real tree at the k = 3 the tests pin; the count
/// is a TEST input, never a measured optimum.
fn seedManyFiles(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, n: usize) !void {
    for (0..n) |i| {
        var name_buf: [32]u8 = undefined;
        const name = try std.fmt.bufPrint(&name_buf, "f{d:0>3}.txt", .{i});
        try writeFile(io, dir, name, "x\n");
        var add = try runZit(gpa, io, dir, &.{ "add", name });
        defer freeResult(gpa, &add);
        try expectExitDiag(add.term, 0, add);
    }
}

test "cli commit submanifests §§5.1/5.4: a partial commit preserves unnamed tree-parent files" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);

    var enable = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "3" });
    defer freeResult(gpa, &enable);
    try expectExitDiag(enable.term, 0, enable);

    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "tree" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);
    const root_hash = try checkinHash(root.stdout);
    var root_manifest = try parseCheckin(gpa, io, tmp.dir, root_hash);
    defer root_manifest.deinit();
    try std.testing.expect(root_manifest.m.s.len != 0);

    try writeFile(io, tmp.dir, "f000.txt", "changed\n");
    var child = try runCommitEnv(
        gpa,
        io,
        tmp.dir,
        .{ .user = "u" },
        &.{ "commit", "-m", "partial", "f000.txt" },
    );
    defer freeResult(gpa, &child);
    try expectExitDiag(child.term, 0, child);

    var changed = try runZit(gpa, io, tmp.dir, &.{ "cat", "f000.txt" });
    defer freeResult(gpa, &changed);
    try expectExit(changed.term, 0);
    try std.testing.expectEqualStrings("changed\n", changed.stdout);

    var unchanged = try runZit(gpa, io, tmp.dir, &.{ "cat", "f001.txt" });
    defer freeResult(gpa, &unchanged);
    try expectExit(unchanged.term, 0);
    try std.testing.expectEqualStrings("x\n", unchanged.stdout);
}

test "cli commit submanifests §5.1 artifact §4.7.6: partial commit consumes carried rename" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);

    var enable = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "3" });
    defer freeResult(gpa, &enable);
    try expectExitDiag(enable.term, 0, enable);
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "tree" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);

    var move = try runZit(gpa, io, tmp.dir, &.{ "mv", "f000.txt", "renamed.txt" });
    defer freeResult(gpa, &move);
    try expectExitDiag(move.term, 0, move);
    var rename = try runCommitEnv(
        gpa,
        io,
        tmp.dir,
        .{ .user = "u" },
        &.{ "commit", "-m", "rename", "renamed.txt" },
    );
    defer freeResult(gpa, &rename);
    try expectExitDiag(rename.term, 0, rename);

    try writeFile(io, tmp.dir, "f001.txt", "changed\n");
    var partial = try runCommitEnv(
        gpa,
        io,
        tmp.dir,
        .{ .user = "u" },
        &.{ "commit", "-m", "other", "f001.txt" },
    );
    defer freeResult(gpa, &partial);
    try expectExitDiag(partial.term, 0, partial);

    var renamed = try runZit(gpa, io, tmp.dir, &.{ "cat", "renamed.txt" });
    defer freeResult(gpa, &renamed);
    try expectExit(renamed.term, 0);
    try std.testing.expectEqualStrings("x\n", renamed.stdout);
    var old = try runZit(gpa, io, tmp.dir, &.{ "cat", "f000.txt" });
    defer freeResult(gpa, &old);
    try expectExit(old.term, 1);
}

test "cli submanifests §6.3/§6.4: retune clears stale cache when checkout resolution fails" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 2);

    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);

    var enable = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "3" });
    defer freeResult(gpa, &enable);
    try expectExitDiag(enable.term, 0, enable);
    try std.testing.expect((try readSubmanifestSettledCostShape(gpa, io, tmp.dir)) != null);

    // Deliberately make the checkout unreadable after the cache was populated.
    // §6.3 permits retuning; failure to reconstruct its effective F-set must
    // conservatively invalidate the implementation-only cache.
    try writeFile(io, tmp.dir, ".zit-checkout", "corrupt\n");
    var retune = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "retune", "--k", "4" });
    defer freeResult(gpa, &retune);
    try expectExitDiag(retune.term, 0, retune);
    try std.testing.expect((try readSubmanifestSettledCostShape(gpa, io, tmp.dir)) == null);
}

test "cli submanifests §6.3: enable clears stale cache when checkout resolution fails" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 2);

    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);
    var enable = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "3" });
    defer freeResult(gpa, &enable);
    try expectExitDiag(enable.term, 0, enable);
    try std.testing.expect((try readSubmanifestSettledCostShape(gpa, io, tmp.dir)) != null);

    var disable = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "disable" });
    defer freeResult(gpa, &disable);
    try expectExitDiag(disable.term, 0, disable);
    try writeFile(io, tmp.dir, ".zit-checkout", "corrupt\n");

    var reenable = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "3" });
    defer freeResult(gpa, &reenable);
    try expectExitDiag(reenable.term, 0, reenable);
    try std.testing.expect((try readSubmanifestSettledCostShape(gpa, io, tmp.dir)) == null);
}

test "cli submanifests §8.1: emission is OFF by default and a default repository mints no S card" {
    // THE default-off proof, at the only layer that can prove it: a repository
    // that nobody configured, committed through the real binary, whose minted
    // manifest carries a full flat F run and an EMPTY S run.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // The stored settings are off and unpinned before anything is asked of them.
    const settings = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expect(!settings.enabled);
    try std.testing.expect(!settings.pinned());

    try seedManyFiles(gpa, io, tmp.dir, 40);
    var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "flat" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    const hash = try checkinHash(ci.stdout);

    var m = try parseCheckin(gpa, io, tmp.dir, hash);
    defer m.deinit();
    try std.testing.expectEqual(@as(usize, 0), m.m.s.len); // §8.1 — no S card
    try std.testing.expectEqual(@as(usize, 40), m.m.f.len); // the whole flat run

    // …and the §8.1 seen-submanifest indicator is untouched, so this repository
    // is not treated as one that holds trees.
    var file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_only });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);
    var oc = (try log.readConfig(gpa)).?;
    defer oc.deinit(gpa);
    try std.testing.expect(!oc.config.seen_submanifest);
}

test "cli submanifests §6.3/§8.1: a one-name check-in has no candidate, and enabling refuses atomically" {
    // The minimal no-candidate scenario: one name makes the height-0 scan flat
    // at every k (§6.2 step 2 needs at least two groups), so §6.3's automatic
    // selection "has no result" and §8.1's rule fires — "the writer MUST refuse
    // the request atomically: enable-submanifests and submanifest-k retain
    // their pre-request values, and no S card is emitted." Distinct from the
    // no-check-in case below: here the name set exists and is analyzed, and the
    // analysis is what comes back empty.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "only.txt");
    {
        var a = try runZit(gpa, io, tmp.dir, &.{ "add", "only.txt" });
        defer freeResult(gpa, &a);
        try expectExitDiag(a.term, 0, a);
    }
    {
        var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
        defer freeResult(gpa, &ci);
        try expectExitDiag(ci.term, 0, ci);
    }

    // The analysis pass agrees there is no candidate, and says so without failing.
    {
        var a = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "analyze" });
        defer freeResult(gpa, &a);
        try expectExitDiag(a.term, 0, a);
        try std.testing.expect(std.mem.indexOf(u8, a.stdout, "no k partitions") != null);
    }

    var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable" });
    defer freeResult(gpa, &r);
    try expectExit(r.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, r.stderr, "no k partitions") != null);
    try std.testing.expect(std.mem.indexOf(u8, r.stderr, "--k") != null);

    // "retain their pre-request values" — atomically, so BOTH settings are as
    // they were, not just the one the refusal is about.
    const after = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expect(!after.enabled);
    try std.testing.expect(!after.pinned());

    // §8.1's escape hatch is still open: an explicit in-range k pins and enables
    // on the very same repository, which is what proves the refusal was about
    // the absent selection and not about the name set being unusable.
    var e = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "4" });
    defer freeResult(gpa, &e);
    try expectExitDiag(e.term, 0, e);
    const pinned = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expect(pinned.enabled);
    try std.testing.expectEqual(@as(u8, 4), pinned.k);
}

test "cli submanifests §8.1: enabling is refused when no parameter can be selected" {
    // §6.3 has the writer select k on enabling, so the bare verb analyzes
    // rather than demanding a number. With no check-in open there is no name
    // set to select from — and §8.1's "MUST NOT enable emission on a repository
    // with no pinned parameter" then makes this a refusal, not a fallback to
    // some value of the implementation's own.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable" });
    defer freeResult(gpa, &r);
    try expectExit(r.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, r.stderr, "no file set") != null);
    try std.testing.expect(std.mem.indexOf(u8, r.stderr, "--k") != null);
    const after = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expect(!after.enabled); // refused means NOT written
    try std.testing.expect(!after.pinned());
}

test "cli submanifests §6.3: an out-of-range k is refused at the pin, at every value outside 1..12" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    for ([_][]const u8{ "0", "13", "200" }) |k| {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", k });
        defer freeResult(gpa, &r);
        try std.testing.expect(r.term.exited != 0);
        const after = try readSubmanifestSettings(gpa, io, tmp.dir);
        try std.testing.expect(!after.enabled);
        try std.testing.expect(!after.pinned());
    }
    // Both ends of §6.3's supported range are accepted.
    for ([_][]const u8{ "1", "12" }) |k| {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "retune", "--k", k });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
}

test "cli submanifests §8.1/§6.2: enabling warns, pins k, and the next commit mints a tree" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);

    {
        // k = 3 is the Appendix A.5 conformance vector's value, pinned here for
        // the same reason: a test must never take its input from a measurement.
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "3" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        // §8.1/§8.2 — the consequence is shown, not implied. Both irreversible
        // halves must appear: the refusal of pre-2.29 peers, and the fact that
        // disabling later cannot retire what was written.
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "2.29") != null);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "cannot retire") != null);
        // §6.3's project scope is stated to the operator, both halves: a clone
        // adopts these settings, an existing repository is not refreshed.
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "a CLONE adopts these settings") != null);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "An EXISTING repository is not refreshed") != null);
    }
    const settings = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expect(settings.enabled);
    try std.testing.expectEqual(@as(u8, 3), settings.k);

    var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "tree" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    const hash = try checkinHash(ci.stdout);

    var m = try parseCheckin(gpa, io, tmp.dir, hash);
    defer m.deinit();
    // §6.2 step 3 — the manifest carries its S run and NO F run: "the file set
    // is conveyed by the F run or the S run", never both.
    try std.testing.expect(m.m.s.len >= 2);
    try std.testing.expectEqual(@as(usize, 0), m.m.f.len);

    // §8.1 — "When the first S-bearing artifact is stored, an implementation
    // SHOULD record a local seen-submanifest indicator". The commit path is one
    // of the two places S-bearing artifacts enter a log; the indicator is what
    // keeps the §7.2 transfer gate armed after the setting is turned back off.
    {
        var file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_only });
        defer file.close(io);
        const st = try file.stat(io);
        var fs = storage.FileStorage.init(io, file, st.size);
        var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
        defer log.deinit(gpa);
        var oc = (try log.readConfig(gpa)).?;
        defer oc.deinit(gpa);
        try std.testing.expect(oc.config.seen_submanifest);
    }

    // §5.1 — the tree enumerates back to the complete file set, so the check-in
    // is not merely well-formed but readable as the 40 files it was built from.
    var listed = try runZit(gpa, io, tmp.dir, &.{ "ls", hash });
    defer freeResult(gpa, &listed);
    try expectExitDiag(listed.term, 0, listed);
    var lines: usize = 0;
    var it = std.mem.splitScalar(u8, listed.stdout, '\n');
    while (it.next()) |l| {
        if (l.len != 0) lines += 1;
    }
    try std.testing.expectEqual(@as(usize, 40), lines);
}

test "cli submanifests: enable pins k from replayed history and cites the evidence" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);

    // Churn history: the same file rewritten across several commits, so the
    // replay has real subtree reuse to measure the candidates against.
    for (0..6) |i| {
        const content = try std.fmt.allocPrint(gpa, "revision {d}\n", .{i});
        defer gpa.free(content);
        try writeFile(io, tmp.dir, "f000.txt", content);
        var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "churn" });
        defer freeResult(gpa, &ci);
        try expectExitDiag(ci.term, 0, ci);
    }

    var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable" });
    defer freeResult(gpa, &r);
    try expectExitDiag(r.term, 0, r);
    // The pin was chosen by the replay, and the output says so — the churn-aware
    // shape: better k from measured history, no verdict, no gate.
    try std.testing.expect(std.mem.indexOf(u8, r.stdout, "by replaying") != null);
    const settings = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expect(settings.enabled);
    try std.testing.expect(settings.k >= 1 and settings.k <= 12);
}

test "cli submanifests: enable on a fresh single-commit repository still proceeds — the snapshot objective selects" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);
    var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "only" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);

    var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable" });
    defer freeResult(gpa, &r);
    try expectExitDiag(r.term, 0, r);
    const settings = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expect(settings.enabled);
}

test "cli submanifests: analyze prints the replay on a repository and calls it unavailable under --names" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);
    for (0..3) |i| {
        const content = try std.fmt.allocPrint(gpa, "rev {d}\n", .{i});
        defer gpa.free(content);
        try writeFile(io, tmp.dir, "f000.txt", content);
        var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c" });
        defer freeResult(gpa, &ci);
        try expectExitDiag(ci.term, 0, ci);
    }

    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "analyze" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "replayed over the last") != null);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "flat (actual):") != null);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "verdict:") != null);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "bootstrap") != null);
    }
    {
        try writeFile(io, tmp.dir, "names.txt", "a.txt\nb.txt\nc.txt\n");
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "analyze", "--names", "names.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "replay: unavailable with --names") != null);
    }
}

test "cli submanifests §8.1: disabling stops new trees, keeps the pin, and says what it cannot undo" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "3" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
    {
        var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "tree" });
        defer freeResult(gpa, &ci);
        try expectExitDiag(ci.term, 0, ci);
    }
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "disable" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        // §8.1 — "Disabling the setting stops the emission of new tree
        // manifests; it cannot retire existing ones."
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "cannot retire") != null);
    }
    // The pin SURVIVES a disable: it is the repository's content-sharing
    // identity, and clearing it would silently make a later re-enable a §6.4
    // re-tune (an every-leaf rewrite) instead of a resume.
    const after = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expect(!after.enabled);
    try std.testing.expectEqual(@as(u8, 3), after.k);

    // …and the next commit is flat again.
    try writeFile(io, tmp.dir, "later.txt", "y\n");
    {
        var add = try runZit(gpa, io, tmp.dir, &.{ "add", "later.txt" });
        defer freeResult(gpa, &add);
        try expectExitDiag(add.term, 0, add);
    }
    var ci2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "flat again" });
    defer freeResult(gpa, &ci2);
    try expectExitDiag(ci2.term, 0, ci2);
    var m = try parseCheckin(gpa, io, tmp.dir, try checkinHash(ci2.stdout));
    defer m.deinit();
    try std.testing.expectEqual(@as(usize, 0), m.m.s.len);
    try std.testing.expectEqual(@as(usize, 41), m.m.f.len);
}

test "cli submanifests §6.3: an out-of-range pinned k falls back to a flat manifest and reports it" {
    // "on receiving a value outside 1..12 it MUST fall back to emitting flat
    // manifests rather than substitute a parameter of its own, and SHOULD report
    // the fallback to the operator." Reached by writing the bad value straight
    // into the config, because the verb refuses to pin one.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "3" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
    try forcePinnedK(gpa, io, tmp.dir, 13);

    var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "fallback" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci); // the commit SUCCEEDS — a fallback, not a failure
    try std.testing.expect(std.mem.indexOf(u8, ci.stderr, "out of range") != null); // the SHOULD-report
    var m = try parseCheckin(gpa, io, tmp.dir, try checkinHash(ci.stdout));
    defer m.deinit();
    try std.testing.expectEqual(@as(usize, 0), m.m.s.len); // flat, with no substituted parameter
    try std.testing.expectEqual(@as(usize, 40), m.m.f.len);

    // `show` reports the same fallback, from the same decision.
    var r = try runZit(gpa, io, tmp.dir, &.{"submanifest"});
    defer freeResult(gpa, &r);
    try expectExitDiag(r.term, 0, r);
    try std.testing.expect(std.mem.indexOf(u8, r.stdout, "out of range") != null);
    try std.testing.expect(std.mem.indexOf(u8, r.stdout, "commits emit: flat manifests") != null);
}

test "cli submanifests §6.4: a re-tune is manual, needs an explicit value, and warns about the leaf rewrite" {
    // "A re-tune MUST be manual and operator-initiated ... an implementation MAY
    // advise of drift from the analyzed optimum but MUST NOT re-tune
    // automatically." A bare `retune` therefore must NOT consult the analysis
    // pass — it must fail and ask for a value.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "3" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "retune" });
        defer freeResult(gpa, &r);
        try expectExit(r.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, r.stderr, "explicit value") != null);
        // Nothing moved.
        const after = try readSubmanifestSettings(gpa, io, tmp.dir);
        try std.testing.expectEqual(@as(u8, 3), after.k);
    }
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "retune", "--k", "4" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        // §6.4 — "The first commit after it shares no sub-manifest with its
        // parent's tree and rewrites every leaf, once."
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "rewrites every") != null);
        const after = try readSubmanifestSettings(gpa, io, tmp.dir);
        try std.testing.expectEqual(@as(u8, 4), after.k);
        try std.testing.expect(after.enabled); // a re-tune does not touch the gate
    }
    // `enable --k N` refuses to silently re-pin: that is what `retune` is for.
    var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "5" });
    defer freeResult(gpa, &r);
    try expectExit(r.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, r.stderr, "re-tune") != null);
    const after = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expectEqual(@as(u8, 4), after.k);
}

test "cli submanifests §6.3: the analysis pass reports every k, changes nothing, and never pins" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);
    {
        var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
        defer freeResult(gpa, &ci);
        try expectExitDiag(ci.term, 0, ci);
    }

    var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "analyze" });
    defer freeResult(gpa, &r);
    try expectExitDiag(r.term, 0, r);
    try std.testing.expect(std.mem.indexOf(u8, r.stdout, "names: 40") != null);
    try std.testing.expect(std.mem.indexOf(u8, r.stdout, "analyzed optimum: k=") != null);
    // §6.5's prediction is printed beside the measurement, so drift between the
    // two is visible rather than assumed away.
    try std.testing.expect(std.mem.indexOf(u8, r.stdout, "§6.5 prediction") != null);
    // §6.3/§6.4 — advisory, and it pins nothing: the settings are untouched.
    const after = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expect(!after.enabled);
    try std.testing.expect(!after.pinned());
}

test "cli submanifests §6.3: enabling selects the parameter it analyzes, without an operator value" {
    // "The partition parameter k is selected once, when the enable-submanifests
    // setting is first turned on for a repository, and pinned" — by the writer,
    // minimizing §6.3's objective over the current name set. So the bare verb
    // succeeds, and the value it pins is the value `analyze` reports: both go
    // through one cost pass, and this is what holds them together.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);
    {
        var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
        defer freeResult(gpa, &ci);
        try expectExitDiag(ci.term, 0, ci);
    }

    // What the advisory pass reports.
    var analyzed_k: u8 = 0;
    {
        var a = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "analyze" });
        defer freeResult(gpa, &a);
        try expectExitDiag(a.term, 0, a);
        const marker = "analyzed optimum: k=";
        const at = std.mem.indexOf(u8, a.stdout, marker) orelse return error.TestUnexpectedResult;
        const rest = a.stdout[at + marker.len ..];
        const end = std.mem.indexOfNone(u8, rest, "0123456789") orelse rest.len;
        analyzed_k = try std.fmt.parseInt(u8, rest[0..end], 10);
        try std.testing.expect(analyzed_k >= 1 and analyzed_k <= 12);
    }

    var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable" });
    defer freeResult(gpa, &r);
    try expectExitDiag(r.term, 0, r);
    // It says the value was selected rather than defaulted, and what from.
    try std.testing.expect(std.mem.indexOf(u8, r.stdout, "selected by the §6.3 analysis") != null);
    try std.testing.expect(std.mem.indexOf(u8, r.stdout, "40 names") != null);

    const after = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expect(after.enabled);
    try std.testing.expect(after.pinned());
    try std.testing.expectEqual(analyzed_k, after.k);

    // §6.3: "k is selected ONCE ... and pinned". A second enable re-measures
    // nothing and leaves the pin where it is — moving it is a re-tune (§6.4).
    var again = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable" });
    defer freeResult(gpa, &again);
    try expectExitDiag(again.term, 0, again);
    try std.testing.expect(std.mem.indexOf(u8, again.stdout, "selected by the §6.3 analysis") == null);
    const twice = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expectEqual(analyzed_k, twice.k);
}

test "cli submanifests §6.1: the analysis pass measures a name list, and rejects one out of canonical order" {
    // §6.1: "The predicate reads the filename only: content hash, permission,
    // and old-name never influence the partition" — so a name list is a complete
    // input, which is how a name set from a repository zit cannot open gets
    // measured. §6.2's precondition (strictly ascending canonical order) is
    // reported rather than silently repaired.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var sorted: std.ArrayList(u8) = .empty;
    defer sorted.deinit(gpa);
    for (0..40) |i| try sorted.print(gpa, "src/f{d:0>3}.zig\n", .{i});
    try writeFile(io, tmp.dir, "names.txt", sorted.items);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "analyze", "--names", "names.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, "names: 40") != null);
    }
    try writeFile(io, tmp.dir, "unsorted.txt", "b.txt\na.txt\n");
    var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "analyze", "--names", "unsorted.txt" });
    defer freeResult(gpa, &r);
    try expectExit(r.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, r.stderr, "canonical order") != null);
}

// ===========================================================================
// `zit commit` submanifests §6.4 — the post-commit drift advisory. "An
// implementation MAY advise of drift from the analyzed optimum at any time"
// — unconditional, independent of the `submanifest-auto-retune` gate §6.4
// puts on the re-tune half. These run only through the real binary's
// non-interactive path (test stdin is never a tty): the confirm-prompt's
// accepted-'y' branch is deliberately NOT covered here, matching
// `cmd/sync.zig`'s own
// `readPasswordNoEcho` precedent, which states the same limit about itself.
// ===========================================================================

test "cli commit submanifests §6.4: a repository that never enabled sub-manifests gets no advisory" {
    // Requirement 1: zero added behavior on the ordinary case — a repository
    // that never turned submanifests on prints nothing new, ever.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);

    var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "flat" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    try std.testing.expect(std.mem.indexOf(u8, ci.stdout, "submanifest-k has drifted") == null);
}

test "cli commit submanifests §6.4: no advisory when the pinned k is already the analyzed optimum" {
    // k = 3 is the Appendix A.5 conformance vector's value for this exact
    // 40-file synthetic set (the same fixture `seedManyFiles` builds elsewhere
    // in this file) — i.e. it already IS the analyzed optimum, so `driftCheck`
    // must find nothing to recommend.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "3" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }

    var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "tree" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    try std.testing.expect(std.mem.indexOf(u8, ci.stdout, "submanifest-k has drifted") == null);
}

test "cli commit submanifests §6.4: genuine margin-clearing drift prints the exact advisory line" {
    // k = 1 pins §6.3's coarsest supported partition against 200 names — a
    // fixed, deliberate mismatch (the same deterministic "x\n"-content fixture
    // every seed helper in this file uses, never a measured one) big enough
    // that the analyzed optimum clears the §6.4 margin gate by a wide margin.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 200);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "1" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }

    var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "tree" });
    defer freeResult(gpa, &ci);
    // Requirement 6: an advisory (present or not) never changes commit's own
    // exit code — every commit in this test block, drifted or not, exits 0.
    try expectExitDiag(ci.term, 0, ci);
    try std.testing.expectEqualStrings(
        "submanifest-k has drifted from 1 to 3 (65.3% read-cost improvement)\n",
        ci.stdout[std.mem.indexOf(u8, ci.stdout, "submanifest-k has drifted") orelse
            return error.TestUnexpectedResult ..],
    );
}

test "cli commit submanifests §6.4: --no-submanifest-advice suppresses the advisory even with real drift" {
    // Requirement 2: the flag skips step 3 onward entirely — same genuine-drift
    // fixture as the test above, but no line, no prompt, and the check-in's own
    // success line is untouched.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 200);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "1" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }

    var ci = try runCommitEnv(
        gpa,
        io,
        tmp.dir,
        .{ .user = "u" },
        &.{ "commit", "-m", "tree", "--no-submanifest-advice" },
    );
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    try std.testing.expect(std.mem.indexOf(u8, ci.stdout, "submanifest-k has drifted") == null);
    _ = try checkinHash(ci.stdout); // the ordinary success line still prints
}

// ===========================================================================
// `zit commit` submanifests §6.4 — `submanifest-auto-retune` dispatch. §6.4's
// opt-in shape: default false keeps the "manual and operator-initiated"
// re-tune in force byte-for-byte (the test directly below is the regression
// proof of that); true is the durable project-operator act §6.4 requires
// before ANY automatic re-tune may fire, after which drift no longer needs a
// fresh 'y' at every commit that crosses the margin gate.
// ===========================================================================

test "cli commit submanifests §6.4: auto-retune false (default) is byte-identical to the advisory-only path" {
    // Regression proof, not just a new-behavior test: explicitly forcing the
    // setting to its own default and re-running the exact genuine-drift
    // fixture from the advisory-only tests above must print the identical
    // advisory line, take no automatic action, and leave the pin untouched —
    // the auto-dispatch addition changes nothing on this path.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 200);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "1" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
    try forceAutoRetune(gpa, io, tmp.dir, false);

    var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "tree" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    try std.testing.expectEqualStrings(
        "submanifest-k has drifted from 1 to 3 (65.3% read-cost improvement)\n",
        ci.stdout[std.mem.indexOf(u8, ci.stdout, "submanifest-k has drifted") orelse
            return error.TestUnexpectedResult ..],
    );
    // No prompt is a tty-only concern (test stdin is never a tty, so the
    // existing advisory-only tests already prove the non-interactive prompt
    // path prints nothing further); what THIS test proves is the pin itself:
    // the non-interactive, non-auto-retune path never writes it.
    const settings = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expectEqual(@as(u8, 1), settings.k);
}

test "cli commit submanifests §6.4: auto-retune true dispatches the retune with no prompt" {
    // §6.4's core case: with the durable opt-in set, genuine drift is
    // applied directly — no prompt (works non-interactively, unlike the
    // manual-confirm path), the pin actually moves (proved by a config
    // readback, not just stdout), and the same "next commit rewrites every
    // leaf, once" note the manual path prints appears here too.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 200);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "1" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
    try forceAutoRetune(gpa, io, tmp.dir, true);

    var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "tree" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    try std.testing.expectEqualStrings(
        "submanifest-k has drifted from 1 to 3 (65.3% read-cost improvement)\n" ++
            "submanifest-k: 1 -> 3\n" ++
            "note: the next commit shares no sub-manifest with its parent and rewrites every\n" ++
            "leaf, once. No history is rewritten and no reader behaves differently.\n",
        ci.stdout[std.mem.indexOf(u8, ci.stdout, "submanifest-k has drifted") orelse
            return error.TestUnexpectedResult ..],
    );
    // The durable proof: the pin actually moved in storage, not just on stdout.
    const settings = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expectEqual(@as(u8, 3), settings.k);
}

test "cli commit submanifests §6.4: auto-retune true with no drift is a no-op" {
    // The opt-in changes dispatch, not the drift gate: with the pin already at
    // the analyzed optimum, `driftCheck` recommends nothing, so there is
    // nothing for auto-retune to dispatch — same silence as the advisory-only
    // no-drift test above, and the pin stays exactly where it was.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try seedManyFiles(gpa, io, tmp.dir, 40);
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "3" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
    try forceAutoRetune(gpa, io, tmp.dir, true);

    var ci = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "tree" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    try std.testing.expect(std.mem.indexOf(u8, ci.stdout, "submanifest-k has drifted") == null);
    const settings = try readSubmanifestSettings(gpa, io, tmp.dir);
    try std.testing.expectEqual(@as(u8, 3), settings.k);
}

// ---------------------------------------------------------------------------
// `zit pack` reclaims deferred shuns (the pack filter). No `zit shun` verb exists
// yet, so these tests append the tombstone through the library — the same frame
// that verb will write — and then drive the REAL binary over it.
// ---------------------------------------------------------------------------

/// Appends a shun row naming the artifact in `dir/.zit` whose reconstructed
/// content equals `body`, and returns that artifact's raw digest (caller frees).
/// Finds the artifact by scanning rather than by recomputing a hash, so the test
/// does not have to know which hash algorithm the repository was initialized
/// with. Fails if no artifact holds those bytes — the premise, not an assertion
/// about pack.
fn shunArtifactByContent(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    body: []const u8,
    reason: []const u8,
) ![]u8 {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_write });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.open(gpa, fs.storageFile(), 0);
    defer log.deinit(gpa);

    const rids = try log.ridsInPersistedOrder(gpa);
    defer gpa.free(rids);
    for (rids) |rid| {
        if (log.kindOf(rid) == .config) continue;
        const content = (try log.content(gpa, rid)) orelse continue;
        defer gpa.free(content);
        if (!std.mem.eql(u8, content, body)) continue;
        const digest = try gpa.dupe(u8, log.hashOf(rid));
        errdefer gpa.free(digest);
        try log.putShun(digest, reason, 1753000010);
        try log.commit();
        return digest;
    }
    return error.NoArtifactHoldsThoseBytes;
}

test "cli: zit pack reclaims a deferred shun, reports the count, and keeps the tombstone" {
    // A shun masks; a pack RECLAIMS. Between the two the bytes are still in the
    // store, which is the whole reason `zit pack` grew a filter — an operator who
    // was ordered to remove content needs one command that actually removes it,
    // and needs to be told how much it removed rather than inferring it from a
    // byte count that shrinks on an ordinary re-deltify too.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // Two files so a pack that dropped everything cannot pass by accident.
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "doomed.txt", "content an operator was ordered to remove\n");
    try writeFile(io, tmp.dir, "keep.txt", "content with nothing against it\n");
    try addManaged(gpa, io, tmp.dir, &.{ "doomed.txt", "keep.txt" });
    const root = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(root);

    const doomed = try shunArtifactByContent(
        gpa,
        io,
        tmp.dir,
        "content an operator was ordered to remove\n",
        "takedown notice 4717",
    );
    defer gpa.free(doomed);

    // The premise: masked but STILL STORED. `cat` refuses it (the shun mask),
    // while the frame is physically there for the pack to reclaim.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "doomed.txt", root });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
    }

    {
        var res = try runZit(gpa, io, tmp.dir, &.{"pack"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("", res.stderr);
        // THE OPERATOR-VISIBLE PIN: the run says what it removed. Substring, not
        // the whole line — the artifact/byte counts are environment-dependent
        // and the exact wording is unit-tested in cmd/sync.zig.
        if (std.mem.indexOf(u8, res.stdout, "1 shunned reclaimed") == null) {
            std.debug.print("pack did not report the reclaim: {s}\n", .{res.stdout});
            return error.ReclaimCountNotReported;
        }
    }

    // The on-disk oracle, read back through the library: the bytes are gone from
    // the packed store — not merely masked — and the tombstone that named them is
    // not, so the artifact still reads as absent and a sync will not fetch it back.
    {
        var file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_only });
        defer file.close(io);
        const st = try file.stat(io);
        var fs = storage.FileStorage.init(io, file, st.size);
        var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
        defer log.deinit(gpa);
        try std.testing.expect(log.lookup(doomed) == null);
        try std.testing.expect(log.isShunned(doomed));
        const rows = log.shunList();
        try std.testing.expectEqual(@as(usize, 1), rows.len);
        try std.testing.expectEqualStrings("takedown notice 4717", rows[0].reason);
    }

    // The unshunned neighbour is still readable through the real binary, so the
    // reclaim removed exactly what it was told to and nothing adjacent.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "keep.txt", root });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expectEqualStrings("content with nothing against it\n", res.stdout);
    }

    // A second pack has nothing left to reclaim and must say nothing about it.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"pack"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "shunned reclaimed") == null);
    }
}

// ---------------------------------------------------------------------------
// `zit shun` through the REAL binary.
//
// NO DRAFT SPECIFIES THIS VERB: sync §3.3 (docs/rfcs/
// draft-fossil-sync-protocol-00.txt L518-519) defines the repository shun table
// and L520-521 grants that local administration may add names to it; it names no command
// and no policy. Every expectation below is therefore pinned against zit's own
// stated policy — refuse by default, `--force` gates BOTH durable paths, the
// current checkout's check-in has no override — not against a section number.
// ---------------------------------------------------------------------------

/// The full lowercase-hex name of the artifact in `dir/.zit` whose reconstructed
/// content equals `body` (caller frees). Found by scanning rather than by
/// recomputing a hash, so the test never has to know which hash algorithm the
/// repository was initialized with. Fails if nothing holds those bytes — that is
/// the fixture's premise, not an assertion about shun.
fn artifactNameByContent(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    body: []const u8,
) ![]u8 {
    var file = try dir.openFile(io, ".zit", .{});
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);

    const rids = try log.ridsInPersistedOrder(gpa);
    defer gpa.free(rids);
    for (rids) |rid| {
        const content = (try log.content(gpa, rid)) orelse continue;
        defer gpa.free(content);
        if (!std.mem.eql(u8, content, body)) continue;
        const raw = log.hashOf(rid);
        const hex = try gpa.alloc(u8, raw.len * 2);
        const lower = "0123456789abcdef";
        for (raw, 0..) |b, i| {
            hex[i * 2] = lower[b >> 4];
            hex[i * 2 + 1] = lower[b & 0x0f];
        }
        return hex;
    }
    return error.NoArtifactHoldsThoseBytes;
}

/// Reads `dir/.zit` whole (caller frees) — the byte-identical oracle a refused
/// destructive run must satisfy. A byte comparison, not a size comparison: a run
/// that appended a tombstone and then rewrote back to the same length would pass
/// the latter.
fn storeBytesAlloc(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]u8 {
    var file = try dir.openFile(io, ".zit", .{});
    defer file.close(io);
    const st = try file.stat(io);
    const buf = try gpa.alloc(u8, @intCast(st.size));
    errdefer gpa.free(buf);
    const n = try file.readPositionalAll(io, buf, 0);
    try std.testing.expectEqual(buf.len, n);
    return buf;
}

/// Whether `dir/.zit` still holds an artifact named by the hex `name` — the
/// on-disk oracle for "the bytes are gone", read through the library.
fn storeHoldsArtifact(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, name: []const u8) !bool {
    var file = try dir.openFile(io, ".zit", .{});
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);
    return (try log.resolveHexPrefix(name)) != null;
}

/// The number of rows in `dir/.zit`'s shun table.
fn storeShunRowCount(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !usize {
    var file = try dir.openFile(io, ".zit", .{});
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);
    return log.shunList().len;
}

const doomed_body = "content an operator was ordered to remove\n";
const kept_body = "content with nothing against it\n";

/// Two-file repository, committed once. Returns the root check-in's 64-hex hash
/// (caller frees). Two files so a shun that removed everything cannot pass by
/// accident.
fn shunFixture(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]const u8 {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "doomed.txt", doomed_body);
    try writeFile(io, dir, "keep.txt", kept_body);
    try addManaged(gpa, io, dir, &.{ "doomed.txt", "keep.txt" });
    return commitHashDup(gpa, io, dir, "root");
}

test "cli shun: refuses without --force leaving the store byte-identical; --force then removes the content" {
    // THE DEFAULT IS REFUSAL, and a refusal must be a no-op down to the byte —
    // not "no content removed" but "nothing written at all". A run that recorded
    // the tombstone while deciding would leave a durable, irreversible mark on a
    // repository whose operator explicitly did not say --force.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(doomed);

    const before = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(before);

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        // The refusal names what would go, says it is irreversible, and names the
        // flag that proceeds. Nothing lands on stdout — a refused destructive verb
        // has no success output to compose with.
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "1 hash would be shunned") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "irreversible") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "run 'zit shun --force <HASH>...' to proceed") != null);
        // The type diagnostic: a file blob names what the check-ins lose.
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "file content; every check-in listing it loses that file") != null);
    }

    // THE BYTE-IDENTICAL PIN.
    {
        const after = try storeBytesAlloc(gpa, io, tmp.dir);
        defer gpa.free(after);
        try std.testing.expectEqualSlices(u8, before, after);
    }
    // ...and the content is still readable, so the refusal did not mask anything
    // either.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "doomed.txt", root });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expectEqualStrings(doomed_body, res.stdout);
    }

    // Now with --force: the bytes go and the tombstone stays.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", "--reason", "takedown notice 4417", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expectEqualStrings("", res.stderr);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "shunned: 1 hash recorded, 1 artifact removed") != null);
    }
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp.dir, doomed));
    try std.testing.expectEqual(@as(usize, 1), try storeShunRowCount(gpa, io, tmp.dir));
    {
        // The blob is gone: `cat` reports it missing rather than serving bytes.
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "doomed.txt", root });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expectEqualStrings("", res.stdout);
    }
    {
        // The neighbour survived, so the rewrite removed exactly what it named.
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "keep.txt", root });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expectEqualStrings(kept_body, res.stdout);
    }
    {
        // A repeat is a no-op: exit 0, no --force needed to say "nothing to do".
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "nothing to do; 1 hash already shunned") != null);
    }
}

test "cli shun: the check-in this checkout is on is refused EVEN WITH --force" {
    // The one refusal in this verb with no override. Removing the check-in the
    // working tree is standing on breaks the tree from under the operator, and
    // `zit update` is a cheap way out — so this is the single case where zit
    // overrides the operator's explicit --force rather than obeying it.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const before = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(before);

    var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", root });
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 1, res);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "is the check-in this checkout is on; refusing") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "run 'zit update'") != null);
    // No flag is offered, because none exists: an operator who reads one out of
    // this message would try it and be refused twice.
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "--force") == null);

    // Byte-identical, and no tombstone: the guard runs before anything durable.
    {
        const after = try storeBytesAlloc(gpa, io, tmp.dir);
        defer gpa.free(after);
        try std.testing.expectEqualSlices(u8, before, after);
    }
    try std.testing.expectEqual(@as(usize, 0), try storeShunRowCount(gpa, io, tmp.dir));
}

/// A repository whose second check-in introduces content its first never had —
/// the accident `zit purge` exists for, plus a file both check-ins share, which
/// is what makes the drop set's set difference observable.
fn purgeFixture(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, first: *[]const u8) ![]const u8 {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "keep.txt", kept_body);
    try addManaged(gpa, io, dir, &.{"keep.txt"});
    first.* = try commitHashDup(gpa, io, dir, "root");

    try writeFile(io, dir, "swept.bin", doomed_body);
    try addManaged(gpa, io, dir, &.{"swept.bin"});
    return commitHashDup(gpa, io, dir, "the accident");
}

test "cli purge: undo waits for the store's append lock instead of writing under another writer" {
    // Undo restores by appending, and an append that does not take the lock
    // scans the same end as a concurrent writer: one of the two writes then
    // lands on top of the other with frames that are complete and valid, which
    // nothing afterwards can detect. Holding the lock from here stands in for
    // a commit or pull in progress.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var first: []const u8 = undefined;
    const leaf = try purgeFixture(gpa, io, tmp.dir, &first);
    defer gpa.free(first);
    defer gpa.free(leaf);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "update", first });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "purge", "checkins", "--force", leaf });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    const buried = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(buried);

    var held = try cli.openStoreAppend(io, tmp.dir);
    var held_open = true;
    defer if (held_open) held.close(io);

    var env = std.process.Environ.Map.init(gpa);
    defer env.deinit();
    try env.put("TZ", "UTC");
    try env.put("HOME", "/zit-e2e-isolated-home-does-not-exist");
    try env.put("PATH", "/usr/bin:/bin");
    const cwd_abs = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd_abs);
    const abs_exe = try std.fs.path.resolve(gpa, &.{ cwd_abs, zit_exe });
    defer gpa.free(abs_exe);
    // Through `sh` to keep its stderr in a file: the waiting notice is part
    // of what this test checks. The shell writes a line there first, as a
    // log shared with other writers would have, and the notice must land
    // after it, not over it.
    var child = try std.process.spawn(io, .{
        .argv = &.{ "/bin/sh", "-c", "exec 2>undo.err; echo 'before zit' >&2; exec \"$0\" purge undo 1", abs_exe },
        .cwd = .{ .dir = tmp.dir },
        .environ_map = &env,
        .stdout = .ignore,
        .stderr = .ignore,
    });
    var reaped = false;
    defer if (!reaped) child.kill(io);

    // Long enough for an unlocked undo to have finished its append many times
    // over. With the lock held, nothing may have been written.
    io.sleep(.{ .nanoseconds = 1500 * std.time.ns_per_ms }, .awake) catch {};
    {
        const during = try storeBytesAlloc(gpa, io, tmp.dir);
        defer gpa.free(during);
        try std.testing.expectEqualSlices(u8, buried, during);
    }
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp.dir, leaf));

    // Released, the waiting undo takes its turn and completes, having said
    // what it was waiting for: the holder is this test process.
    held.close(io);
    held_open = false;
    const term = try child.wait(io);
    reaped = true;
    try expectExit(term, 0);
    {
        var err_buf: [1024]u8 = undefined;
        const said = try tmp.dir.readFile(io, "undo.err", &err_buf);
        var want_buf: [96]u8 = undefined;
        const pid: i64 = if (@import("builtin").os.tag == .linux) std.os.linux.getpid() else std.c.getpid();
        const want = try std.fmt.bufPrint(&want_buf, "zit: waiting for the repository lock, held by process {d} on ", .{pid});
        if (!std.mem.startsWith(u8, said, "before zit\n") or std.mem.indexOf(u8, said, want) == null) {
            std.debug.print("\nundo stderr:\n{s}\n", .{said});
            return error.NoWaitingNotice;
        }
    }
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp.dir, leaf));
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"verify"});
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "0 faults") != null);
    }
}

test "cli purge: a leaf's own content goes, shared content stays, and undo restores the store" {
    // THE WHOLE CONTRACT IN ONE PASS. A purge must (1) refuse without --force
    // leaving the store byte-identical, (2) remove the check-in AND the blobs
    // only it introduced, (3) leave a blob a retained check-in still names
    // exactly where it is — dropping that one would not free anything, it would
    // create a phantom — and (4) be undoable, because "recoverable" is the whole
    // difference between this verb and `shun`.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var first: []const u8 = undefined;
    const leaf = try purgeFixture(gpa, io, tmp.dir, &first);
    defer gpa.free(first);
    defer gpa.free(leaf);
    const swept = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(swept);
    const shared = try artifactNameByContent(gpa, io, tmp.dir, kept_body);
    defer gpa.free(shared);

    // Stand somewhere else: the current checkout is refused with no override.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "update", first });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }

    const before = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(before);

    // (1) Refused, and byte-identical after — not merely "nothing removed".
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "purge", "checkins", leaf });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "1 check-in") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "pass --force to proceed") != null);
        // The refusal points at the recovery path, which is what distinguishes
        // this verb's warning from shun's "irreversible".
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "zit purge undo") != null);
    }
    {
        const after = try storeBytesAlloc(gpa, io, tmp.dir);
        defer gpa.free(after);
        try std.testing.expectEqualSlices(u8, before, after);
    }

    // (2) and (3): the accident's blob goes, the shared blob stays.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "purge", "checkins", "--reason", "swept in a build tree", "--force", leaf });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "purged:") != null);
    }
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp.dir, leaf));
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp.dir, swept));
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp.dir, shared));
    // No tombstone: a purge is local and recoverable, which is exactly why it
    // records nothing that would stop the content coming back from a peer.
    try std.testing.expectEqual(@as(usize, 0), try storeShunRowCount(gpa, io, tmp.dir));
    {
        // The retained check-in still reads, so the rewrite left no phantom.
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "keep.txt", first });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expectEqualStrings(kept_body, res.stdout);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"verify"});
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "0 faults") != null);
    }
    {
        // The reason is what `purge list` is for: which grave holds what.
        var res = try runZit(gpa, io, tmp.dir, &.{ "purge", "list" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "swept in a build tree") != null);
    }

    // (4) Undo puts the store back — byte-for-byte, since restoring is an
    // ordinary append of the same artifacts in the same order.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "purge", "undo", "1" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp.dir, leaf));
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp.dir, swept));
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"verify"});
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "0 faults") != null);
    }
    {
        // The graveyard is emptied by the undo, so nothing is left to obliterate.
        var res = try runZit(gpa, io, tmp.dir, &.{ "purge", "list" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expectEqualStrings("", res.stdout);
    }
}

test "cli purge: the current checkout and a non-leaf are both refused, applying NOTHING" {
    // The two refusals with no --force override between them, and the pin that
    // BOTH block the whole invocation rather than the offending token alone.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var first: []const u8 = undefined;
    const leaf = try purgeFixture(gpa, io, tmp.dir, &first);
    defer gpa.free(first);
    defer gpa.free(leaf);

    const before = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(before);

    // Standing on the leaf: purging it is refused even with --force.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "purge", "checkins", "--force", leaf });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "the current checkout") != null);
    }
    // Its parent has a child, so it is refused for the other reason — naming it
    // alone would orphan the leaf.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "purge", "checkins", "--force", first });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "not a leaf") != null);
    }
    // Naming BOTH still refuses: the leaf is the checkout, and one blocked
    // target stops the whole plan rather than the possible half of it.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "purge", "checkins", "--force", first, leaf });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "the current checkout") != null);
    }

    const after = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);
}

test "cli shun: an invocation naming the current checkout alongside a shunnable blob applies NOTHING" {
    // THE PARTIAL-APPLICATION CASE, and the test that pins WHERE the
    // current-checkout guard sits rather than merely that it exists. An
    // invocation is ONE operation: a run that removed the blob and then reported
    // a refusal would have destroyed content irreversibly while telling the
    // operator nothing happened, and there is no way back from that half. Its own
    // test (not a tail assertion on the single-target case) so the byte-identity
    // claim is reached even when the simpler refusal is broken too.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(doomed);
    const before = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(before);

    var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", doomed, root });
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 1, res);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "is the check-in this checkout is on") != null);

    const after = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);
    try std.testing.expectEqual(@as(usize, 0), try storeShunRowCount(gpa, io, tmp.dir));
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp.dir, doomed));
}

test "cli shun: a hash this repository does not hold is recorded and exits 0; a prefix that matches nothing is refused" {
    // The pre-emptive block. sync §3.3 L527-530 filters the phantom scan on the
    // shun table, so a name recorded before the artifact ever arrives is never
    // requested — recording one is meaningful and costs no rewrite. An
    // ABBREVIATED prefix is the opposite case: the table is keyed by complete
    // name (§3.3 L518-519), a prefix cannot be completed, and a row keyed on one
    // would read as a removal while naming nothing.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const absent = "dead" ++ "beef" ** 15; // 64 hex, held by nothing here

    {
        // Nothing is held, so NO REWRITE RUNS: the report says a row was recorded
        // and refuses to claim a removal. A whole-store rewrite here would cost
        // O(store) plus transient room for both generations to reclaim nothing, and
        // would end in a "shun" whose byte count had grown.
        var res = try runCommitEnv(gpa, io, tmp.dir, .{}, &.{ "shun", "--force", "--reason", "pre-emptive block", absent });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expectEqualStrings(
            "shunned: 1 hash recorded; none is held here, so nothing was removed\n",
            res.stdout,
        );
    }
    try std.testing.expectEqual(@as(usize, 1), try storeShunRowCount(gpa, io, tmp.dir));
    {
        // `--list` shows it, with the recorded date and the reason. The date comes
        // from the pinned SOURCE_DATE_EPOCH (1234567890 = 2009-02-13).
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--list" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expectEqualStrings(
            absent ++ "  2009-02-13  pre-emptive block\n",
            res.stdout,
        );
    }
    {
        // A prefix matching nothing: refused, and it teaches the way through.
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", "dead" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no artifact matches 'dead'") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "give the complete name") != null);
    }
    // Still exactly one row: the refused prefix recorded nothing.
    try std.testing.expectEqual(@as(usize, 1), try storeShunRowCount(gpa, io, tmp.dir));
    {
        // A token that is not a hash at all is refused the same way.
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", "not-a-hash" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "'not-a-hash' is not an artifact name") != null);
    }
    {
        // An abbreviated prefix that DOES resolve is accepted — resolution to
        // exactly one complete name happens before any row is written.
        const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
        defer gpa.free(doomed);
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", doomed[0..8] });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "1 hash recorded, 1 artifact removed") != null);
        try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp.dir, doomed));
    }
}

test "cli shun --defer: the content stays on disk, the mask is immediate, and a later zit pack reclaims" {
    // `--defer` is a name-level mask, not a byte-level one. The bytes remain and
    // stay reconstructible through any delta stored against them until a rewrite
    // runs — which is exactly why `shun_help` refuses to describe --defer as
    // having removed anything.
    //
    // NOTE on the pinned property: a tombstone is an APPENDED frame, so `.zit`
    // cannot come out the same SIZE — it necessarily grows by one frame. What
    // --defer promises is that the CONTENT was not reclaimed, and that is what is
    // asserted here.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(doomed);

    {
        // --defer still needs --force: the row it appends is just as irreversible
        // as the rewrite's, and the mask takes effect immediately.
        //
        // THE REFUSAL MUST DESCRIBE THE RUN THAT WAS ASKED FOR. Against the real
        // binary: a --defer run removes nothing now, so it must not claim a permanent
        // removal, and the command it suggests must carry --defer. An operator who
        // pastes a bare `zit shun --force` out of this block escalates a deferred
        // tombstone into a whole-store reclaim of every deferred row, named or not.
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--defer", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "1 hash would be shunned") != null);
        try std.testing.expect(std.mem.indexOf(
            u8,
            res.stderr,
            "the bytes of 1 artifact stay on disk and stay reconstructible until 'zit pack'",
        ) != null);
        try std.testing.expect(std.mem.indexOf(
            u8,
            res.stderr,
            "run 'zit shun --defer --force <HASH>...' to proceed\n",
        ) != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "permanently removed") == null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "run 'zit shun --force") == null);
    }
    try std.testing.expectEqual(@as(usize, 0), try storeShunRowCount(gpa, io, tmp.dir));

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--defer", "--force", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expectEqualStrings("", res.stderr);
        try std.testing.expectEqualStrings(
            "shunned: 1 hash recorded; run 'zit pack' to reclaim the space\n",
            res.stdout,
        );
    }
    // The row is durable and the CONTENT IS STILL THERE — the whole point of the
    // deferred path, and the reason the help text says the bytes stay
    // reconstructible.
    try std.testing.expectEqual(@as(usize, 1), try storeShunRowCount(gpa, io, tmp.dir));
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp.dir, doomed));
    {
        // The mask, however, is immediate: reads answer identically either way.
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "doomed.txt", root });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
    }
    {
        // ...and `zit pack` is what actually removes them.
        var res = try runZit(gpa, io, tmp.dir, &.{"pack"});
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "1 shunned reclaimed") != null);
    }
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp.dir, doomed));
    // The tombstone outlived the content, so a later sync will not re-fetch it.
    try std.testing.expectEqual(@as(usize, 1), try storeShunRowCount(gpa, io, tmp.dir));
}

test "cli shun: a 40-hex ABBREVIATION of a 64-hex name resolves and removes, rather than tombstoning nothing" {
    // THE WIDTH TRAP. 40 hex is a COMPLETE sha1 name and also a legal prefix of a
    // 64-hex sha3 one. Deciding by width alone — "40 or 64 hex means complete name,
    // skip resolution" — makes this invocation record a row naming nothing, report
    // success, rewrite the store for no reason, and leave the content the operator
    // was ordered to remove still readable. The `unresolved` refusal exists to stop
    // exactly "a row that names nothing"; only asking the store FIRST keeps it in
    // the path. 39 and 41 hex always worked; this pins the two widths that did not.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(doomed);
    // The fixture is a default (sha3-256) repository, so the name is 64 hex and a
    // 40-hex slice of it is an abbreviation, not a name.
    try std.testing.expectEqual(@as(usize, 64), doomed.len);

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", doomed[0..40] });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "1 hash recorded, 1 artifact removed") != null);
    }
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp.dir, doomed));
    {
        // The row names the COMPLETE artifact, not the 40-hex token.
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--list" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, doomed) != null);
    }
    {
        // ...and the content is actually gone, which is the claim that used to be
        // false while the exit code said 0.
        var res = try runZit(gpa, io, tmp.dir, &.{ "cat", "doomed.txt", root });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expectEqualStrings("", res.stdout);
    }
}

test "cli shun: the current-checkout stop fires on a 40-hex abbreviation of the check-in too" {
    // The same width trap, aimed at the one refusal with no override: a token that
    // never resolves cannot be compared against the checkout's name, so a
    // width-first rule let `zit shun --force <40 hex of the current check-in>`
    // through with "0 artifacts removed" instead of refusing.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const before = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(before);

    var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", root[0..40] });
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 1, res);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "is the check-in this checkout is on; refusing") != null);

    const after = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);
    try std.testing.expectEqual(@as(usize, 0), try storeShunRowCount(gpa, io, tmp.dir));
}

test "cli shun: one artifact named twice in one invocation is ONE row, not a corruption claim" {
    // `packLog`'s row-count gate requires `extra_shuns` rows to be DISTINCT, so an
    // undeduplicated repeat used to abort with `zit shun: repository is corrupt` —
    // a false and alarming diagnostic on the one verb where an operator can least
    // afford to doubt their store. The prefix form is the dangerous one: an operator
    // pasting a takedown list beside an abbreviation from `zit log` cannot see that
    // the two name the same artifact.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(doomed);

    {
        // The refusal collapses the duplicate too: listing one artifact twice would
        // overstate what the operator is consenting to.
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", doomed, doomed[0..12] });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "1 hash would be shunned") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "1 artifact would be permanently removed") != null);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", doomed, doomed[0..12] });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "corrupt") == null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "1 hash recorded, 1 artifact removed") != null);
    }
    // ONE row, so `recorded` is the count of rows actually written.
    try std.testing.expectEqual(@as(usize, 1), try storeShunRowCount(gpa, io, tmp.dir));
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp.dir, doomed));
}

test "cli shun --force after --defer RECLAIMS rather than reporting nothing to do" {
    // A hash tombstoned by an earlier `--defer` still has its bytes on disk. Folding
    // that into "already shunned" made `zit shun --force H` print "nothing to do"
    // and exit 0 over content the operator had just been ordered to remove — with
    // the reclaim unreachable through this verb at all. So it is its own
    // disposition: no new row, and still a rewrite.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(doomed);

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--defer", "--force", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp.dir, doomed));

    {
        // Still refused without --force: this run destroys content, so it is not the
        // no-op path even though it records nothing.
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "no new hash would be recorded") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "already shunned; content still here and would be removed") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "nothing to do") == null);
    }
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp.dir, doomed));

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "0 hashes recorded, 1 artifact removed") != null);
    }
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp.dir, doomed));
    // Exactly one row throughout: the reclaim added none.
    try std.testing.expectEqual(@as(usize, 1), try storeShunRowCount(gpa, io, tmp.dir));
    {
        // NOW it is a no-op, and only now may it say so.
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "nothing to do; 1 hash already shunned") != null);
    }
}

test "cli shun: a --force run turned away by the temp lock records NO tombstone" {
    // TWO PROPERTIES IN ONE, both about the fixed-name temp `pack` and `shun` share.
    //
    // (1) An overlapping rewrite is turned away with a diagnostic that names what is
    //     happening. Unguarded, the two runs truncate each other's in-flight new
    //     generation and both fail the §7.7 reconstruct-and-rehash pass, reported as
    //     `error.Corrupt` — two healthy processes telling the operator their
    //     repository is corrupt, while the shun silently does not happen.
    //
    // (2) THE ONE-DURABLE-TRANSITION RULE, pinned at the RUNNER rather than at
    //     `rewriteAndSwap`. `runShun` must reach the store only through
    //     `extra_shuns`; append the rows to `.zit` first and then rewrite — two
    //     durable transitions — and this aborted run leaves a tombstone behind with
    //     the content it names still present. Holding the lock from the test is a
    //     deterministic abort seam inside the rewrite, no race required.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(doomed);
    const before = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(before);

    // Stand in for the concurrent rewrite: hold the rewrite lock for the whole
    // of the child's run.
    var held = try holdRewriteLock(io, tmp.dir);
    defer held.release(io);

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "another repository rewrite is in progress") != null);
        // Never the word that used to come out of this collision.
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "corrupt") == null);
    }

    // NO TOMBSTONE, and `.zit` byte-identical: the rows never touched the live log.
    try std.testing.expectEqual(@as(usize, 0), try storeShunRowCount(gpa, io, tmp.dir));
    const after = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp.dir, doomed));
}

test "cli shun: the APPEND paths are turned away by the temp lock too, recording nothing" {
    // The append paths — `--defer`, and a `--force` run with nothing to reclaim —
    // reopen `.zit` and commit on the strength of a snapshot taken earlier. Without
    // the same exclusive lock the rewriting verbs take, a `zit pack` (or a
    // `zit shun --force`) that renames its new generation over `.zit` after that
    // reopen leaves the row landing on an ORPHANED INODE: it is lost with the old
    // generation while this run reports success. On the `--defer` path that means the
    // deferred shun silently does not exist, having just told the operator to run
    // `zit pack` to finish it — the one outcome this verb must never produce.
    //
    // Holding the lock from the test stands in for the concurrent rewrite, which
    // makes this deterministic rather than a race.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(doomed);
    const absent = "dead" ++ "beef" ** 15; // 64 hex, held by nothing here
    const before = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(before);

    var held = try holdRewriteLock(io, tmp.dir);
    defer held.release(io);

    {
        // `--defer`: content to leave behind, one row to append.
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--defer", "--force", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "another repository rewrite is in progress") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "corrupt") == null);
    }
    {
        // The purely pre-emptive path: nothing to reclaim, so it appends too.
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", absent });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "another repository rewrite is in progress") != null);
    }

    // Nothing recorded and `.zit` byte-identical on both paths.
    try std.testing.expectEqual(@as(usize, 0), try storeShunRowCount(gpa, io, tmp.dir));
    const after = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp.dir, doomed));
}

test "cli shun: a --reason carrying a newline is refused rather than forging a --list row" {
    // `--list` is the only view of a durable, irreversible, un-removable record and
    // it prints one row per line, so a newline inside a reason manufactures a row
    // there indistinguishable from a real one. Reasons arrive from scripts and pasted
    // takedown text, not only from typing. NO DRAFT SPEAKS TO THIS: refusing at input
    // is a local decision, taken the same way the length gate was — refuse rather
    // than mangle a note that may be a legal reference.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(doomed);
    const before = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(before);

    var res = try runZit(gpa, io, tmp.dir, &.{
        "shun",                                            "--defer", "--force", "--reason",
        "legit\ndeadbeefdeadbeef  2020-01-01  forged row", doomed,
    });
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 1, res);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "--reason contains a control byte") != null);

    const after = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);
}

test "cli shun: the usage surface — no hash, --list with a positional, an unknown option, and --help" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const before = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(before);

    const Case = struct { argv: []const []const u8, needle: []const u8 };
    for ([_]Case{
        .{ .argv = &.{"shun"}, .needle = "no hash given" },
        .{ .argv = &.{ "shun", "--list", "aabb" }, .needle = "unexpected argument 'aabb'" },
        .{ .argv = &.{ "shun", "--force", "-f", "aabb" }, .needle = "unknown option '-f'" },
    }) |c| {
        var res = try runZit(gpa, io, tmp.dir, c.argv);
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 2, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, c.needle) != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "try 'zit shun --help'") != null);
    }
    {
        // `-h` wins over `--force` and over a real hash: help never destroys.
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", root, "-h" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "usage: zit shun") != null);
        // The two disclosures the help text is REQUIRED to carry: --defer leaves
        // the bytes reconstructible, and a running `zit serve` keeps serving them.
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "reconstructible") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "zit serve") != null);
    }
    {
        // `-R` is refused by name: a whole-store rewrite operates on the
        // discovered checkout, never on a foreign repository.
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "-R", ".", "--force", "aabbccdd" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 2, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "is not supported here") != null);
    }
    // Not one of those runs touched the store.
    const after = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);
}

test "cli shun: a check-in that is not the current checkout's names its own consequence and can be forced" {
    // The manifest arm of the per-type diagnostic, through the real binary: the
    // report says a check-in's descendants become unresolvable, and — unlike the
    // current checkout — the operator's --force is obeyed.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    try writeFile(io, tmp.dir, "keep.txt", "a second revision so the root is not the checkout\n");
    const second = try commitHashDup(gpa, io, tmp.dir, "second");
    defer gpa.free(second);

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", root });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 1, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "checkin  a check-in; its descendants become unresolvable") != null);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", root });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "1 hash recorded, 1 artifact removed") != null);
    }
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp.dir, root));
}

// --- pull with a repository shun table (sync §3.3 L527-530) ----------------
//
// The durable end of the phantom-scan shun filter, through the REAL binary: a
// repository that has shunned a hash the peer holds must not fetch it back, and
// must not fail for declining to. §3.3 filters the phantom SCAN, not phantom
// creation, so B still mints the phantom from A's `igot` — it simply never asks,
// on this pull or any later one.
//
// The shun rows are written straight into B's log through the library (the `zit
// shun` verb is a separate slice); everything else is the real CLI over loopback
// HTTP, so this is the guard that the built shun set actually reaches the pull
// client. Replace `shun.shunSet()` with `.none` at `pullRun`'s call site and this
// test fails.

/// Add one shun row naming `digest` to `dir/.zit` (sync §3.3 L518-519 — keyed by
/// the complete artifact name, so the whole digest, never a prefix).
fn shunDigestInRepo(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, digest: []const u8) !void {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_write });
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    // `open` re-reads the stored policy from the header, so the kind passed here
    // is only the empty-file seed value and never overrides B's own.
    var log = try logstore.LogStore.open(gpa, fs.storageFile(), @intFromEnum(artifact.defaultPolicy(false, false)));
    defer log.deinit(gpa);
    try log.putShun(digest, "e2e operator request", 1);
    try log.commit();
}

fn digestSetContains(set: [][]u8, needle: []const u8) bool {
    for (set) |d| {
        if (std.mem.eql(u8, d, needle)) return true;
    }
    return false;
}

test "cli pull: an artifact in the repository shun table is not fetched from a peer that holds it (sync §3.3 L527-530)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A: init + a first check-in.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "one.txt", "first artifact\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"one.txt"});
    var commit1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit1);
    try expectExit(commit1.term, 0);

    // Clone A's seed state into B, then let A grow past it.
    const clone_port = try freshLoopbackPort(io);
    var clone_server = try spawnServe(gpa, io, tmp_a.dir, clone_port);
    try waitForListen(io, clone_port);
    const clone_url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{clone_port});
    defer gpa.free(clone_url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", clone_url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    clone_server.kill(io);

    try writeFile(io, tmp_a.dir, "two.txt", "an artifact B will shun\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"two.txt"});
    var commit2 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "grow" });
    defer freeResult(gpa, &commit2);
    try expectExit(commit2.term, 0);

    var b_repo = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_repo.close(io);

    // Everything A gained past the clone — exactly what an unshunned pull would
    // fetch — is shunned in B.
    const a_digests = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);
    const b_before = try collectArtifactDigests(gpa, io, b_repo);
    defer freeDigests(gpa, b_before);
    var shunned: usize = 0;
    for (a_digests) |d| {
        if (digestSetContains(b_before, d)) continue;
        try shunDigestInRepo(gpa, io, b_repo, d);
        shunned += 1;
    }
    try std.testing.expect(shunned > 0); // the pull has something to decline

    // Pull from A. A announces every one of those hashes, B mints their phantoms
    // and asks for none of them.
    const pull_port = try freshLoopbackPort(io);
    var pull_server = try spawnServe(gpa, io, tmp_a.dir, pull_port);
    defer pull_server.kill(io);
    try waitForListen(io, pull_port);
    const pull_url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{pull_port});
    defer gpa.free(pull_url);
    var pull = try runZit(gpa, io, b_repo, &.{ "pull", pull_url });
    defer freeResult(gpa, &pull);

    // Exit 0 and a silent stderr: a deliberately-suppressed phantom is not an
    // unmet expectation, so the §3.7 clean terminal is a SUCCESS. Were it counted,
    // `zit pull` would report "the peer holds N artifacts this pull did not
    // obtain" here and on every later pull, forever, for obeying the shun rows.
    try expectExitDiag(pull.term, 0, pull);
    try std.testing.expectEqualStrings("", pull.stderr);

    // THE PROOF: B's on-disk artifact set is unchanged — not one shunned artifact
    // came over the wire.
    const b_after = try collectArtifactDigests(gpa, io, b_repo);
    defer freeDigests(gpa, b_after);
    try expectSameDigestSet(b_before, b_after);

    // The same guard for `zit sync`, whose pull half runs the very same §3.3 scan
    // — the filter is a property of the operation, not of one verb's plumbing.
    var sync_run = try runZit(gpa, io, b_repo, &.{ "sync", pull_url });
    defer freeResult(gpa, &sync_run);
    try expectExitDiag(sync_run.term, 0, sync_run);
    const b_after_sync = try collectArtifactDigests(gpa, io, b_repo);
    defer freeDigests(gpa, b_after_sync);
    try expectSameDigestSet(b_before, b_after_sync);
}

// ---------------------------------------------------------------------------
// sync §9.5 L1988-1992 — the SEND side of the shun filter, through the REAL
// binary over real loopback HTTP.
//
//   "A subsequent read-authorized transfer path requests igot for every
//    eligible holding after the Section 3.3 shun, phantom, private, and
//    peer-has filters"
//
// `zit shun --force --defer` records the row WITHOUT reclaiming, so the bytes are
// still in the served store: only the send-side filter can keep them off the wire.
// That makes `--defer` the sharper fixture than `--force` alone — a reclaimed
// artifact would be absent for a reason that has nothing to do with §9.5.
//
// THE CLONE TEST BELOW IS NOT §9.5, and says so in one place rather than in its
// title: §9.5's sentence is scoped to the `send-catalog` pragma's igot inventory,
// §3.4 L545-547 hands clone to §8.1-§8.4, and §8.2 L1504-1506 licenses only the
// gap MECHANISM. Withholding a shunned holding from a clone batch is this
// implementation's extension — see the erratum candidate on `LogIndexStore.masked`
// — and the test pins the extension, not a clause.
// ---------------------------------------------------------------------------

/// Every digest in `all` except the one whose lowercase-hex spelling is `drop_hex`
/// — `collectArtifactDigests` yields RAW binary digests while a shun names an
/// artifact by its complete hex name, so the comparison happens in hex. Caller
/// frees the outer slice only; the element slices are borrowed from `all`.
fn digestsExcept(gpa: std.mem.Allocator, all: []const []const u8, drop_hex: []const u8) ![][]const u8 {
    var out: std.ArrayListUnmanaged([]const u8) = .empty;
    errdefer out.deinit(gpa);
    for (all) |d| {
        var hex_buf: [2 * 64]u8 = undefined;
        if (d.len * 2 <= hex_buf.len) {
            const hex = std.fmt.bufPrint(&hex_buf, "{x}", .{d}) catch unreachable;
            if (std.mem.eql(u8, hex, drop_hex)) continue;
        }
        try out.append(gpa, d);
    }
    return out.toOwnedSlice(gpa);
}

test "cli clone: a deferred-shun artifact is served to nobody — the clone converges without it and without error (extension; erratum candidate vs sync §8.1/§8.2)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    const root = try shunFixture(gpa, io, tmp_a.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp_a.dir, doomed_body);
    defer gpa.free(doomed);

    // Record the shun WITHOUT reclaiming: the bytes stay in A's store.
    {
        var res = try runZit(gpa, io, tmp_a.dir, &.{ "shun", "--force", "--defer", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    // The fixture's premise: masked but STILL STORED, so nothing but the send-side
    // filter can keep it off the wire.
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp_a.dir, doomed));

    const a_digests = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_digests);
    const want = try digestsExcept(gpa, a_digests, doomed);
    defer gpa.free(want);
    // The shunned artifact really is one of A's holdings, and A holds more.
    try std.testing.expectEqual(a_digests.len - 1, want.len);
    try std.testing.expect(want.len >= 1);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
    defer freeResult(gpa, &clone);
    // ABSENT, not an error: the clone completes clean. A protocol error or a
    // dangling delta would be the CORRUPT outcome this filter exists to avoid.
    try expectExitDiag(clone.term, 0, clone);
    try std.testing.expectEqualStrings("", clone.stderr);

    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    const b_digests = try collectArtifactDigests(gpa, io, b_dir);
    defer freeDigests(gpa, b_digests);
    try expectSameDigestSet(want, b_digests);
    // …and stated the other way round, so a set comparison that somehow admitted
    // it could not pass: B does not hold the shunned artifact at all.
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, b_dir, doomed));
}

/// POST `plain` to the served repository as an `application/x-fossil` sync
/// request and return the DECODED reply body (caller frees). Raw HTTP rather than
/// a `zit` subcommand because no verb emits a bare `gimme` for an arbitrary hash —
/// that card is what §3.3 L532-535's "If absent, silently skip" is about.
fn postXferPlain(gpa: std.mem.Allocator, io: std.Io, port: u16, plain: []const u8) ![]u8 {
    const body = try container.encode(gpa, plain, .default);
    defer gpa.free(body);

    const stream = try dialServe(io, port);
    defer stream.close(io);
    {
        var wbuf: [512]u8 = undefined;
        var w = stream.writer(io, &wbuf);
        const head = try std.fmt.allocPrint(
            gpa,
            "POST /xfer HTTP/1.1\r\nHost: z\r\nContent-Type: application/x-fossil\r\n" ++
                "Content-Length: {d}\r\nConnection: close\r\n\r\n",
            .{body.len},
        );
        defer gpa.free(head);
        try w.interface.writeAll(head);
        try w.interface.writeAll(body);
        try w.interface.flush();
    }

    // Read to EOF (the server closed the connection), then split off the head. A
    // READ FAULT is not end-of-stream: both callers assert on a name being ABSENT
    // from the reply, which a truncated body satisfies for free, so a socket error
    // must fail the test rather than shorten the body.
    var rbuf: [4096]u8 = undefined;
    var r = stream.reader(io, &rbuf);
    const raw = try r.interface.allocRemaining(gpa, .limited(1 << 20));
    defer gpa.free(raw);

    const sep = std.mem.indexOf(u8, raw, "\r\n\r\n") orelse return error.NoHttpHeadEnd;
    if (std.mem.indexOf(u8, raw[0..sep], " 200 ") == null) return error.NotOk;
    return container.decode(gpa, raw[sep + 4 ..], 1 << 20);
}

test "cli serve: a slow-but-draining peer receives a multi-megabyte clone reply intact — the write deadline scales with the reply (at the §8.2 byte budget)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // A repo whose one clone batch approaches the §8.2 byte budget:
    // deterministic incompressible blobs, so the reply body is multi-MiB and
    // the server's write genuinely blocks on the peer draining it (loopback
    // kernel buffering absorbs the first few hundred KiB for free).
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var prng = std.Random.DefaultPrng.init(0x5eed_c0de);
    const blob = try gpa.alloc(u8, 512 * 1024);
    defer gpa.free(blob);
    var names_buf: [8][16]u8 = undefined;
    var i: usize = 0;
    while (i < 8) : (i += 1) {
        prng.random().bytes(blob);
        const name = std.fmt.bufPrint(&names_buf[i], "blob{d}.bin", .{i}) catch unreachable;
        try writeFile(io, tmp.dir, name, blob);
        try addManaged(gpa, io, tmp.dir, &.{name});
    }
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "wide" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // The raw clone opener a transport would send.
    const req = try container.encode(gpa, "pragma client-version 1\nclone 3 1\n", .default);
    defer gpa.free(req);
    const stream = try dialServe(io, port);
    defer stream.close(io);
    {
        var wbuf: [512]u8 = undefined;
        var w = stream.writer(io, &wbuf);
        const head = try std.fmt.allocPrint(
            gpa,
            "POST /xfer HTTP/1.1\r\nHost: z\r\nContent-Type: application/x-fossil\r\n" ++
                "Content-Length: {d}\r\nConnection: close\r\n\r\n",
            .{req.len},
        );
        defer gpa.free(head);
        try w.interface.writeAll(head);
        try w.interface.writeAll(req);
        try w.interface.flush();
    }

    // Drain the reply THROTTLED to ~427 KiB/s — a healthy DSL-class peer.
    // Over a ~4 MiB reply that is ~10 s of draining: far past the flat 5 s
    // deadline (which used to cancel this healthy write mid-flight — the
    // regression shape), comfortably above the scaled deadline's minimum
    // drain rate. The whole body must arrive: a truncated reply is exactly
    // the regression this test exists to catch.
    var chunk: [64 * 1024]u8 = undefined;
    var raw: std.ArrayList(u8) = .empty;
    defer raw.deinit(gpa);
    var rbuf: [4096]u8 = undefined;
    var r = stream.reader(io, &rbuf);
    while (true) {
        const n = r.interface.readSliceShort(&chunk) catch break;
        if (n == 0) break;
        try raw.appendSlice(gpa, chunk[0..n]);
        io.sleep(.{ .nanoseconds = 150 * std.time.ns_per_ms }, .awake) catch {};
    }

    // The head parsed 200 with a Content-Length the body honors in full.
    const sep = std.mem.indexOf(u8, raw.items, "\r\n\r\n") orelse return error.NoHttpHeadEnd;
    try std.testing.expect(std.mem.indexOf(u8, raw.items[0..sep], " 200 ") != null);
    const cl_key = "content-length: ";
    const head_lower = try std.ascii.allocLowerString(gpa, raw.items[0..sep]);
    defer gpa.free(head_lower);
    const cl_at = std.mem.indexOf(u8, head_lower, cl_key) orelse return error.NoContentLength;
    const cl_end = std.mem.indexOfScalarPos(u8, head_lower, cl_at, '\r') orelse return error.NoContentLength;
    const content_length = try std.fmt.parseInt(usize, head_lower[cl_at + cl_key.len .. cl_end], 10);
    // The batch really was multi-MiB — the premise that makes the write block.
    try std.testing.expect(content_length >= 3 << 20);
    try std.testing.expectEqual(content_length, raw.items.len - (sep + 4));
}

test "cli serve: a --force shun's generation swap is served on the next round — the live server reopens the renamed store, no restart" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    const root = try shunFixture(gpa, io, tmp_a.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp_a.dir, doomed_body);
    defer gpa.free(doomed);
    const kept = try artifactNameByContent(gpa, io, tmp_a.dir, kept_body);
    defer gpa.free(kept);

    // ONE server for the whole test: the point is what happens without a restart.
    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    // Before the shun: a clone receives the doomed artifact — the premise.
    {
        var clone = try runZit(gpa, io, tmp_a.dir, &.{ "clone", url, "before" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
        var b_dir = try tmp_a.dir.openDir(io, "before", .{});
        defer b_dir.close(io);
        try std.testing.expect(try storeHoldsArtifact(gpa, io, b_dir, doomed));
    }

    // The takedown: --force reclaims by RENAMING a new generation over `.zit`.
    // The running server still holds the old inode's handle.
    {
        var res = try runZit(gpa, io, tmp_a.dir, &.{ "shun", "--force", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp_a.dir, doomed));

    // The very next round serves the rewritten generation: the doomed bytes are
    // gone from a fresh clone, the kept artifact still arrives — no restart.
    {
        var clone = try runZit(gpa, io, tmp_a.dir, &.{ "clone", "--no-open", url, "after" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
        var b_dir = try tmp_a.dir.openDir(io, "after", .{});
        defer b_dir.close(io);
        try std.testing.expect(!try storeHoldsArtifact(gpa, io, b_dir, doomed));
        try std.testing.expect(try storeHoldsArtifact(gpa, io, b_dir, kept));
    }
}

test "cli serve §9.1: the clone reply's push card carries a real derived servercode, not the placeholder" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // A clone reply rides UNCOMPRESSED (§8.1), so read the raw body rather
    // than routing through the container-decoding helper.
    const req = try container.encode(gpa, "pragma client-version 1\nclone 3 1\n", .default);
    defer gpa.free(req);
    const stream = try dialServe(io, port);
    defer stream.close(io);
    {
        var wbuf: [512]u8 = undefined;
        var w = stream.writer(io, &wbuf);
        const head = try std.fmt.allocPrint(
            gpa,
            "POST /xfer HTTP/1.1\r\nHost: z\r\nContent-Type: application/x-fossil\r\n" ++
                "Content-Length: {d}\r\nConnection: close\r\n\r\n",
            .{req.len},
        );
        defer gpa.free(head);
        try w.interface.writeAll(head);
        try w.interface.writeAll(req);
        try w.interface.flush();
    }
    var rbuf: [4096]u8 = undefined;
    var r = stream.reader(io, &rbuf);
    const raw = try r.interface.allocRemaining(gpa, .limited(1 << 20));
    defer gpa.free(raw);
    const sep = std.mem.indexOf(u8, raw, "\r\n\r\n") orelse return error.NoHttpHeadEnd;
    try std.testing.expect(std.mem.indexOf(u8, raw[0..sep], " 200 ") != null);
    const reply = raw[sep + 4 ..];
    const at = std.mem.indexOf(u8, reply, "\npush ") orelse return error.NoPushCard;
    const line_end = std.mem.indexOfScalarPos(u8, reply, at + 1, '\n') orelse return error.NoPushCard;
    const line = reply[at + 1 .. line_end];
    var toks = std.mem.tokenizeScalar(u8, line, ' ');
    _ = toks.next(); // "push"
    const servercode = toks.next() orelse return error.NoPushCard;
    // Real identity: 40 lowercase hex, and not the informational placeholder.
    try std.testing.expectEqual(@as(usize, 40), servercode.len);
    for (servercode) |c| try std.testing.expect(std.ascii.isHex(c) and !std.ascii.isUpper(c));
    try std.testing.expect(!std.mem.eql(u8, servercode, "0000000000000000"));
    try std.testing.expect(std.mem.indexOf(u8, servercode, "00000000") == null or !std.mem.allEqual(u8, servercode, '0'));
}

test "cli serve: a shunned holding is never igot-announced, and an inbound gimme for it is silently skipped (sync §9.5 L1988-1992, §3.3 L532-535)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    const root = try shunFixture(gpa, io, tmp_a.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp_a.dir, doomed_body);
    defer gpa.free(doomed);
    const kept = try artifactNameByContent(gpa, io, tmp_a.dir, kept_body);
    defer gpa.free(kept);

    {
        var res = try runZit(gpa, io, tmp_a.dir, &.{ "shun", "--force", "--defer", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    // Premise again: the server still HOLDS the bytes it must refuse to send.
    try std.testing.expect(try storeHoldsArtifact(gpa, io, tmp_a.dir, doomed));

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // ROUND 1 — the §3.2/§9.5 INVENTORY WALK. The client announces a hash this
    // repo does not hold, so the server answers with igot for "every eligible
    // holding after the Section 3.3 shun … filters" (§9.5 L1989-1991). The
    // unshunned neighbour proves the walk ran at all, so the shunned name's
    // absence is the filter and not an empty reply.
    {
        const absent = try gpa.alloc(u8, doomed.len);
        defer gpa.free(absent);
        @memset(absent, '0');
        const plain = try std.fmt.allocPrint(gpa, "pragma client-version 20000\nigot {s}\n", .{absent});
        defer gpa.free(plain);
        const reply = try postXferPlain(gpa, io, port, plain);
        defer gpa.free(reply);

        const kept_igot = try std.fmt.allocPrint(gpa, "igot {s}\n", .{kept});
        defer gpa.free(kept_igot);
        try std.testing.expect(std.mem.indexOf(u8, reply, kept_igot) != null);
        // The shunned name appears on NO card — not igot (it is not an eligible
        // holding), not gimme (§3.3 L527-530 excludes it from the phantom scan),
        // and above all not `error`.
        try std.testing.expect(std.mem.indexOf(u8, reply, doomed) == null);
        try std.testing.expect(std.mem.indexOf(u8, reply, "error") == null);
    }

    // ROUND 2 — an inbound `gimme` for the shunned hash. §3.3 L532-535: "If
    // absent, silently skip." The mask is what makes it absent to the
    // enumeration, so this needs no code of its own on the gimme path.
    {
        const plain = try std.fmt.allocPrint(
            gpa,
            "pragma client-version 20000\ngimme {s}\ngimme {s}\n",
            .{ doomed, kept },
        );
        defer gpa.free(plain);
        const reply = try postXferPlain(gpa, io, port, plain);
        defer gpa.free(reply);

        // The neighbour comes back as a `file` card…
        const kept_card = try std.fmt.allocPrint(gpa, "file {s} ", .{kept});
        defer gpa.free(kept_card);
        try std.testing.expect(std.mem.indexOf(u8, reply, kept_card) != null);
        // …and the shunned hash produces nothing at all, error card included.
        try std.testing.expect(std.mem.indexOf(u8, reply, doomed) == null);
        try std.testing.expect(std.mem.indexOf(u8, reply, "error") == null);
    }
}

// --- RECEIVE-side admission through the real binary (sync §7.7 L1434-1463) --
//
// The send-side filter above keeps a shunned artifact off the wire when WE are
// the sender. This is the other direction, and the one a shun is worthless
// without: a PEER pushes the shunned artifact AT us. §7.7 L1437-1440 — "A
// non-clone file whose target is shunned is consumed and discarded after its
// payload and pending private marker; it emits no error, does not set
// content-received, and makes no repository mutation." A server under
// push-receive is always non-clone, so this is the discard arm, unconditionally.
//
// WHY A RAW POST AND NOT `zit push`. A zit-to-zit push cannot reach this code at
// all: the receiving server's §3.3 L527-530 phantom scan already declines to
// `gimme` a shunned name, and a zit push client only uploads what the peer asked
// for — so the send-side filter masks the receive-side gap end to end, and a
// `zit push` e2e passes with the whole admission check deleted (checked, not
// assumed). A real Fossil push is UNSOLICITED: it sends the content its own
// inventory says the peer lacks. `postXferPlain` is that shape, and it is the
// only shape that puts the receive path under test.
//
// The shun is `--force`, so A does not hold the bytes: the ONLY thing that can
// keep them out is admission at ingest.

test "cli serve: an unsolicited push of a shunned artifact is discarded — nothing lands and no error card comes back (sync §7.7 L1437-1440)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    const root = try shunFixture(gpa, io, tmp_a.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp_a.dir, doomed_body);
    defer gpa.free(doomed);

    {
        var res = try runZit(gpa, io, tmp_a.dir, &.{ "shun", "--force", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
    // The premise: the operator's removal actually happened.
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp_a.dir, doomed));

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    // §9.1 — a `push SERVERCODE PROJECTCODE` card, then the artifact itself, sent
    // without ever having been asked for.
    const plain = try std.fmt.allocPrint(
        gpa,
        "pragma client-version 20000\npush {s} {s}\nfile {s} {d}\n{s}",
        .{ "1" ** 40, "2" ** 40, doomed, doomed_body.len, doomed_body },
    );
    defer gpa.free(plain);
    const reply = try postXferPlain(gpa, io, port, plain);
    defer gpa.free(reply);

    // "it emits no error" — no error card of any kind, and the shunned name is
    // not echoed back on one either.
    try std.testing.expect(std.mem.indexOf(u8, reply, "error") == null);
    try std.testing.expect(std.mem.indexOf(u8, reply, doomed) == null);

    // The SAME card in DELTA form (§7.1 `file HASH DELTASRC SIZE`), against a
    // basis this repository does not hold. That is the COMMON shape for a
    // shunned target — its neighbours are unlikely to still be here — and it is
    // the one that reaches the server's delta reconstruction. L1437-1440 draws no
    // distinction between the two forms: still no error card, still nothing
    // lands. The delta payload is deliberately junk; the basis lookup fails
    // before it is parsed, so the path is deterministic either way.
    const delta_plain = try std.fmt.allocPrint(
        gpa,
        "pragma client-version 20000\npush {s} {s}\nfile {s} {s} 4\njunk",
        .{ "1" ** 40, "2" ** 40, doomed, "3" ** 64 },
    );
    defer gpa.free(delta_plain);
    const delta_reply = try postXferPlain(gpa, io, port, delta_plain);
    defer gpa.free(delta_reply);
    try std.testing.expect(std.mem.indexOf(u8, delta_reply, "error") == null);
    try std.testing.expect(std.mem.indexOf(u8, delta_reply, doomed) == null);
    // "makes no repository mutation" — not even the DELTASRC phantom, because
    // admission is decided before DELTASRC is looked at (L1434-1435).
    try std.testing.expect(std.mem.indexOf(u8, delta_reply, "3" ** 64) == null);

    // THE PROOF: release the server's handles, then read A back off disk. The
    // repository the operator emptied is still empty of it.
    server.kill(io);
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, tmp_a.dir, doomed));
}

// ---------------------------------------------------------------------------
// `zit verify` — zit's own fsck. No contract doc and no RFC section stand
// behind this verb (it is a local design decision; repo-state §7.3 blesses a
// full rebuild as a fallback but says nothing about a verb's shape), so these
// titles cite neither a `§` nor a `B-` id.
// ---------------------------------------------------------------------------

/// The byte offset of the first PAYLOAD byte of the frame holding `body`.
///
/// THE PITFALL THIS EXISTS TO CLOSE: the two older corruption e2e tests flip at
/// `frame_offset + 10`, which lands inside the HEADER window (the hash), not the
/// payload. A payload-fault fixture that used that offset would pass for the
/// wrong reason — it would be exercising header-checksum detection. The payload
/// begins after `frameHeaderSpan` = 22 + hash_len bytes.
fn artifactPayloadOffset(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, body: []const u8) !u64 {
    var file = try dir.openFile(io, ".zit", .{});
    defer file.close(io);
    const st = try file.stat(io);
    var fs = storage.FileStorage.init(io, file, st.size);
    var log = try logstore.LogStore.openReadOnly(gpa, fs.storageFile());
    defer log.deinit(gpa);

    const rids = try log.ridsInPersistedOrder(gpa);
    defer gpa.free(rids);
    for (rids) |rid| {
        const content = (try log.content(gpa, rid)) orelse continue;
        defer gpa.free(content);
        if (!std.mem.eql(u8, content, body)) continue;
        return log.offsetOf(rid) + 22 + @as(u64, log.hashOf(rid).len);
    }
    return error.NoArtifactHoldsThoseBytes;
}

/// Flips every bit of the byte at `at` in `dir/.zit`.
fn flipStoreByte(io: std.Io, dir: std.Io.Dir, at: u64) !void {
    var file = try dir.openFile(io, ".zit", .{ .mode = .read_write });
    defer file.close(io);
    var byte: [1]u8 = undefined;
    try std.testing.expectEqual(@as(usize, 1), try file.readPositionalAll(io, &byte, at));
    byte[0] ^= 0xff;
    try file.writePositionalAll(io, &byte, at);
}

test "cli verify: a clean repository verifies and exits 0" {
    // Two files, so a verb that reported damage on everything could not pass
    // this by accident.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);

    var res = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 0, res);
    try std.testing.expectEqualStrings("", res.stderr);
    try std.testing.expect(std.mem.startsWith(u8, res.stdout, "verified: "));
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "0 faults") != null);
}

test "cli verify: a bitflipped artifact payload is reported by name and exits 1" {
    // ACCEPTANCE 1. The fault line's job is to be pasted into `zit artifact` or
    // `zit shun`, so the FULL name has to appear — and the reason has to say
    // which check failed, or the assertion cannot tell the two verification
    // layers apart (see the mutation proof).
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(doomed);
    const at = try artifactPayloadOffset(gpa, io, tmp.dir, doomed_body);
    try flipStoreByte(io, tmp.dir, at);

    const before = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(before);

    var res = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 1, res);
    // Nothing on stdout: a run that found damage has no clean summary to
    // compose with.
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, doomed) != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "payload checksum mismatch") != null);

    // AN FSCK THAT MUTATES IS A BUG. Byte-identical, not same-length.
    const after = try storeBytesAlloc(gpa, io, tmp.dir);
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);
}

test "cli verify: a bitflipped config-record payload is reported and exits 1" {
    // THE SEMANTIC MUTATION TARGET. A config frame is never interned by `scan`,
    // so `ridsInPersistedOrder` never yields it and the per-artifact pass
    // structurally cannot see it. Only the whole-log frame pass catches this.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Fixture premise, pinned rather than assumed: a fresh `zit init` store is
    // the file header followed by exactly one config frame, whose one-byte
    // sentinel hash puts its payload at header_len + 22 + 1.
    {
        const bytes = try storeBytesAlloc(gpa, io, tmp.dir);
        defer gpa.free(bytes);
        try std.testing.expectEqual(@intFromEnum(logstore.FrameKind.config), bytes[logstore.header_len + 4]);
        try std.testing.expectEqual(@as(u8, 1), bytes[logstore.header_len + 5]);
    }
    try flipStoreByte(io, tmp.dir, logstore.header_len + 23);

    var res = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 1, res);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "config record") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "payload checksum mismatch") != null);
}

/// The start offset of the first frame of `kind` in a raw store image, found by
/// walking the frame geometry: payload_len at +0, flags at +4, hash_len at +5,
/// and an 8-aligned span of 26 + hash_len + payload_len.
fn frameStartOfKind(bytes: []const u8, kind: logstore.FrameKind) !usize {
    var off: usize = logstore.header_len;
    while (off + 22 <= bytes.len) {
        const pl: usize = std.mem.readInt(u32, bytes[off..][0..4], .little);
        const flags = bytes[off + 4];
        const hl: usize = bytes[off + 5];
        if (flags == @intFromEnum(kind)) return off;
        off += ((26 + hl + pl) + 7) / 8 * 8;
    }
    return error.NoFrameOfThatKind;
}

test "cli verify: a store the frame pass finds clean but no open will accept is a fault, not a clean run" {
    // THE ANSWER THIS VERB MUST NEVER GIVE. The frame pass walks raw bytes, so
    // it comes up clean on a shun row whose checksums are all valid and whose
    // payload `scan` still rejects — and the per-artifact pass then cannot open
    // the store at all. Reporting "0 faults" and exiting 0 on a repository
    // every other verb refuses to open would send an operator away satisfied
    // from a broken store, and contradicts verify's own exit-status table.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const doomed = try artifactNameByContent(gpa, io, tmp.dir, doomed_body);
    defer gpa.free(doomed);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", doomed });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }

    {
        var file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_write });
        defer file.close(io);
        const st = try file.stat(io);
        const bytes = try gpa.alloc(u8, @intCast(st.size));
        defer gpa.free(bytes);
        try std.testing.expectEqual(bytes.len, try file.readPositionalAll(io, bytes, 0));

        const start = try frameStartOfKind(bytes, .shun);
        const pl: usize = std.mem.readInt(u32, bytes[start..][0..4], .little);
        const hl: usize = bytes[start + 5];
        // A shun payload is epoch(8) | reason_len(2) | reason. Declaring a
        // reason longer than the payload can hold makes `parseShun` reject it,
        // while leaving every length the framing itself depends on untouched.
        std.mem.writeInt(u16, bytes[start + 22 + hl + 8 ..][0..2], 0xffff, .little);
        // Recompute the whole-frame checksum so the frame pass has no complaint:
        // the header window is untouched, so only this one has moved.
        var fcrc = std.hash.Crc32.init();
        fcrc.update(bytes[start .. start + 22 + hl + pl]);
        std.mem.writeInt(u32, bytes[start + 22 + hl + pl ..][0..4], fcrc.final(), .little);
        try file.writePositionalAll(io, bytes[start .. start + 22 + hl + pl + 4], @intCast(start));
    }

    var res = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 1, res);
    try std.testing.expectEqualStrings("", res.stdout);
    // The frame pass genuinely had nothing to say — that is the premise, not an
    // accident — so this can only be the second pass refusing the store.
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "payload checksum mismatch") == null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "could not be opened for per-artifact verification") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "bytes checked") != null);
}

test "cli verify: an incomplete final frame is reported but is not a fault (exit 0)" {
    // Guards against sending an operator to re-clone over an ordinary crash. A
    // torn tail is an interrupted write; the next write truncates it away.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);

    {
        var file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_write });
        defer file.close(io);
        const st = try file.stat(io);
        try file.setLength(io, st.size - 4);
    }

    var res = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 0, res);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "incomplete final frame") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "this is not corruption") != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "fault") == null);
    // `openReadOnly` refuses a torn tail, so the per-artifact pass could not
    // run — and the summary has to say so rather than report "0 artifacts",
    // which would read as "this store holds none".
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, "per-artifact verification did not run") != null);
}

test "cli verify: a corrupt frame header stops the walk and says so" {
    // A SHORT WALK MUST NOT READ AS A COMPLETE ONE. Past a damaged header
    // window there is no trustworthy frame boundary, so the byte accounting in
    // the verdict has to come up short of the file size.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const at = try artifactPayloadOffset(gpa, io, tmp.dir, doomed_body);
    const total = blk: {
        const bytes = try storeBytesAlloc(gpa, io, tmp.dir);
        defer gpa.free(bytes);
        break :blk bytes.len;
    };
    // `at - 10` is past the 10-byte prefix window (so the prefix crc still
    // passes) and inside the header crc's coverage — with a 32-byte hash it
    // lands in `delta_of`. Which header field it hits does not matter; that it
    // is a header-window byte and not a payload byte is the whole point.
    try flipStoreByte(io, tmp.dir, at - 10);

    var res = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 1, res);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "cannot verify past this point") != null);

    var total_buf: [32]u8 = undefined;
    const total_str = try std.fmt.bufPrint(&total_buf, " of {d} bytes checked", .{total});
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, total_str) != null);
    // The bytes-checked figure is short of the total, stated as its own fact.
    var full_buf: [64]u8 = undefined;
    const full_str = try std.fmt.bufPrint(&full_buf, "{d} of {d} bytes checked", .{ total, total });
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, full_str) == null);
}

test "cli verify: --help prints its own help, and an unknown option is a usage error" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "verify", "--help" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.startsWith(u8, res.stdout, "usage: zit verify"));
        try std.testing.expectEqualStrings("", res.stderr);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "verify", "--deep" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 2, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "--deep") != null);
    }
}

test "cli verify: a checksum-valid frame whose content does not match its name is caught by the per-artifact pass" {
    // THE SECOND LAYER'S OWN COVERAGE. Rewriting the frame's hash field AND
    // recomputing both checksums that cover it produces a frame the whole-log
    // pass has no complaint about — every checksum is valid — carrying content
    // that does not hash to the name the frame gives it. Only reconstructing the
    // artifact and re-hashing it finds this, which is why the two passes are not
    // redundant with each other.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const root = try shunFixture(gpa, io, tmp.dir);
    defer gpa.free(root);
    const payload_at = try artifactPayloadOffset(gpa, io, tmp.dir, doomed_body);

    var renamed_buf: [2 * logstore.max_hash_len]u8 = undefined;
    var renamed_len: usize = 0;
    {
        var file = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_write });
        defer file.close(io);
        const st = try file.stat(io);
        const bytes = try gpa.alloc(u8, @intCast(st.size));
        defer gpa.free(bytes);
        try std.testing.expectEqual(bytes.len, try file.readPositionalAll(io, bytes, 0));

        // Recover the frame start from the payload offset: the payload begins
        // 22 + hash_len bytes into the frame, and hash_len is the sixth byte of
        // the frame's prefix window — so the candidate start that agrees with
        // its own declared width is the right one.
        const start: usize = blk: {
            var hl: usize = logstore.min_hash_len;
            while (hl <= logstore.max_hash_len) : (hl += 1) {
                const cand: usize = @intCast(payload_at - 22 - hl);
                if (bytes[cand + 5] == hl) break :blk cand;
            }
            return error.FrameStartNotRecoverable;
        };
        const hl: usize = bytes[start + 5];
        const pl: usize = std.mem.readInt(u32, bytes[start..][0..4], .little);
        try std.testing.expectEqual(@as(usize, @intCast(payload_at)), start + 22 + hl);

        // Rename the frame, leaving its payload alone. The prefix window is
        // untouched (it does not cover the hash), so only the header checksum
        // and the whole-frame checksum have to be recomputed.
        bytes[start + 10] ^= 0xff;
        var hcrc = std.hash.Crc32.init();
        hcrc.update(bytes[start .. start + 18 + hl]);
        std.mem.writeInt(u32, bytes[start + 18 + hl ..][0..4], hcrc.final(), .little);
        var fcrc = std.hash.Crc32.init();
        fcrc.update(bytes[start .. start + 22 + hl + pl]);
        std.mem.writeInt(u32, bytes[start + 22 + hl + pl ..][0..4], fcrc.final(), .little);
        try file.writePositionalAll(io, bytes[start .. start + 22 + hl + pl + 4], @intCast(start));

        const lower = "0123456789abcdef";
        for (bytes[start + 10 ..][0..hl], 0..) |b, i| {
            renamed_buf[i * 2] = lower[b >> 4];
            renamed_buf[i * 2 + 1] = lower[b & 0x0f];
        }
        renamed_len = hl * 2;
    }
    const renamed = renamed_buf[0..renamed_len];

    var res = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 1, res);
    try std.testing.expectEqualStrings("", res.stdout);
    // No frame-pass complaint — every checksum is valid — so this line can only
    // have come from the per-artifact pass.
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "payload checksum mismatch") == null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, renamed) != null);
    try std.testing.expect(std.mem.indexOf(u8, res.stderr, "content does not hash to its name") != null);
}

// ---------------------------------------------------------------------------
// Consolidation Task 6 Step 8: after a full verb sweep the checkout root
// carries exactly the three live reserved names plus working files — no
// absorbed sibling, no legacy leftover, no stranded scratch. Run on a fresh
// checkout AND on one migrated mid-life from the legacy generation.
// ---------------------------------------------------------------------------

fn expectConsolidatedRoot(io: std.Io, dir: std.Io.Dir, working: []const []const u8) !void {
    // Reopened for iteration: the caller's handle may lack iterate rights,
    // which Linux enforces at readdir time (macOS does not).
    var it_dir = try dir.openDir(io, ".", .{ .iterate = true });
    defer it_dir.close(io);
    var entries = it_dir.iterate();
    outer: while (try entries.next(io)) |entry| {
        for ([_][]const u8{ ".zit", ".zit-checkout", ".zit-cache" }) |ok| {
            if (std.mem.eql(u8, entry.name, ok)) continue :outer;
        }
        for (working) |w| {
            if (std.mem.eql(u8, entry.name, w)) continue :outer;
        }
        std.debug.print("unexpected root entry after verb sweep: {s}\n", .{entry.name});
        return error.UnexpectedRootEntry;
    }
}

fn okZit(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, args: []const []const u8) !void {
    var res = try runZit(gpa, io, dir, args);
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
}

fn okCommit(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, msg: []const u8) !void {
    var res = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", msg });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
}

/// Every mutating and reading verb at least once, ending with a clean tree:
/// merge+commit, add/mv/rm (each committed), update + undo/redo, revert,
/// bisect record+reset, status/extras/clean.
fn verbSweepBody(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, fx: *const MergeDiamond) !void {
    try okZit(gpa, io, dir, &.{ "merge", fx.other });
    try okCommit(gpa, io, dir, "merged");

    try writeFile(io, dir, "extra.txt", "extra\n");
    try okZit(gpa, io, dir, &.{ "add", "extra.txt" });
    try okCommit(gpa, io, dir, "add extra");
    try okZit(gpa, io, dir, &.{ "mv", "extra.txt", "moved.txt" });
    try okCommit(gpa, io, dir, "move extra");
    // Plain rm untracks and leaves the working file — which gives `extras`
    // something to report and `clean` something to sweep below.
    try okZit(gpa, io, dir, &.{ "rm", "moved.txt" });
    try okCommit(gpa, io, dir, "drop moved");

    try okZit(gpa, io, dir, &.{ "update", fx.base });
    try okZit(gpa, io, dir, &.{"undo"});
    try okZit(gpa, io, dir, &.{"redo"});
    try okZit(gpa, io, dir, &.{"undo"}); // end back at the head

    try writeFile(io, dir, "poem", "scribble\n");
    try okZit(gpa, io, dir, &.{ "revert", "poem" });

    try okZit(gpa, io, dir, &.{ "bisect", "bad", fx.current });
    try okZit(gpa, io, dir, &.{ "bisect", "reset" });

    try okZit(gpa, io, dir, &.{"status"});
    try okZit(gpa, io, dir, &.{"extras"});
    try okZit(gpa, io, dir, &.{ "clean", "--force" }); // flagless clean refuses while extras exist (B-clean-1)
}

/// Rewrites a container-generation checkout as the LEGACY generation an old
/// binary would have left: each section's raw bytes become the sibling file
/// (the payload formats are identical) and the checkout section's TLV record
/// becomes `.zit-checkout` itself (§8's byte-1 dispatch reads it as legacy).
fn devolveCheckoutToLegacy(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !void {
    inline for (.{
        .{ checkout.SectionTag.managed, ".zit-managed" },
        .{ checkout.SectionTag.undo, ".zit-undo" },
        .{ checkout.SectionTag.bisect, ".zit-bisect" },
    }) |pair| {
        if (try checkout.readSectionAlloc(gpa, io, dir, pair[0])) |bytes| {
            defer gpa.free(bytes);
            try writeFile(io, dir, pair[1], bytes);
        }
    }
    const record = (try checkout.readSectionAlloc(gpa, io, dir, .checkout)).?;
    defer gpa.free(record);
    try writeFile(io, dir, checkout.filename, record);
}

test "consolidation: a full verb sweep on a fresh checkout leaves exactly { .zit, .zit-checkout, .zit-cache } + working files" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
    defer fx.deinit(gpa);
    try verbSweepBody(gpa, io, tmp.dir, &fx);
    try expectConsolidatedRoot(io, tmp.dir, &.{"poem"});
}

test "consolidation: the same sweep on a migrated legacy checkout converges to the same root" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
    defer fx.deinit(gpa);
    try devolveCheckoutToLegacy(gpa, io, tmp.dir);
    try std.testing.expect(fileExists(io, tmp.dir, ".zit-managed")); // genuinely legacy before the sweep

    try verbSweepBody(gpa, io, tmp.dir, &fx);
    try expectConsolidatedRoot(io, tmp.dir, &.{"poem"});
    // The absorbed siblings were folded in and swept, never resurrected.
    try std.testing.expect(!fileExists(io, tmp.dir, ".zit-managed"));
    try std.testing.expect(!fileExists(io, tmp.dir, ".zit-undo"));
    try std.testing.expect(!fileExists(io, tmp.dir, ".zit-bisect"));
}

test "consolidation: every legacy leftover on an old checkout migrates or surfaces, and clean ends the story" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // An old checkout: legacy-generation control files plus every retired
    // leftover a previous release could have stranded at the root.
    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
    defer fx.deinit(gpa);
    try devolveCheckoutToLegacy(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, ".zit-vstat", "stale sidecar");
    try writeFile(io, tmp.dir, ".zit-pack-tmp", "stale pack temp");
    try writeFile(io, tmp.dir, ".zit-import-map.jsonl", "{}");
    try tmp.dir.createDir(io, ".zit-update-txn-stale", .default_dir);
    try writeFile(io, tmp.dir, ".zit-update-txn-stale/f", "x");
    try tmp.dir.createDir(io, ".zit-import-tmp-stale", .default_dir);
    try writeFile(io, tmp.dir, ".zit-import-tmp-stale/f", "x");
    try tmp.dir.createDir(io, "refs", .default_dir);
    try writeFile(io, tmp.dir, "refs/heads-main", "deadbeef");

    // One mutation migrates the authoritative siblings into the container,
    // losslessly (the tracked file's dirty state survives via revert below).
    try writeFile(io, tmp.dir, "poem", "dirty\n");
    try okZit(gpa, io, tmp.dir, &.{ "revert", "poem" });
    try std.testing.expect(!fileExists(io, tmp.dir, ".zit-managed"));
    try expectManaged(gpa, io, tmp.dir, &.{"poem"}); // membership survived the fold

    // The retired leftovers lost their reservations: extras SEES them...
    var ex = try runZit(gpa, io, tmp.dir, &.{ "extras", "--dotfiles" });
    defer freeResult(gpa, &ex);
    try expectExit(ex.term, 0);
    for ([_][]const u8{ ".zit-vstat", ".zit-pack-tmp", ".zit-import-map.jsonl", ".zit-update-txn-stale/f", ".zit-import-tmp-stale/f", "refs/heads-main" }) |name| {
        try std.testing.expect(std.mem.indexOf(u8, ex.stdout, name) != null);
    }
    // ...and clean deletes them. Directory husks survive because clean
    // sweeps files, not directories — a general clean property, the same for
    // any emptied untracked directory — so they are allowed here while every
    // legacy FILE must be gone.
    try okZit(gpa, io, tmp.dir, &.{ "clean", "--force", "--dotfiles" });
    try expectConsolidatedRoot(io, tmp.dir, &.{ "poem", "refs", ".zit-update-txn-stale", ".zit-import-tmp-stale" });
    for ([_][]const u8{ ".zit-vstat", ".zit-pack-tmp", ".zit-import-map.jsonl", ".zit-update-txn-stale/f", ".zit-import-tmp-stale/f", "refs/heads-main" }) |name| {
        try std.testing.expect(!fileExists(io, tmp.dir, name));
    }
    try expectManaged(gpa, io, tmp.dir, &.{"poem"});
}

// ---------------------------------------------------------------------------
// zit checkpoint: the durable rebuild checkpoint's CLI surface, driven over
// real fixture repositories through the built binary.
// ---------------------------------------------------------------------------

test "cli checkpoint: help exits 0, usage errors exit 2, and show reports absence" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var help = try runZit(gpa, io, tmp.dir, &.{ "checkpoint", "--help" });
    defer freeResult(gpa, &help);
    try expectExit(help.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, help.stdout, "usage: zit checkpoint") != null);

    var bad = try runZit(gpa, io, tmp.dir, &.{ "checkpoint", "--bogus" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 2);
    var extra = try runZit(gpa, io, tmp.dir, &.{ "checkpoint", "verify", "deadbeef" });
    defer freeResult(gpa, &extra);
    try expectExit(extra.term, 2);

    var show = try runZit(gpa, io, tmp.dir, &.{"checkpoint"});
    defer freeResult(gpa, &show);
    try expectExit(show.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, show.stdout, "none") != null);
}

test "cli checkpoint: witness build on an emission-off repository leaves .zit byte-identical, then verify and drop close the loop" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var c1: std.process.RunResult = undefined;
    defer freeResult(gpa, &c1);
    _ = try commitFile(gpa, io, tmp.dir, "a.txt", "one\n", "first", &c1);
    var c2: std.process.RunResult = undefined;
    defer freeResult(gpa, &c2);
    _ = try commitFile(gpa, io, tmp.dir, "b.txt", "two\n", "second", &c2);

    const zit_before = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(zit_before);
    var build = try runZit(gpa, io, tmp.dir, &.{ "checkpoint", "build" });
    defer freeResult(gpa, &build);
    try expectExit(build.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, build.stdout, "checkpoint: built at") != null);
    const zit_after = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(zit_after);
    try std.testing.expectEqualSlices(u8, zit_before, zit_after); // zero artifacts

    var show = try runZit(gpa, io, tmp.dir, &.{ "checkpoint", "show" });
    defer freeResult(gpa, &show);
    try expectExit(show.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, show.stdout, "witness") != null);
    try std.testing.expect(std.mem.indexOf(u8, show.stdout, "validates against the live log") != null);

    var ver = try runZit(gpa, io, tmp.dir, &.{ "checkpoint", "verify" });
    defer freeResult(gpa, &ver);
    try expectExit(ver.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, ver.stdout, "verified ok") != null);

    var drop1 = try runZit(gpa, io, tmp.dir, &.{ "checkpoint", "drop" });
    defer freeResult(gpa, &drop1);
    try expectExit(drop1.term, 0);
    var drop2 = try runZit(gpa, io, tmp.dir, &.{ "checkpoint", "drop" });
    defer freeResult(gpa, &drop2);
    try expectExit(drop2.term, 0); // absence is success
}

test "cli checkpoint: reads answer identically across build, growth, corruption, and --no-cache" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var c1: std.process.RunResult = undefined;
    defer freeResult(gpa, &c1);
    _ = try commitFile(gpa, io, tmp.dir, "a.txt", "one\n", "first", &c1);
    var build = try runZit(gpa, io, tmp.dir, &.{ "checkpoint", "build" });
    defer freeResult(gpa, &build);
    try expectExit(build.term, 0);

    // Growth after the checkpoint: a later commit and its read.
    var c2: std.process.RunResult = undefined;
    defer freeResult(gpa, &c2);
    const tip = try commitFile(gpa, io, tmp.dir, "b.txt", "two\n", "second", &c2);

    var reference: ?[]u8 = null;
    defer if (reference) |s| gpa.free(s);
    // The same read, four ways: warm, cold-cache (checkpoint resumes),
    // corrupted sidecar (full derive), and --no-cache (paranoid) — one answer.
    for (0..4) |variant| {
        switch (variant) {
            0 => {},
            1 => tmp.dir.deleteFile(io, ".zit-cache/repo") catch {},
            2 => {
                tmp.dir.deleteFile(io, ".zit-cache/repo") catch {};
                try writeFile(io, tmp.dir, ".zit-cache/checkpoint", "garbage over the sidecar");
            },
            3 => {},
            else => unreachable,
        }
        const args: []const []const u8 = if (variant == 3)
            &.{ "leaves", "--no-cache" }
        else
            &.{"leaves"};
        var res = try runZit(gpa, io, tmp.dir, args);
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, tip[0..12]) != null);
        if (reference) |want| {
            try std.testing.expectEqualStrings(want, res.stdout);
        } else {
            reference = try gpa.dupe(u8, res.stdout);
        }
    }
}

test "cli checkpoint: build on an emission-enabled repository is still sidecar-only and verifies" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var commit_out: std.process.RunResult = undefined;
    defer freeResult(gpa, &commit_out);
    for (0..12) |i| {
        var name_buf: [32]u8 = undefined;
        const name = try std.fmt.bufPrint(&name_buf, "{c}{d}.txt", .{ @as(u8, 'a') + @as(u8, @intCast(i * 2)), i });
        try writeFile(io, tmp.dir, name, "content\n");
        try addManaged(gpa, io, tmp.dir, &.{name});
    }
    commit_out = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "wide" });
    try expectExit(commit_out.term, 0);

    var enable = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "enable", "--k", "1" });
    defer freeResult(gpa, &enable);
    try expectExit(enable.term, 0);

    const zit_before = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(zit_before);
    var build = try runZit(gpa, io, tmp.dir, &.{ "checkpoint", "build" });
    defer freeResult(gpa, &build);
    try expectExit(build.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, build.stdout, "checkpoint: built at") != null);
    // Sidecar only, even with emission enabled: the store never moves.
    const zit_after = try readFile(gpa, io, tmp.dir, ".zit");
    defer gpa.free(zit_after);
    try std.testing.expectEqualSlices(u8, zit_before, zit_after);

    var ver = try runZit(gpa, io, tmp.dir, &.{ "checkpoint", "verify" });
    defer freeResult(gpa, &ver);
    try expectExit(ver.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, ver.stdout, "verified ok") != null);
}

test "cli sparse cone P4: open --cone scopes materialization, tracking, extras, and add refusal" {
    // Goal: one sparse checkout exercises the section 4.1 surface end to end:
    // only the cone materializes; the managed set is in-cone; an uncovered
    // on-disk file is invisible to extras; an explicit add outside the cone
    // refuses naming the mechanism (never "no such file"/"corrupt"); status
    // stays clean. Method: build a two-directory repo, adopt its store into a
    // fresh dir, open --cone=src, then probe each verb.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "rootfile.txt", "root\n");
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "a\n");
    try writeFile(io, tmp_a.dir, "src/b.zig", "b\n");
    try writeFile(io, tmp_a.dir, "tools/t.sh", "t\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "rootfile.txt", "src/a.zig", "src/b.zig", "tools/t.sh" });
    const head = try commitHashDup(gpa, io, tmp_a.dir, "base");
    defer gpa.free(head);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    try initRepo(gpa, io, tmp_b.dir);
    try adoptStore(gpa, io, tmp_a.dir, tmp_b.dir);

    var opened = try runZit(gpa, io, tmp_b.dir, &.{ "open", "--cone=src" });
    defer freeResult(gpa, &opened);
    try expectExitDiag(opened.term, 0, opened);
    try expectCheckoutVersion(io, tmp_b.dir, head);

    // Positive space: the cone materialized. Negative space: nothing else did.
    try std.testing.expect(fileExists(io, tmp_b.dir, "src/a.zig"));
    try std.testing.expect(fileExists(io, tmp_b.dir, "src/b.zig"));
    try std.testing.expect(!fileExists(io, tmp_b.dir, "rootfile.txt"));
    try std.testing.expect(!fileExists(io, tmp_b.dir, "tools/t.sh"));

    // A sparse checkout is CLEAN: the out-of-cone entries live in the record,
    // not the managed set, and status compares cone against cone.
    var st = try runZit(gpa, io, tmp_b.dir, &.{"status"});
    defer freeResult(gpa, &st);
    try expectExitDiag(st.term, 0, st);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "removed") == null);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "missing") == null);

    // An uncovered on-disk file is NOT an extra (section 4.1).
    try writeFile(io, tmp_b.dir, "stray.txt", "outside\n");
    try writeFile(io, tmp_b.dir, "src/new.zig", "inside\n");
    var ex = try runZit(gpa, io, tmp_b.dir, &.{"extras"});
    defer freeResult(gpa, &ex);
    try expectExitDiag(ex.term, 0, ex);
    try std.testing.expectEqualStrings("src/new.zig\n", ex.stdout);

    // Explicit add outside the cone: exit 1, names the mechanism and both
    // remedies, whole-stderr asserted so wording changes come through here.
    var ad = try runZit(gpa, io, tmp_b.dir, &.{ "add", "stray.txt" });
    defer freeResult(gpa, &ad);
    try expectExit(ad.term, 1);
    try std.testing.expectEqualStrings(
        "zit add: 'stray.txt' is outside this checkout's sparse cone\n" ++
            "widen it with 'zit update --cone=stray.txt' or return to dense with 'zit update --dense'\n",
        ad.stderr,
    );
    try std.testing.expect(std.mem.indexOf(u8, ad.stderr, "no such file") == null);
    try std.testing.expect(std.mem.indexOf(u8, ad.stderr, "corrupt") == null);

    // In-cone add works; `add .` expands to the cone only.
    var ad2 = try runZit(gpa, io, tmp_b.dir, &.{ "add", "." });
    defer freeResult(gpa, &ad2);
    try expectExitDiag(ad2.term, 0, ad2);
    try std.testing.expect(std.mem.indexOf(u8, ad2.stdout, "src/new.zig") != null);
    try std.testing.expect(std.mem.indexOf(u8, ad2.stdout, "stray.txt") == null);
}

test "cli sparse cone P4: update --dense rematerializes the full tree and re-scopes extras" {
    // Goal: section 5.4's ultimate widening -- dropping the cone -- restores
    // every out-of-cone file and returns the verbs to dense behavior. Method:
    // sparse open as above, then `update --dense` at the same check-in.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "a\n");
    try writeFile(io, tmp_a.dir, "tools/t.sh", "t\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.sh" });
    const head = try commitHashDup(gpa, io, tmp_a.dir, "base");
    defer gpa.free(head);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    try initRepo(gpa, io, tmp_b.dir);
    try adoptStore(gpa, io, tmp_a.dir, tmp_b.dir);
    var opened = try runZit(gpa, io, tmp_b.dir, &.{ "open", "--cone=src" });
    defer freeResult(gpa, &opened);
    try expectExitDiag(opened.term, 0, opened);
    try std.testing.expect(!fileExists(io, tmp_b.dir, "tools/t.sh"));

    var up = try runZit(gpa, io, tmp_b.dir, &.{ "update", "--dense", head });
    defer freeResult(gpa, &up);
    try expectExitDiag(up.term, 0, up);
    try std.testing.expect(fileExists(io, tmp_b.dir, "tools/t.sh"));
    try std.testing.expect(fileExists(io, tmp_b.dir, "src/a.zig"));

    // Dense again: a stray file is an extra once more.
    try writeFile(io, tmp_b.dir, "stray.txt", "outside\n");
    var ex = try runZit(gpa, io, tmp_b.dir, &.{"extras"});
    defer freeResult(gpa, &ex);
    try expectExitDiag(ex.term, 0, ex);
    try std.testing.expectEqualStrings("stray.txt\n", ex.stdout);
}

test "cli sparse cone P5 keystone: a sparse commit is indistinguishable from the dense commit of the same change" {
    // Goal: RFC section 4.2's central claim. Two clones of one repo make the
    // SAME logical change -- edit src/a.zig, add src/new.zig -- one densely,
    // one through a src-only cone. The minted check-ins must agree on their
    // complete file sets (out-of-cone entries carried verbatim, R card and
    // all), the sparse one must reopen densely elsewhere with every file
    // intact, and verify must be clean. Method: byte-compare `zit ls` output
    // and per-file `zit cat` across the two results.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "a v1\n");
    try writeFile(io, tmp_a.dir, "tools/t.sh", "tool\n");
    try writeFile(io, tmp_a.dir, "README", "readme\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.sh", "README" });
    const base = try commitHashDup(gpa, io, tmp_a.dir, "base");
    defer gpa.free(base);

    // Dense arm: ordinary checkout, make the change, commit.
    var dense = std.testing.tmpDir(.{});
    defer dense.cleanup();
    try initRepo(gpa, io, dense.dir);
    try adoptStore(gpa, io, tmp_a.dir, dense.dir);
    var od = try runZit(gpa, io, dense.dir, &.{"open"});
    defer freeResult(gpa, &od);
    try expectExitDiag(od.term, 0, od);
    try writeFile(io, dense.dir, "src/a.zig", "a v2\n");
    try writeFile(io, dense.dir, "src/new.zig", "new\n");
    var ad = try runZit(gpa, io, dense.dir, &.{ "add", "src/new.zig" });
    defer freeResult(gpa, &ad);
    try expectExitDiag(ad.term, 0, ad);
    var cd = try runZit(gpa, io, dense.dir, &.{ "commit", "-m", "same change", "--user", "e2e" });
    defer freeResult(gpa, &cd);
    try expectExitDiag(cd.term, 0, cd);

    // Sparse arm: src-only cone, the same change, commit.
    var sparse = std.testing.tmpDir(.{});
    defer sparse.cleanup();
    try initRepo(gpa, io, sparse.dir);
    try adoptStore(gpa, io, tmp_a.dir, sparse.dir);
    var os_ = try runZit(gpa, io, sparse.dir, &.{ "open", "--cone=src" });
    defer freeResult(gpa, &os_);
    try expectExitDiag(os_.term, 0, os_);
    try std.testing.expect(!fileExists(io, sparse.dir, "tools/t.sh"));
    try writeFile(io, sparse.dir, "src/a.zig", "a v2\n");
    try writeFile(io, sparse.dir, "src/new.zig", "new\n");
    var as_ = try runZit(gpa, io, sparse.dir, &.{ "add", "src/new.zig" });
    defer freeResult(gpa, &as_);
    try expectExitDiag(as_.term, 0, as_);
    var cs = try runZit(gpa, io, sparse.dir, &.{ "commit", "-m", "same change", "--user", "e2e" });
    defer freeResult(gpa, &cs);
    try expectExitDiag(cs.term, 0, cs);

    // The two tips enumerate identical COMPLETE file sets: `ls` output must
    // be byte-equal, and it must still contain the out-of-cone files.
    var ls_dense = try runZit(gpa, io, dense.dir, &.{"ls"});
    defer freeResult(gpa, &ls_dense);
    try expectExitDiag(ls_dense.term, 0, ls_dense);
    var ls_sparse = try runZit(gpa, io, sparse.dir, &.{"ls"});
    defer freeResult(gpa, &ls_sparse);
    try expectExitDiag(ls_sparse.term, 0, ls_sparse);
    try std.testing.expectEqualStrings(ls_dense.stdout, ls_sparse.stdout);
    try std.testing.expect(std.mem.indexOf(u8, ls_sparse.stdout, "tools/t.sh") != null);
    try std.testing.expect(std.mem.indexOf(u8, ls_sparse.stdout, "README") != null);

    // Out-of-cone content carried verbatim: cat from the sparse store.
    var cat_tool = try runZit(gpa, io, sparse.dir, &.{ "cat", "tools/t.sh" });
    defer freeResult(gpa, &cat_tool);
    try expectExitDiag(cat_tool.term, 0, cat_tool);
    try std.testing.expectEqualStrings("tool\n", cat_tool.stdout);

    // The sparse store verifies clean, and a fresh DENSE open of it
    // materializes the full tree including the sparse-committed change.
    var ver = try runZit(gpa, io, sparse.dir, &.{"verify"});
    defer freeResult(gpa, &ver);
    try expectExitDiag(ver.term, 0, ver);

    var reader = std.testing.tmpDir(.{});
    defer reader.cleanup();
    try initRepo(gpa, io, reader.dir);
    try adoptStore(gpa, io, sparse.dir, reader.dir);
    var or_ = try runZit(gpa, io, reader.dir, &.{"open"});
    defer freeResult(gpa, &or_);
    try expectExitDiag(or_.term, 0, or_);
    const a2 = try readFile(gpa, io, reader.dir, "src/a.zig");
    defer gpa.free(a2);
    try std.testing.expectEqualStrings("a v2\n", a2);
    const t2 = try readFile(gpa, io, reader.dir, "tools/t.sh");
    defer gpa.free(t2);
    try std.testing.expectEqualStrings("tool\n", t2);
}

test "cli sparse cone P5: in-cone deletion commits as a deletion; out-of-cone files never deleted by absence" {
    // Goal: the sparse snapshot's negative space -- rm of an in-cone file is
    // a real deletion, while every out-of-cone file (absent from the sparse
    // worktree by definition) survives the commit untouched. This is the
    // data-loss trap the compose carry exists to prevent.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "a\n");
    try writeFile(io, tmp_a.dir, "src/b.zig", "b\n");
    try writeFile(io, tmp_a.dir, "tools/t.sh", "tool\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "src/b.zig", "tools/t.sh" });
    const base = try commitHashDup(gpa, io, tmp_a.dir, "base");
    defer gpa.free(base);

    var sp = std.testing.tmpDir(.{});
    defer sp.cleanup();
    try initRepo(gpa, io, sp.dir);
    try adoptStore(gpa, io, tmp_a.dir, sp.dir);
    var op = try runZit(gpa, io, sp.dir, &.{ "open", "--cone=src" });
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);

    var rm = try runZit(gpa, io, sp.dir, &.{ "rm", "src/b.zig" });
    defer freeResult(gpa, &rm);
    try expectExitDiag(rm.term, 0, rm);
    sp.dir.deleteFile(io, "src/b.zig") catch {};
    var cm = try runZit(gpa, io, sp.dir, &.{ "commit", "-m", "drop b", "--user", "e2e" });
    defer freeResult(gpa, &cm);
    try expectExitDiag(cm.term, 0, cm);

    var ls = try runZit(gpa, io, sp.dir, &.{"ls"});
    defer freeResult(gpa, &ls);
    try expectExitDiag(ls.term, 0, ls);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "src/b.zig") == null); // deleted
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "src/a.zig") != null); // kept
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "tools/t.sh") != null); // CARRIED
}

test "cli sparse cone P6: narrowing dematerializes; status and vstat stay correct, never wrong" {
    // Goal: RFC section 3.1 mutability + the cache-key audit (PLAN Task 6) --
    // a cone change on an existing checkout costs a flatten, never a wrong
    // answer. Narrowing from dense removes out-of-cone files from the tree
    // (the record keeps them); status is clean before and after; widening
    // back restores them.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "a\n");
    try writeFile(io, tmp_a.dir, "tools/t.sh", "tool\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.sh" });
    const head = try commitHashDup(gpa, io, tmp_a.dir, "base");
    defer gpa.free(head);

    var wt = std.testing.tmpDir(.{});
    defer wt.cleanup();
    try initRepo(gpa, io, wt.dir);
    try adoptStore(gpa, io, tmp_a.dir, wt.dir);
    var op = try runZit(gpa, io, wt.dir, &.{"open"});
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);
    try std.testing.expect(fileExists(io, wt.dir, "tools/t.sh"));

    // Narrow: dense -> src cone at the same check-in.
    var nr = try runZit(gpa, io, wt.dir, &.{ "update", "--cone=src", head });
    defer freeResult(gpa, &nr);
    try expectExitDiag(nr.term, 0, nr);
    try std.testing.expect(!fileExists(io, wt.dir, "tools/t.sh")); // dematerialized
    try std.testing.expect(fileExists(io, wt.dir, "src/a.zig"));

    var st = try runZit(gpa, io, wt.dir, &.{"status"});
    defer freeResult(gpa, &st);
    try expectExitDiag(st.term, 0, st);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "removed") == null);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "missing") == null);

    // Widen back to dense: everything returns, still clean.
    var wd = try runZit(gpa, io, wt.dir, &.{ "update", "--dense", head });
    defer freeResult(gpa, &wd);
    try expectExitDiag(wd.term, 0, wd);
    try std.testing.expect(fileExists(io, wt.dir, "tools/t.sh"));
    var st2 = try runZit(gpa, io, wt.dir, &.{"status"});
    defer freeResult(gpa, &st2);
    try expectExitDiag(st2.term, 0, st2);
    try std.testing.expect(std.mem.indexOf(u8, st2.stdout, "removed") == null);
}

test "cli sparse cone P6: the keystone holds on a submanifest-enabled repository" {
    // Goal: the tree-dialect half of reader-indistinguishability (RFC 4.2).
    // Same shape as the P5 keystone, but the source repo emits S-card tree
    // manifests, so the sparse commit's composed F-set drives the tree
    // construction and untouched out-of-cone subtrees reproduce their node
    // hashes by construction over the dense store. Method: enable
    // sub-manifests before the base commit, then dense-vs-sparse the same
    // change and byte-compare the enumerated file sets.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    var en = try runZit(gpa, io, tmp_a.dir, &.{ "submanifest", "enable", "--k", "3" });
    defer freeResult(gpa, &en);
    try expectExitDiag(en.term, 0, en);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "a v1\n");
    try writeFile(io, tmp_a.dir, "tools/t.sh", "tool\n");
    try writeFile(io, tmp_a.dir, "README", "readme\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.sh", "README" });
    const base = try commitHashDup(gpa, io, tmp_a.dir, "base");
    defer gpa.free(base);

    var dense = std.testing.tmpDir(.{});
    defer dense.cleanup();
    try initRepo(gpa, io, dense.dir);
    try adoptStore(gpa, io, tmp_a.dir, dense.dir);
    var od = try runZit(gpa, io, dense.dir, &.{"open"});
    defer freeResult(gpa, &od);
    try expectExitDiag(od.term, 0, od);
    try writeFile(io, dense.dir, "src/a.zig", "a v2\n");
    var cd = try runZit(gpa, io, dense.dir, &.{ "commit", "-m", "edit", "--user", "e2e" });
    defer freeResult(gpa, &cd);
    try expectExitDiag(cd.term, 0, cd);

    var sparse = std.testing.tmpDir(.{});
    defer sparse.cleanup();
    try initRepo(gpa, io, sparse.dir);
    try adoptStore(gpa, io, tmp_a.dir, sparse.dir);
    var os_ = try runZit(gpa, io, sparse.dir, &.{ "open", "--cone=src" });
    defer freeResult(gpa, &os_);
    try expectExitDiag(os_.term, 0, os_);
    try writeFile(io, sparse.dir, "src/a.zig", "a v2\n");
    var cs = try runZit(gpa, io, sparse.dir, &.{ "commit", "-m", "edit", "--user", "e2e" });
    defer freeResult(gpa, &cs);
    try expectExitDiag(cs.term, 0, cs);

    var ls_dense = try runZit(gpa, io, dense.dir, &.{"ls"});
    defer freeResult(gpa, &ls_dense);
    try expectExitDiag(ls_dense.term, 0, ls_dense);
    var ls_sparse = try runZit(gpa, io, sparse.dir, &.{"ls"});
    defer freeResult(gpa, &ls_sparse);
    try expectExitDiag(ls_sparse.term, 0, ls_sparse);
    try std.testing.expectEqualStrings(ls_dense.stdout, ls_sparse.stdout);
    try std.testing.expect(std.mem.indexOf(u8, ls_sparse.stdout, "tools/t.sh") != null);

    var ver = try runZit(gpa, io, sparse.dir, &.{"verify"});
    defer freeResult(gpa, &ver);
    try expectExitDiag(ver.term, 0, ver);
}

test "cli clone --cone: local filtering over the wire — elision counted, verify healthy, convergence sound" {
    // Sparse-cone RFC section 6.3 end to end against a cone-ignorant server:
    // the client filters locally, persists spine + in-cone content, reports
    // the elision count, verifies healthy, materializes the cone — and a
    // dense re-clone FROM THE SERVER still sees everything (the sparse clone
    // took nothing away from anyone).
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "in-cone content\n");
    try writeFile(io, tmp_a.dir, "tools/big.bin", "out-of-cone content that should elide\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/big.bin" });
    var commit = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &commit);
    try expectExit(commit.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", "--cone=src", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    // Wave 3: the server is cone-aware and filters its reply (RFC 6.2), so
    // the CLIENT has nothing left to elide -- the count reads zero while
    // verify still reports the store's one elided phantom below. Against a
    // cone-IGNORANT server this line reads "(1 elided by the cone)": the
    // client-side belt (RFC 6.3) is what both arms share.
    try std.testing.expect(std.mem.indexOf(u8, clone.stdout, "(0 elided by the cone)") != null);

    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    // The sparse store opens at the cone: in-cone file present, elided not.
    var open_res = try runZit(gpa, io, b_dir, &.{ "open", "--cone=src" });
    defer freeResult(gpa, &open_res);
    try expectExitDiag(open_res.term, 0, open_res);
    const a_content = try readFile(gpa, io, b_dir, "src/a.zig");
    defer gpa.free(a_content);
    try std.testing.expectEqualStrings("in-cone content\n", a_content);
    try std.testing.expect(!fileExists(io, b_dir, "tools/big.bin"));

    // The elided blob's content is genuinely absent: cat refuses rather than
    // invents (the store holds the manifest naming it, not its bytes).
    var cat_res = try runZit(gpa, io, b_dir, &.{ "cat", "tools/big.bin" });
    defer freeResult(gpa, &cat_res);
    try expectExit(cat_res.term, 1);

    // Convergence untouched: a DENSE clone from the same server still gets
    // everything — sparseness was purely local policy.
    var tmp_c = std.testing.tmpDir(.{});
    defer tmp_c.cleanup();
    var dense_clone = try runZit(gpa, io, tmp_c.dir, &.{ "clone", "--no-open", url, "repo" });
    defer freeResult(gpa, &dense_clone);
    try expectExitDiag(dense_clone.term, 0, dense_clone);
    var c_dir = try tmp_c.dir.openDir(io, "repo", .{});
    defer c_dir.close(io);
    var open_c = try runZit(gpa, io, c_dir, &.{"open"});
    defer freeResult(gpa, &open_c);
    try expectExitDiag(open_c.term, 0, open_c);
    const big = try readFile(gpa, io, c_dir, "tools/big.bin");
    defer gpa.free(big);
    try std.testing.expectEqualStrings("out-of-cone content that should elide\n", big);
}

test "cli pull on a sparse store: elided phantoms are never spontaneously re-requested" {
    // Sparse-cone RFC section 5.2 rule 3 over the real wire: after a sparse
    // clone, a pull against the same dense server must not re-download the
    // elided blob (the store's byte size is the witness), while a NEW commit
    // on the server -- touching the cone -- still arrives. Suppression
    // narrows requests, never freshness.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "one\n");
    try writeFile(io, tmp_a.dir, "tools/big.bin", "elidable payload\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/big.bin" });
    var c1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--cone=src", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    const size_after_clone = blk: {
        const st = try b_dir.statFile(io, ".zit", .{});
        break :blk st.size;
    };

    // An idle pull: nothing new on the server, and the elided phantom must
    // stay suppressed -- the store may not grow.
    var idle = try runZit(gpa, io, b_dir, &.{"pull"});
    defer freeResult(gpa, &idle);
    try expectExitDiag(idle.term, 0, idle);
    const size_after_idle = blk: {
        const st = try b_dir.statFile(io, ".zit", .{});
        break :blk st.size;
    };
    try std.testing.expectEqual(size_after_clone, size_after_idle);

    // Freshness intact: a new in-cone commit on the server arrives on pull.
    // (The server child holds its own snapshot; restart it to serve the new
    // commit.)
    server.kill(io);
    try writeFile(io, tmp_a.dir, "src/a.zig", "two\n");
    var c2 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "more" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    server = try spawnServe(gpa, io, tmp_a.dir, port);
    try waitForListen(io, port);

    var fresh = try runZit(gpa, io, b_dir, &.{"pull"});
    defer freeResult(gpa, &fresh);
    try expectExitDiag(fresh.term, 0, fresh);
    const size_after_fresh = blk: {
        const st = try b_dir.statFile(io, ".zit", .{});
        break :blk st.size;
    };
    try std.testing.expect(size_after_fresh > size_after_idle);
}

test "cli verify on a sparse store: elision in the summary, damage still damage" {
    // Sparse-cone RFC section 5.2: verify reports a complete-within-its-cone
    // store as healthy with the elision count in the SUMMARY (never as
    // findings), and real in-cone damage still reports exactly as on a dense
    // store -- elision must never mask corruption.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "in-cone\n");
    try writeFile(io, tmp_a.dir, "tools/t.bin", "out-of-cone\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.bin" });
    var c1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--cone=src", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    // Healthy: summary carries the sparse line with the exact count.
    var ver = try runZit(gpa, io, b_dir, &.{"verify"});
    defer freeResult(gpa, &ver);
    try expectExitDiag(ver.term, 0, ver);
    try std.testing.expect(std.mem.indexOf(u8, ver.stdout, "sparse store: cone of 1 member; 1 artifact deliberately elided") != null);
    try std.testing.expect(std.mem.indexOf(u8, ver.stdout, "0 faults") != null);
    try std.testing.expect(std.mem.indexOf(u8, ver.stderr, "missing") == null);

    // Negative space: flip one byte mid-store; verify must still find real
    // damage -- the sparse summary never absorbs a genuine fault.
    {
        var f = try b_dir.openFile(io, ".zit", .{ .mode = .read_write });
        defer f.close(io);
        const st = try f.stat(io);
        const mid = st.size / 2;
        var one: [1]u8 = undefined;
        _ = try f.readPositionalAll(io, &one, mid);
        one[0] ^= 0x01;
        try f.writePositionalAll(io, &one, mid);
    }
    var damaged = try runZit(gpa, io, b_dir, &.{"verify"});
    defer freeResult(gpa, &damaged);
    try expectExit(damaged.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, damaged.stderr, "fault") != null);
}

test "cli commit on a sparse store: succeeds, omits R, and converges back to the dense server" {
    // Sparse-cone RFC sections 4.3 and 5.3: an in-cone commit on a sparse
    // STORE cannot compute the whole-tree R aggregate (elided content), MUST
    // omit R rather than fail or fabricate, and the resulting ordinary
    // check-in pushes back to a dense peer, where a dense clone materializes
    // the full tree including the sparse-committed change.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "one\n");
    try writeFile(io, tmp_a.dir, "tools/t.bin", "payload\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.bin" });
    var c1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", "--cone=src", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    var op = try runZit(gpa, io, b_dir, &.{ "open", "--cone=src" });
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);

    // The sparse commit: edit in-cone, commit. Must succeed with R omitted.
    try writeFile(io, b_dir, "src/a.zig", "two\n");
    const sparse_tip = try commitHashDup(gpa, io, b_dir, "sparse edit");
    defer gpa.free(sparse_tip);

    // The minted manifest carries no R card but still carries the out-of-cone
    // F card, hash intact.
    var info = try runZit(gpa, io, b_dir, &.{ "artifact", sparse_tip });
    defer freeResult(gpa, &info);
    try expectExitDiag(info.term, 0, info);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "\nR ") == null);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "tools/t.bin") != null);

    // Push back; a dense clone from the server sees the sparse edit AND the
    // untouched out-of-cone file.
    var push = try runZit(gpa, io, b_dir, &.{ "push", url });
    defer freeResult(gpa, &push);
    try expectExitDiag(push.term, 0, push);

    var tmp_c = std.testing.tmpDir(.{});
    defer tmp_c.cleanup();
    var dense_clone = try runZit(gpa, io, tmp_c.dir, &.{ "clone", "--no-open", url, "repo" });
    defer freeResult(gpa, &dense_clone);
    try expectExitDiag(dense_clone.term, 0, dense_clone);
    var c_dir = try tmp_c.dir.openDir(io, "repo", .{});
    defer c_dir.close(io);
    var oc = try runZit(gpa, io, c_dir, &.{"open"});
    defer freeResult(gpa, &oc);
    try expectExitDiag(oc.term, 0, oc);
    const edited = try readFile(gpa, io, c_dir, "src/a.zig");
    defer gpa.free(edited);
    try std.testing.expectEqualStrings("two\n", edited);
    const carried = try readFile(gpa, io, c_dir, "tools/t.bin");
    defer gpa.free(carried);
    try std.testing.expectEqualStrings("payload\n", carried);
}

test "cli open dense on a sparse store: demand fetch via the saved remote, refusal without one" {
    // Sparse-cone RFC section 6.4, policy form: a dense open on a sparse
    // store expresses new coverage intent -- the store cone drops, an
    // ordinary pull fetches what the new policy wants from the saved remote
    // (clone saved it), and the open proceeds to a full tree. With no saved
    // remote the operation refuses naming the elision class.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "in\n");
    try writeFile(io, tmp_a.dir, "tools/t.bin", "out\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.bin" });
    var c1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    // Arm 1: saved remote present (clone saves it) -- dense open fetches.
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", "--cone=src", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    var od = try runZit(gpa, io, b_dir, &.{"open"});
    defer freeResult(gpa, &od);
    try expectExitDiag(od.term, 0, od);
    try std.testing.expect(std.mem.indexOf(u8, od.stderr, "fetching from the saved remote") != null);
    const fetched = try readFile(gpa, io, b_dir, "tools/t.bin");
    defer gpa.free(fetched);
    try std.testing.expectEqualStrings("out\n", fetched);

    // Arm 2: no saved remote -- the elided-class refusal, naming remedies.
    var tmp_c = std.testing.tmpDir(.{});
    defer tmp_c.cleanup();
    var clone2 = try runZit(gpa, io, tmp_c.dir, &.{ "clone", "--no-open", "--cone=src", "--once", url, "repo" });
    defer freeResult(gpa, &clone2);
    try expectExitDiag(clone2.term, 0, clone2);
    var c_dir = try tmp_c.dir.openDir(io, "repo", .{});
    defer c_dir.close(io);
    var oc = try runZit(gpa, io, c_dir, &.{"open"});
    defer freeResult(gpa, &oc);
    try expectExit(oc.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, oc.stderr, "elided by the store's sparse cone") != null);
    try std.testing.expect(std.mem.indexOf(u8, oc.stderr, "no saved remote") != null);
    try std.testing.expect(std.mem.indexOf(u8, oc.stderr, "corrupt") == null);
}

test "cli update --cone widens a sparse store through the saved remote" {
    // Sparse-cone RFC section 6.4, update's half: widening the cone past the
    // store's holdings rewrites store policy and pulls the newly covered
    // content; --dense densifies the same way. The narrow request needs no
    // fetch at all.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try tmp_a.dir.createDir(io, "docs", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "src\n");
    try writeFile(io, tmp_a.dir, "tools/t.bin", "tools\n");
    try writeFile(io, tmp_a.dir, "docs/d.md", "docs\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.bin", "docs/d.md" });
    const head = try commitHashDup(gpa, io, tmp_a.dir, "seed");
    defer gpa.free(head);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", "--cone=src", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    var op = try runZit(gpa, io, b_dir, &.{ "open", "--cone=src" });
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);
    try std.testing.expect(!fileExists(io, b_dir, "tools/t.bin"));

    // Widen: src -> src+tools. The store lacks tools' blob; the fetch fires.
    var widen = try runZit(gpa, io, b_dir, &.{ "update", "--cone=src", "--cone=tools", head });
    defer freeResult(gpa, &widen);
    try expectExitDiag(widen.term, 0, widen);
    try std.testing.expect(std.mem.indexOf(u8, widen.stderr, "fetching from the saved remote") != null);
    const tools_content = try readFile(gpa, io, b_dir, "tools/t.bin");
    defer gpa.free(tools_content);
    try std.testing.expectEqualStrings("tools\n", tools_content);
    try std.testing.expect(!fileExists(io, b_dir, "docs/d.md")); // still outside the cone

    // Densify: --dense fetches the remainder.
    var dense = try runZit(gpa, io, b_dir, &.{ "update", "--dense", head });
    defer freeResult(gpa, &dense);
    try expectExitDiag(dense.term, 0, dense);
    const docs_content = try readFile(gpa, io, b_dir, "docs/d.md");
    defer gpa.free(docs_content);
    try std.testing.expectEqualStrings("docs\n", docs_content);
}

test "cli users: a fossil user export imports, folds newest-mtime-wins, replaces on import, and survives pack" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    const t_ = std.testing;

    // A `fossil configuration export user` file, byte for byte as fossil
    // writes it: a `#` banner, then one card per row. `photo` is a BLOB, so
    // it arrives as SQLite's bare X'..' literal — the token shape that is not
    // quoted and that the parser must scan to whitespace rather than reject.
    try writeFile(io, tmp.dir, "users.txt",
        \\# The "user" configuration exported from
        \\# repository "tk.fossil"
        \\config /user 61
        \\1700000000 'alice' pw 'deadbeef' cap 's' info 'Admin' jx '{}'
        \\config /user 76
        \\1700000000 'bob' pw '' cap 'ei' info 'Dev' photo X'89504e470d0a1a0a' jx '{}'
        \\config /user 45
        \\1700000000 'nobody' pw '' cap 'gjorz' info ''
        \\
    );
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "config", "import", "users.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try t_.expect(std.mem.indexOf(u8, r.stdout, "3 rows imported") != null);
    }
    {
        var u = try runZit(gpa, io, tmp.dir, &.{"users"});
        defer freeResult(gpa, &u);
        try expectExitDiag(u.term, 0, u);
        // Sorted by login; caps verbatim; the hash withheld by default.
        try t_.expectEqualStrings(
            "alice\ts\t1700000000\tAdmin\nbob\tei\t1700000000\tDev\nnobody\tgjorz\t1700000000\t\n",
            u.stdout,
        );
        try t_.expect(std.mem.indexOf(u8, u.stdout, "deadbeef") == null);
    }
    {
        var u = try runZit(gpa, io, tmp.dir, &.{ "users", "--raw" });
        defer freeResult(gpa, &u);
        try expectExitDiag(u.term, 0, u);
        try t_.expect(std.mem.indexOf(u8, u.stdout, "deadbeef") != null);
    }

    // MERGE folds newest-mtime-wins: alice's caps change under a newer mtime
    // and win; a STALE row for bob is ignored rather than applied.
    try writeFile(io, tmp.dir, "newer.txt",
        \\config /user 46
        \\1800000000 'alice' pw '' cap 'as' info 'Admin'
        \\config /user 43
        \\1600000000 'bob' pw '' cap 'zzz' info 'Old'
        \\
    );
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "config", "merge", "newer.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        var u = try runZit(gpa, io, tmp.dir, &.{"users"});
        defer freeResult(gpa, &u);
        try t_.expect(std.mem.indexOf(u8, u.stdout, "alice\tas\t1800000000") != null);
        try t_.expect(std.mem.indexOf(u8, u.stdout, "bob\tei\t") != null); // the stale row lost
        try t_.expectEqual(@as(usize, 3), std.mem.count(u8, u.stdout, "\n"));
    }

    // IMPORT replaces: the table becomes exactly the file's rows, so a login
    // the file omits is gone — fossil's own CONFIGSET_OVERWRITE.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "config", "import", "newer.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        var u = try runZit(gpa, io, tmp.dir, &.{"users"});
        defer freeResult(gpa, &u);
        try t_.expectEqual(@as(usize, 2), std.mem.count(u8, u.stdout, "\n"));
        try t_.expect(std.mem.indexOf(u8, u.stdout, "nobody") == null);
    }

    // The table survives a pack, which folds the superseded rows away, and
    // the store still verifies.
    {
        var p = try runZit(gpa, io, tmp.dir, &.{"pack"});
        defer freeResult(gpa, &p);
        try expectExitDiag(p.term, 0, p);
        var u = try runZit(gpa, io, tmp.dir, &.{"users"});
        defer freeResult(gpa, &u);
        try t_.expectEqual(@as(usize, 2), std.mem.count(u8, u.stdout, "\n"));
        try t_.expect(std.mem.indexOf(u8, u.stdout, "alice\tas\t1800000000") != null);
        var v = try runZit(gpa, io, tmp.dir, &.{"verify"});
        defer freeResult(gpa, &v);
        try expectExitDiag(v.term, 0, v);
    }
}

test "cli config export: a table backed up, replaced, and restored comes back as it was, in fossil's own card format" {
    // import replaces a configuration area; without its counterpart there was
    // no way to keep what it replaced. The round trip is the whole contract:
    // export, import something else, import the export, and the table is back.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    const t_ = std.testing;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "users.txt",
        \\config /user 53
        \\1700000000 'alice' pw 'deadbeef' cap 's' info 'Admin'
        \\config /user 42
        \\1700000000 'bob' pw '' cap 'ei' info 'Dev'
        \\config /user 45
        \\1700000000 'nobody' pw '' cap 'gjorz' info ''
        \\
    );
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "config", "import", "users.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }
    var before = try runZit(gpa, io, tmp.dir, &.{"users"});
    defer freeResult(gpa, &before);
    try expectExitDiag(before.term, 0, before);

    // Byte for byte what fossil writes for the same rows: one card per row,
    // `config /user SIZE`, the payload, a newline. A banner line leads, as
    // fossil's does, and every reader skips it.
    var exported = try runZit(gpa, io, tmp.dir, &.{ "config", "export", "user" });
    defer freeResult(gpa, &exported);
    try expectExitDiag(exported.term, 0, exported);
    try t_.expect(std.mem.startsWith(u8, exported.stdout, "# The \"user\" configuration exported from\n"));
    try t_.expect(std.mem.indexOf(u8, exported.stdout, "config /user 45\n1700000000 'nobody' pw '' cap 'gjorz' info ''\n") != null);
    try t_.expect(std.mem.indexOf(u8, exported.stdout, "config /user 53\n1700000000 'alice' pw 'deadbeef' cap 's' info 'Admin'\n") != null);
    try t_.expectEqual(@as(usize, 3), std.mem.count(u8, exported.stdout, "config /user "));

    // To a file: the same bytes, and a receipt that names where they went.
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "config", "export", "user", "backup.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try t_.expectEqualStrings("user: 3 rows exported to backup.txt\n", r.stdout);
        const on_disk = try readFile(gpa, io, tmp.dir, "backup.txt");
        defer gpa.free(on_disk);
        try t_.expectEqualStrings(exported.stdout, on_disk);
    }

    // Replace the table with something else entirely, then restore.
    try writeFile(io, tmp.dir, "other.txt",
        \\config /user 43
        \\1900000000 'carol' pw '' cap 'v' info 'New'
        \\
    );
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "config", "import", "other.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        var u = try runZit(gpa, io, tmp.dir, &.{"users"});
        defer freeResult(gpa, &u);
        try t_.expectEqualStrings("carol\tv\t1900000000\tNew\n", u.stdout);
    }
    {
        var r = try runZit(gpa, io, tmp.dir, &.{ "config", "import", "backup.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        var u = try runZit(gpa, io, tmp.dir, &.{"users"});
        defer freeResult(gpa, &u);
        try t_.expectEqualStrings(before.stdout, u.stdout);
    }
}

test "cli config export: shun rows travel by file too, and a file never un-shuns anything" {
    // The shun table is a takedown record. Export carries it; import and merge
    // both ADD its rows. Replacing it would un-shun whatever a file did not
    // name, which zit never does, so an import of a file without the row
    // leaves the row where it is.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    const t_ = std.testing;
    const hash = "a" ** 64;
    const reason = "court order";

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "shun.txt", "config /shun 96\n1700000000 '" ++ hash ++ "' scom '" ++ reason ++ "'\n");
    {
        var r = try runZit(gpa, io, tmp_a.dir, &.{ "config", "merge", "shun.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try t_.expect(std.mem.indexOf(u8, r.stdout, "shun: 1 row added") != null);
    }
    var exported = try runZit(gpa, io, tmp_a.dir, &.{ "config", "export", "shun" });
    defer freeResult(gpa, &exported);
    try expectExitDiag(exported.term, 0, exported);
    try t_.expect(std.mem.indexOf(u8, exported.stdout, "config /shun 96\n1700000000 '" ++ hash ++ "' scom '" ++ reason ++ "'\n") != null);

    // Into a fresh repository by file alone.
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    try initRepo(gpa, io, tmp_b.dir);
    try writeFile(io, tmp_b.dir, "backup.txt", exported.stdout);
    {
        var r = try runZit(gpa, io, tmp_b.dir, &.{ "config", "import", "backup.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        var list = try runZit(gpa, io, tmp_b.dir, &.{ "shun", "--list" });
        defer freeResult(gpa, &list);
        try expectExitDiag(list.term, 0, list);
        try t_.expect(std.mem.indexOf(u8, list.stdout, hash) != null);
        try t_.expect(std.mem.indexOf(u8, list.stdout, reason) != null);
    }

    // `import` of a file that names no shun row replaces the user table and
    // leaves the shun table alone.
    try writeFile(io, tmp_b.dir, "users.txt", "config /user 43\n1900000000 'carol' pw '' cap 'v' info 'New'\n");
    {
        var r = try runZit(gpa, io, tmp_b.dir, &.{ "config", "import", "users.txt" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        var list = try runZit(gpa, io, tmp_b.dir, &.{ "shun", "--list" });
        defer freeResult(gpa, &list);
        try t_.expect(std.mem.indexOf(u8, list.stdout, hash) != null);
    }
}

test "cli pack: a repeat pack over an unchanged log is a no-op, and an append re-arms it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "alpha " ** 200 ++ "\n");
    try writeFile(io, tmp.dir, "b.txt", "alpha " ** 199 ++ "beta\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "t" }, &.{ "commit", "-m", "r" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    const t_ = std.testing;

    {
        var p = try runZit(gpa, io, tmp.dir, &.{"pack"});
        defer freeResult(gpa, &p);
        try expectExitDiag(p.term, 0, p);
        // A tiny store may pack to identical size, which renderPackStats
        // reports as "already packed: N artifacts walked ..." — still a REAL
        // walk. The stats line is the discriminator; the no-op gate prints
        // no stats at all.
        try t_.expect(std.mem.indexOf(u8, p.stdout, "artifacts walked") != null);
    }
    // Unchanged log: the recorded generation key matches, nothing is walked.
    {
        var p = try runZit(gpa, io, tmp.dir, &.{"pack"});
        defer freeResult(gpa, &p);
        try expectExitDiag(p.term, 0, p);
        try t_.expect(std.mem.indexOf(u8, p.stdout, "already packed; nothing to do") != null);
        try t_.expect(std.mem.indexOf(u8, p.stdout, "artifacts walked") == null);
    }
    // Any append (a commit here; a shun row or a pull equally) changes the
    // key, so the next pack runs for real — and re-records.
    try writeFile(io, tmp.dir, "a.txt", "alpha " ** 200 ++ "more\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "t" }, &.{ "commit", "-m", "r2" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    {
        var p = try runZit(gpa, io, tmp.dir, &.{"pack"});
        defer freeResult(gpa, &p);
        try expectExitDiag(p.term, 0, p);
        try t_.expect(std.mem.indexOf(u8, p.stdout, "artifacts walked") != null);
    }
    // The ladder rule for the memo: deleting the advisory tier costs a real
    // pack, never an answer — and the repository still reads identically.
    try tmp.dir.deleteTree(io, ".zit-cache");
    {
        var p = try runZit(gpa, io, tmp.dir, &.{"pack"});
        defer freeResult(gpa, &p);
        try expectExitDiag(p.term, 0, p);
        try t_.expect(std.mem.indexOf(u8, p.stdout, "artifacts walked") != null);
    }
    var ls = try runZit(gpa, io, tmp.dir, &.{ "ls", "trunk" });
    defer freeResult(gpa, &ls);
    try expectExitDiag(ls.term, 0, ls);
    try t_.expect(std.mem.indexOf(u8, ls.stdout, "a.txt") != null);
    try t_.expect(std.mem.indexOf(u8, ls.stdout, "b.txt") != null);
}

test "cli pack --drop-elided: narrow-then-compact shrinks the store and harms nothing kept" {
    // Sparse-cone RFC section 5.4, the spec's only deletion. A sparse clone
    // (store cone src+tools) narrows to src via update --cone -- store policy
    // follows the worktree BECAUSE the store is already sparse -- then an
    // explicit pack --drop-elided reclaims tools' blob. The store shrinks,
    // verify stays healthy, in-cone content is untouched, and on a DENSE
    // store the flag refuses rather than guesses.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "keep me\n");
    try writeFile(io, tmp_a.dir, "tools/big.bin", "x" ** 4096 ++ "\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/big.bin" });
    const head = try commitHashDup(gpa, io, tmp_a.dir, "seed");
    defer gpa.free(head);

    // Refusal on the dense origin: nothing is "elided" without a store cone.
    var refuse = try runZit(gpa, io, tmp_a.dir, &.{ "pack", "--drop-elided" });
    defer freeResult(gpa, &refuse);
    try expectExit(refuse.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, refuse.stderr, "needs a store cone") != null);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", "--cone=src", "--cone=tools", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    var op = try runZit(gpa, io, b_dir, &.{ "open", "--cone=src", "--cone=tools" });
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);

    // Narrow to src: store policy follows (the store is sparse); tools'
    // blob is now elided-by-policy though still physically held.
    var nr = try runZit(gpa, io, b_dir, &.{ "update", "--cone=src", head });
    defer freeResult(gpa, &nr);
    try expectExitDiag(nr.term, 0, nr);
    try std.testing.expect(!fileExists(io, b_dir, "tools/big.bin"));
    const size_before = blk: {
        const st = try b_dir.statFile(io, ".zit", .{});
        break :blk st.size;
    };

    var compact = try runZit(gpa, io, b_dir, &.{ "pack", "--drop-elided" });
    defer freeResult(gpa, &compact);
    try expectExitDiag(compact.term, 0, compact);
    const size_after = blk: {
        const st = try b_dir.statFile(io, ".zit", .{});
        break :blk st.size;
    };
    try std.testing.expect(size_after < size_before);

    var cat_keep = try runZit(gpa, io, b_dir, &.{ "cat", "src/a.zig" });
    defer freeResult(gpa, &cat_keep);
    try expectExitDiag(cat_keep.term, 0, cat_keep);
    try std.testing.expectEqualStrings("keep me\n", cat_keep.stdout);
    var ver = try runZit(gpa, io, b_dir, &.{"verify"});
    defer freeResult(gpa, &ver);
    try expectExitDiag(ver.term, 0, ver);
    try std.testing.expect(std.mem.indexOf(u8, ver.stdout, "sparse store") != null);
}

test "cli cone: show, add fetches through the saved remote, remove narrows" {
    // Wave 5 sugar over the section 6.4 machinery: `zit cone` lists members,
    // `cone add` unions + fetches newly covered content, `cone remove`
    // narrows and dematerializes. One code path with update --cone.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "src\n");
    try writeFile(io, tmp_a.dir, "tools/t.bin", "tools\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.bin" });
    var c1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", "--cone=src", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    var op = try runZit(gpa, io, b_dir, &.{ "open", "--cone=src" });
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);

    var show1 = try runZit(gpa, io, b_dir, &.{"cone"});
    defer freeResult(gpa, &show1);
    try expectExitDiag(show1.term, 0, show1);
    try std.testing.expectEqualStrings("src\n", show1.stdout);

    var add = try runZit(gpa, io, b_dir, &.{ "cone", "add", "tools" });
    defer freeResult(gpa, &add);
    try expectExitDiag(add.term, 0, add);
    const tools_content = try readFile(gpa, io, b_dir, "tools/t.bin");
    defer gpa.free(tools_content);
    try std.testing.expectEqualStrings("tools\n", tools_content);
    var show2 = try runZit(gpa, io, b_dir, &.{"cone"});
    defer freeResult(gpa, &show2);
    try expectExitDiag(show2.term, 0, show2);
    try std.testing.expectEqualStrings("src\ntools\n", show2.stdout);

    var rm = try runZit(gpa, io, b_dir, &.{ "cone", "remove", "tools" });
    defer freeResult(gpa, &rm);
    try expectExitDiag(rm.term, 0, rm);
    try std.testing.expect(!fileExists(io, b_dir, "tools/t.bin"));
    var show3 = try runZit(gpa, io, b_dir, &.{"cone"});
    defer freeResult(gpa, &show3);
    try expectExitDiag(show3.term, 0, show3);
    try std.testing.expectEqualStrings("src\n", show3.stdout);
}

test "cli merge across the cone: theirs-only changes apply to the record; both-changed refuses" {
    // Sparse-cone RFC section 4.4, record-level application. A dense
    // contributor edits tools/ on a side branch; a src-coned checkout merges
    // it: the out-of-cone change resolves at hash level into the merge
    // overlay, the commit composes it, and a dense reader sees theirs'
    // content -- nothing materialized out-of-cone, nothing lost. When BOTH
    // sides changed the same out-of-cone file, the merge refuses naming it.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "src v1\n");
    try writeFile(io, tmp_a.dir, "tools/t.bin", "tool v1\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.bin" });
    const base = try commitHashDup(gpa, io, tmp_a.dir, "base");
    defer gpa.free(base);

    // The side branch: a dense edit to tools/t.bin.
    try writeFile(io, tmp_a.dir, "tools/t.bin", "tool v2 from the side\n");
    const side = try commitHashDup(gpa, io, tmp_a.dir, "side edit");
    defer gpa.free(side);
    // Rewind the dense tree to base so trunk and side diverge.
    var back = try runZit(gpa, io, tmp_a.dir, &.{ "update", base });
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);
    try writeFile(io, tmp_a.dir, "src/a.zig", "src v2 on trunk\n");
    // Diverging from base while `side` holds the leaf: a deliberate fork,
    // so the fork guard asks for the flag.
    var trunk_commit = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "trunk edit", "--allow-fork" });
    defer freeResult(gpa, &trunk_commit);
    try expectExitDiag(trunk_commit.term, 0, trunk_commit);
    const trunk_tip = try gpa.dupe(u8, try checkinHash(trunk_commit.stdout));
    defer gpa.free(trunk_tip);

    // Sparse participant: cone=src, positioned at trunk_tip.
    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", "--cone=src", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    var op = try runZit(gpa, io, b_dir, &.{ "open", "--cone=src", trunk_tip });
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);

    // The record-level merge: theirs' tools edit lands in the overlay.
    var mg = try runZit(gpa, io, b_dir, &.{ "merge", side });
    defer freeResult(gpa, &mg);
    try expectExitDiag(mg.term, 0, mg);
    try std.testing.expect(std.mem.indexOf(u8, mg.stdout, "1 out-of-cone change(s) applied to the record") != null);
    try std.testing.expect(!fileExists(io, b_dir, "tools/t.bin")); // never materialized

    var merge_commit = try runCommitEnv(gpa, io, b_dir, .{ .user = "u" }, &.{ "commit", "-m", "merge side" });
    defer freeResult(gpa, &merge_commit);
    try expectExitDiag(merge_commit.term, 0, merge_commit);
    const merged = try gpa.dupe(u8, try checkinHash(merge_commit.stdout));
    defer gpa.free(merged);
    var push = try runZit(gpa, io, b_dir, &.{"push"});
    defer freeResult(gpa, &push);
    try expectExitDiag(push.term, 0, push);

    // A dense reader of the merged check-in sees theirs' tools content.
    var reader = std.testing.tmpDir(.{});
    defer reader.cleanup();
    var rc = try runZit(gpa, io, reader.dir, &.{ "clone", "--no-open", url, "repo" });
    defer freeResult(gpa, &rc);
    try expectExitDiag(rc.term, 0, rc);
    var r_dir = try reader.dir.openDir(io, "repo", .{});
    defer r_dir.close(io);
    var ro = try runZit(gpa, io, r_dir, &.{ "open", merged });
    defer freeResult(gpa, &ro);
    try expectExitDiag(ro.term, 0, ro);
    const tool_bytes = try readFile(gpa, io, r_dir, "tools/t.bin");
    defer gpa.free(tool_bytes);
    try std.testing.expectEqualStrings("tool v2 from the side\n", tool_bytes);
    const src_bytes = try readFile(gpa, io, r_dir, "src/a.zig");
    defer gpa.free(src_bytes);
    try std.testing.expectEqualStrings("src v2 on trunk\n", src_bytes);

    // Both-changed arm: trunk ALSO edits tools/t.bin; a fresh sparse
    // checkout at the new trunk tip merging `side` must refuse by name.
    server.kill(io);
    var fwd = try runZit(gpa, io, tmp_a.dir, &.{ "update", trunk_tip });
    defer freeResult(gpa, &fwd);
    try expectExitDiag(fwd.term, 0, fwd);
    try writeFile(io, tmp_a.dir, "tools/t.bin", "tool v2 on trunk too\n");
    // The pushed merge holds trunk's leaf now; this divergence is the
    // point of the arm, so fork deliberately.
    var tool_commit = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "trunk tool edit", "--allow-fork" });
    defer freeResult(gpa, &tool_commit);
    try expectExitDiag(tool_commit.term, 0, tool_commit);
    const trunk_tool = try gpa.dupe(u8, try checkinHash(tool_commit.stdout));
    defer gpa.free(trunk_tool);
    server = try spawnServe(gpa, io, tmp_a.dir, port);
    try waitForListen(io, port);

    var tmp_c = std.testing.tmpDir(.{});
    defer tmp_c.cleanup();
    var clone2 = try runZit(gpa, io, tmp_c.dir, &.{ "clone", "--no-open", "--cone=src", url, "repo" });
    defer freeResult(gpa, &clone2);
    try expectExitDiag(clone2.term, 0, clone2);
    var c_dir = try tmp_c.dir.openDir(io, "repo", .{});
    defer c_dir.close(io);
    var oc = try runZit(gpa, io, c_dir, &.{ "open", "--cone=src", trunk_tool });
    defer freeResult(gpa, &oc);
    try expectExitDiag(oc.term, 0, oc);
    var mg2 = try runZit(gpa, io, c_dir, &.{ "merge", side });
    defer freeResult(gpa, &mg2);
    try expectExit(mg2.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, mg2.stderr, "changed on both sides, starting with 'tools/t.bin'") != null);
    try std.testing.expect(std.mem.indexOf(u8, mg2.stderr, "widen the cone") != null);
}

test "cli sparse cone exclusions: open --cone-not carves a subtree out, re-inclusion survives" {
    // Goal: section 3.2 exclusions end to end over the local surface: an
    // excluded subtree neither materializes nor tracks; a deeper
    // re-inclusion under the exclusion does; `zit cone` shows the exclusion
    // in the cone-file grammar; a dead exclusion refuses with the rule.
    // Method: adopt a store, open with --cone/--cone-not, probe the tree.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "src/vendor", .default_dir);
    try tmp_a.dir.createDir(io, "src/vendor/keep", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "a\n");
    try writeFile(io, tmp_a.dir, "src/vendor/big.bin", "vendored\n");
    try writeFile(io, tmp_a.dir, "src/vendor/keep/gem.txt", "kept\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "src/vendor/big.bin", "src/vendor/keep/gem.txt" });
    const head = try commitHashDup(gpa, io, tmp_a.dir, "base");
    defer gpa.free(head);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    try initRepo(gpa, io, tmp_b.dir);
    try adoptStore(gpa, io, tmp_a.dir, tmp_b.dir);

    var opened = try runZit(gpa, io, tmp_b.dir, &.{
        "open", "--cone=src", "--cone-not=src/vendor", "--cone=src/vendor/keep",
    });
    defer freeResult(gpa, &opened);
    try expectExitDiag(opened.term, 0, opened);
    try expectCheckoutVersion(io, tmp_b.dir, head);

    // Positive space: the inclusion and the re-inclusion. Negative space:
    // the excluded interior.
    try std.testing.expect(fileExists(io, tmp_b.dir, "src/a.zig"));
    try std.testing.expect(fileExists(io, tmp_b.dir, "src/vendor/keep/gem.txt"));
    try std.testing.expect(!fileExists(io, tmp_b.dir, "src/vendor/big.bin"));

    // The cone verb prints exclusions in the cone-file grammar.
    var show = try runZit(gpa, io, tmp_b.dir, &.{"cone"});
    defer freeResult(gpa, &show);
    try expectExitDiag(show.term, 0, show);
    try std.testing.expectEqualStrings("src\nsrc/vendor/keep\n!src/vendor\n", show.stdout);

    // The excluded file is not an extra when it appears on disk, and a
    // commit round-trip keeps it in the record (reader indistinguishability
    // extends to exclusion boundaries).
    try writeFile(io, tmp_b.dir, "src/b.zig", "b\n");
    var ad = try runZit(gpa, io, tmp_b.dir, &.{ "add", "src/b.zig" });
    defer freeResult(gpa, &ad);
    try expectExitDiag(ad.term, 0, ad);
    var c2 = try runCommitEnv(gpa, io, tmp_b.dir, .{ .user = "u" }, &.{ "commit", "-m", "sparse add" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    var ls = try runZit(gpa, io, tmp_b.dir, &.{ "ls", "tip" });
    defer freeResult(gpa, &ls);
    try expectExitDiag(ls.term, 0, ls);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "src/vendor/big.bin") != null);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "src/b.zig") != null);

    // A dead exclusion refuses with the rule, not a guess.
    var tmp_c = std.testing.tmpDir(.{});
    defer tmp_c.cleanup();
    try initRepo(gpa, io, tmp_c.dir);
    try adoptStore(gpa, io, tmp_a.dir, tmp_c.dir);
    var dead = try runZit(gpa, io, tmp_c.dir, &.{ "open", "--cone=src", "--cone-not=tools" });
    defer freeResult(gpa, &dead);
    try expectExit(dead.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, dead.stderr, "dead rule") != null);
}

/// One `zit cone test` output line, split back into its three columns.
const ConeTestLine = struct {
    verdict: []const u8,
    path: []const u8,
    /// Byte offset of the third column, so alignment is checked as the
    /// property it is rather than by transcribing one instance's spaces.
    reason_at: usize,
    reason: []const u8,
};

fn parseConeTestLine(line: []const u8) !ConeTestLine {
    const v_end = std.mem.indexOfScalar(u8, line, ' ') orelse return error.NoVerdict;
    var i = v_end;
    while (i < line.len and line[i] == ' ') i += 1;
    const p_start = i;
    const p_end = std.mem.indexOfScalarPos(u8, line, p_start, ' ') orelse return error.NoPath;
    i = p_end;
    while (i < line.len and line[i] == ' ') i += 1;
    return .{
        .verdict = line[0..v_end],
        .path = line[p_start..p_end],
        .reason_at = i,
        .reason = line[i..],
    };
}

/// One member read back out of a tar stream: enough of the header to assert
/// what the archive verbs promise, plus the content bytes.
const TarMember = struct {
    name: []const u8,
    /// Octal permission bits from the header's mode field.
    mode: u32,
    type_flag: u8,
    mtime: u64,
    uid: u32,
    gid: u32,
    link_target: []const u8,
    content: []const u8,
};

fn tarOctal(field: []const u8) u64 {
    var v: u64 = 0;
    for (field) |c| {
        if (c < '0' or c > '7') break; // NUL- or space-terminated
        v = v * 8 + (c - '0');
    }
    return v;
}

fn tarCstr(field: []const u8) []const u8 {
    const end = std.mem.indexOfScalar(u8, field, 0) orelse field.len;
    return field[0..end];
}

/// Gunzip `bytes` and walk the tar blocks into members. Deliberately hand
/// rolled rather than run through a tar library: the point is to assert the
/// bytes this repository writes, and a library that disagreed with the writer
/// in the same direction twice would hide exactly the bug worth catching.
/// Understands the GNU long-name escape, since tracked paths outrun ustar.
/// The decoded archive and its members. `members` borrow `tar`, so both are
/// freed together by `deinit`.
const TarArchive = struct {
    tar: []u8,
    members: []TarMember,

    fn deinit(self: *TarArchive, gpa: std.mem.Allocator) void {
        gpa.free(self.members);
        gpa.free(self.tar);
        self.* = undefined;
    }
};

fn readTarGz(gpa: std.mem.Allocator, bytes: []const u8) !TarArchive {
    var fr = std.Io.Reader.fixed(bytes);
    const window = try gpa.alloc(u8, std.compress.flate.max_window_len);
    defer gpa.free(window);
    var dc = std.compress.flate.Decompress.init(&fr, .gzip, window);
    const tar = try dc.reader.allocRemaining(gpa, .limited(64 * 1024 * 1024));
    errdefer gpa.free(tar);
    // An archive that decoded to nothing would make every assertion below pass
    // vacuously, which is the one way this reader could hide the bug it exists
    // to catch.
    try std.testing.expect(tar.len >= 512);

    var out: std.ArrayListUnmanaged(TarMember) = .empty;
    errdefer out.deinit(gpa);
    var off: usize = 0;
    var pending_name: ?[]const u8 = null;
    while (off + 512 <= tar.len) {
        const h = tar[off..][0..512];
        var all_zero = true;
        for (h) |b| {
            if (b != 0) {
                all_zero = false;
                break;
            }
        }
        if (all_zero) break; // end-of-archive marker
        off += 512;
        const size: usize = @intCast(tarOctal(h[124..136]));
        const flag = h[156];
        const body = tar[off..][0..size];
        off += (size + 511) / 512 * 512;

        if (flag == 'L') { // GNU long name: the NEXT header's real path
            pending_name = tarCstr(body);
            continue;
        }
        const prefix = tarCstr(h[345..500]);
        const short = tarCstr(h[0..100]);
        // These fixtures stay inside ustar's 100-byte name field or use the
        // GNU escape; a prefix split would need an allocation this reader
        // deliberately does not make, so assert rather than silently join.
        try std.testing.expectEqual(@as(usize, 0), prefix.len);
        const name = pending_name orelse short;
        pending_name = null;
        try out.append(gpa, .{
            .name = name,
            .mode = @intCast(tarOctal(h[100..108])),
            .type_flag = flag,
            .mtime = tarOctal(h[136..148]),
            .uid = @intCast(tarOctal(h[108..116])),
            .gid = @intCast(tarOctal(h[116..124])),
            .link_target = tarCstr(h[157..257]),
            .content = body,
        });
    }
    return .{ .tar = tar, .members = try out.toOwnedSlice(gpa) };
}

test "cli tarball: the member set, the modes, and byte-identical repeats" {
    // Goal: tarball's stated properties, end to end against the real binary — the
    // archive equals the check-in minus control files, an executable stays
    // executable, and two runs of one VERSION produce identical bytes.
    // Method: commit a tree with a nested path and an executable, archive it,
    // and read the tar back out block by block.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, "src", .default_dir);
    try tmp.dir.createDirPath(io, "deep/a/b/c");
    try writeFile(io, tmp.dir, "README", "readme\n");
    try writeFile(io, tmp.dir, "src/main.c", "int main(){}\n");
    try writeFile(io, tmp.dir, "configure", "#!/bin/sh\n");
    try writeFile(io, tmp.dir, "deep/a/b/c/nested.txt", "deep\n");
    // The executable bit is what makes the F card's perm token carry an `x`,
    // and losing it is the archetypal release bug: right files, right bytes,
    // and ./configure will not run.
    try chmodFile(io, tmp.dir, "configure", 0o755);
    try addManaged(gpa, io, tmp.dir, &.{
        "README", "src/main.c", "configure", "deep/a/b/c/nested.txt",
    });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "base" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    var made = try runZit(gpa, io, tmp.dir, &.{ "tarball", "tip", "out.tar.gz", "--name", "rel-1" });
    defer freeResult(gpa, &made);
    try expectExitDiag(made.term, 0, made);

    const archive_bytes = try readFile(gpa, io, tmp.dir, "out.tar.gz");
    defer gpa.free(archive_bytes);
    var arch = try readTarGz(gpa, archive_bytes);
    defer arch.deinit(gpa);
    const members = arch.members;

    // Canonical filename order, every member under the one top directory, and
    // NO control file — `.zit`, `.zit-checkout`, and the cache all sit in the
    // same tree this archive was cut from.
    try std.testing.expectEqual(@as(usize, 4), members.len);
    try std.testing.expectEqualStrings("rel-1/README", members[0].name);
    try std.testing.expectEqualStrings("rel-1/configure", members[1].name);
    try std.testing.expectEqualStrings("rel-1/deep/a/b/c/nested.txt", members[2].name);
    try std.testing.expectEqualStrings("rel-1/src/main.c", members[3].name);
    for (members) |m| {
        try std.testing.expect(std.mem.indexOf(u8, m.name, ".zit") == null);
        // No identity is embedded, so two machines agree.
        try std.testing.expectEqual(@as(u32, 0), m.uid);
        try std.testing.expectEqual(@as(u32, 0), m.gid);
        try std.testing.expectEqual(@as(u8, '0'), m.type_flag);
    }

    // Contents survive verbatim.
    try std.testing.expectEqualStrings("readme\n", members[0].content);
    try std.testing.expectEqualStrings("#!/bin/sh\n", members[1].content);
    try std.testing.expectEqualStrings("deep\n", members[2].content);
    try std.testing.expectEqualStrings("int main(){}\n", members[3].content);

    // Modes come from the F card, not from the disk the archive was written on.
    try std.testing.expectEqual(@as(u32, 0o644), members[0].mode);
    try std.testing.expectEqual(@as(u32, 0o755), members[1].mode); // configure
    try std.testing.expectEqual(@as(u32, 0o644), members[2].mode);

    // Every member carries the CHECK-IN's time, one value, never the clock.
    for (members) |m| try std.testing.expectEqual(members[0].mtime, m.mtime);

    // A BARE store archives too, with no checkout anywhere: cutting a release
    // from a forge mirror is the primary use of this verb, not an edge case.
    // (This assertion exists because the `-R` wiring was silently dropped once
    // — the dispatch handed the verb the pre-override environment — and every
    // other test in this file ran from inside a checkout, so none noticed.)
    try tmp.dir.createDir(io, "bare", .default_dir);
    var bare = try tmp.dir.openDir(io, "bare", .{});
    defer bare.close(io);
    const store_bytes = try tmp.dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(store_bytes);
    try writeFile(io, bare, ".zit", store_bytes);
    var bare_list = try runZit(gpa, io, tmp.dir, &.{ "tarball", "-R", "bare", "tip", "--list", "--name", "rel-1" });
    defer freeResult(gpa, &bare_list);
    try expectExitDiag(bare_list.term, 0, bare_list);
    try std.testing.expectEqualStrings(
        "rel-1/README\nrel-1/configure\nrel-1/deep/a/b/c/nested.txt\nrel-1/src/main.c\n",
        bare_list.stdout,
    );

    // Reproducible: a second run of the same VERSION is byte-identical.
    var again = try runZit(gpa, io, tmp.dir, &.{ "tarball", "tip", "again.tar.gz", "--name", "rel-1" });
    defer freeResult(gpa, &again);
    try expectExitDiag(again.term, 0, again);
    const again_bytes = try readFile(gpa, io, tmp.dir, "again.tar.gz");
    defer gpa.free(again_bytes);
    try std.testing.expectEqualSlices(u8, archive_bytes, again_bytes);
}

test "cli tarball: --list previews, globs select, and an elided file refuses" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, "src", .default_dir);
    try tmp.dir.createDir(io, "src/vendor", .default_dir);
    try writeFile(io, tmp.dir, "README", "readme\n");
    try writeFile(io, tmp.dir, "src/a.c", "a\n");
    try writeFile(io, tmp.dir, "src/vendor/big.c", "vendored\n");
    try addManaged(gpa, io, tmp.dir, &.{ "README", "src/a.c", "src/vendor/big.c" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "base" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    // --list names the members and writes nothing, even with an OUTPUTFILE.
    var listed = try runZit(gpa, io, tmp.dir, &.{ "tarball", "tip", "preview.tar.gz", "--list" });
    defer freeResult(gpa, &listed);
    try expectExitDiag(listed.term, 0, listed);
    try std.testing.expectEqualStrings(
        "preview/README\npreview/src/a.c\npreview/src/vendor/big.c\n",
        listed.stdout,
    );
    try std.testing.expect(!fileExists(io, tmp.dir, "preview.tar.gz"));

    // The top directory is derived from the output name with the archive
    // suffix stripped; /dev/null has no usable name, so the check-in stands in.
    var devnull = try runZit(gpa, io, tmp.dir, &.{ "tarball", "tip", "/dev/null", "--list" });
    defer freeResult(gpa, &devnull);
    try expectExitDiag(devnull.term, 0, devnull);
    try std.testing.expect(std.mem.indexOf(u8, devnull.stdout, "null/README") == null);

    // Exclude wins over include on a file matching both — fossil's rule.
    var globbed = try runZit(gpa, io, tmp.dir, &.{
        "tarball", "tip", "g.tar.gz", "--list", "--include", "*.c", "--exclude", "src/vendor/*",
    });
    defer freeResult(gpa, &globbed);
    try expectExitDiag(globbed.term, 0, globbed);
    try std.testing.expectEqualStrings("g/src/a.c\n", globbed.stdout);
}

/// One zip central-directory entry, read back out of the archive. Only the
/// central directory is parsed: it is the record a reader trusts for metadata,
/// and the only place a member's mode appears.
const ZipEntry = struct {
    name: []const u8,
    method: u16,
    crc: u32,
    compressed_size: u32,
    uncompressed_size: u32,
    /// The Unix `st_mode` from the high 16 bits of the external attributes.
    unix_mode: u32,
};

/// Walk a zip's central directory. Hand rolled for the same reason the tar
/// reader is: the point is to assert the bytes this repository writes, and a
/// library agreeing with the writer in the same wrong direction would hide the
/// bug worth catching.
fn readZipCentral(gpa: std.mem.Allocator, bytes: []const u8) ![]ZipEntry {
    // Find the end-of-central-directory record by scanning back for its
    // signature. No archive comment is written, so it is the last 22 bytes;
    // scanning anyway keeps the reader honest if that ever changes.
    if (bytes.len < 22) return error.TooShort;
    var eocd: usize = bytes.len - 22;
    while (true) {
        if (std.mem.readInt(u32, bytes[eocd..][0..4], .little) == 0x06054b50) break;
        if (eocd == 0) return error.NoEndOfCentralDirectory;
        eocd -= 1;
    }
    const count = std.mem.readInt(u16, bytes[eocd + 10 ..][0..2], .little);
    var off: usize = std.mem.readInt(u32, bytes[eocd + 16 ..][0..4], .little);

    var out: std.ArrayListUnmanaged(ZipEntry) = .empty;
    errdefer out.deinit(gpa);
    var i: u16 = 0;
    while (i < count) : (i += 1) {
        if (std.mem.readInt(u32, bytes[off..][0..4], .little) != 0x02014b50)
            return error.BadCentralHeader;
        const name_len = std.mem.readInt(u16, bytes[off + 28 ..][0..2], .little);
        const extra_len = std.mem.readInt(u16, bytes[off + 30 ..][0..2], .little);
        const comment_len = std.mem.readInt(u16, bytes[off + 32 ..][0..2], .little);
        try out.append(gpa, .{
            .name = bytes[off + 46 ..][0..name_len],
            .method = std.mem.readInt(u16, bytes[off + 10 ..][0..2], .little),
            .crc = std.mem.readInt(u32, bytes[off + 16 ..][0..4], .little),
            .compressed_size = std.mem.readInt(u32, bytes[off + 20 ..][0..4], .little),
            .uncompressed_size = std.mem.readInt(u32, bytes[off + 24 ..][0..4], .little),
            .unix_mode = std.mem.readInt(u32, bytes[off + 38 ..][0..4], .little) >> 16,
        });
        off += 46 + @as(usize, name_len) + extra_len + comment_len;
    }
    return out.toOwnedSlice(gpa);
}

test "cli zip: modes ride the external attributes, and the CRCs are real" {
    // Goal: the zip half of the archive verbs. Same member set and selection as tarball, but
    // the mode lives one field deeper — in the central directory's external
    // attributes, honoured only when `version made by` says Unix — so a writer
    // can produce a perfectly readable archive in which every file is 0644.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, "src", .default_dir);
    try writeFile(io, tmp.dir, "README", "readme\n");
    try writeFile(io, tmp.dir, "src/main.c", "int main(){}\n");
    try writeFile(io, tmp.dir, "configure", "#!/bin/sh\n");
    // Long and highly repetitive, so deflate is a real win and the writer's
    // "compress only when it helps" rule has both branches exercised here.
    const compressible = "the same line over and over again\n" ** 400;
    try writeFile(io, tmp.dir, "big.txt", compressible);
    try chmodFile(io, tmp.dir, "configure", 0o755);
    try addManaged(gpa, io, tmp.dir, &.{ "README", "src/main.c", "configure", "big.txt" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "base" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    var made = try runZit(gpa, io, tmp.dir, &.{ "zip", "tip", "out.zip", "--name", "rel-1" });
    defer freeResult(gpa, &made);
    try expectExitDiag(made.term, 0, made);

    const archive_bytes = try readFile(gpa, io, tmp.dir, "out.zip");
    defer gpa.free(archive_bytes);
    const entries = try readZipCentral(gpa, archive_bytes);
    defer gpa.free(entries);

    try std.testing.expectEqual(@as(usize, 4), entries.len);
    try std.testing.expectEqualStrings("rel-1/README", entries[0].name);
    try std.testing.expectEqualStrings("rel-1/big.txt", entries[1].name);
    try std.testing.expectEqualStrings("rel-1/configure", entries[2].name);
    try std.testing.expectEqualStrings("rel-1/src/main.c", entries[3].name);

    // S_IFREG with the F card's permission. The executable is the case that
    // ships a release whose ./configure will not run.
    try std.testing.expectEqual(@as(u32, 0o100644), entries[0].unix_mode);
    try std.testing.expectEqual(@as(u32, 0o100755), entries[2].unix_mode);

    // The CRC is the archive's own integrity claim; a wrong one makes every
    // reader reject the member, so it is checked against an independent
    // computation rather than assumed.
    try std.testing.expectEqual(@import("zit").crc32.Crc32.hash("readme\n"), entries[0].crc);
    try std.testing.expectEqual(@as(u32, 7), entries[0].uncompressed_size);

    // Compression is applied only when it helps: the repetitive file deflates,
    // the seven-byte one would GROW and is therefore stored.
    try std.testing.expectEqual(@as(u16, 8), entries[1].method); // deflate
    try std.testing.expectEqual(@as(u32, compressible.len), entries[1].uncompressed_size);
    try std.testing.expect(entries[1].compressed_size < entries[1].uncompressed_size / 4);
    try std.testing.expectEqual(@as(u16, 0), entries[0].method); // stored

    // Reproducible, exactly as the tarball is.
    var again = try runZit(gpa, io, tmp.dir, &.{ "zip", "tip", "again.zip", "--name", "rel-1" });
    defer freeResult(gpa, &again);
    try expectExitDiag(again.term, 0, again);
    const again_bytes = try readFile(gpa, io, tmp.dir, "again.zip");
    defer gpa.free(again_bytes);
    try std.testing.expectEqualSlices(u8, archive_bytes, again_bytes);

    // The shared surface is genuinely shared: selection, --list, and the
    // top-directory rule come from one place, so one probe per verb is enough.
    var listed = try runZit(gpa, io, tmp.dir, &.{ "zip", "tip", "--list", "--exclude", "src/*" });
    defer freeResult(gpa, &listed);
    try expectExitDiag(listed.term, 0, listed);
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, "src/main.c") == null);
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, "/README") != null);
}

test "cli tarball: a sparse store refuses by name rather than writing a partial archive" {
    // Goal: the failure mode that looks like success. A sparse clone holds no
    // bytes for out-of-cone files, and an archive quietly missing them would
    // have the right shape and the wrong contents. Method: serve a repo, clone
    // it with a cone, and archive the tip.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "README", "readme\n");
    try writeFile(io, tmp_a.dir, "src/a.c", "a\n");
    try writeFile(io, tmp_a.dir, "tools/t.bin", "tools\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "README", "src/a.c", "tools/t.bin" });
    var c1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", "--cone=src", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    var op = try runZit(gpa, io, b_dir, &.{ "open", "--cone=src" });
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);

    // The in-cone file archives on its own; the elided ones refuse BY NAME.
    var refused = try runZit(gpa, io, b_dir, &.{ "tarball", "tip", "part.tar.gz" });
    defer freeResult(gpa, &refused);
    try expectExit(refused.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, refused.stderr, "partial archive") != null);
    try std.testing.expect(std.mem.indexOf(u8, refused.stderr, "tools/t.bin") != null);
    // Named, not counted: the remedy differs per path, so a bare count would
    // send the operator back to guess which files were missing.
    try std.testing.expect(std.mem.indexOf(u8, refused.stderr, "cone add") != null);
    try std.testing.expect(!fileExists(io, b_dir, "part.tar.gz"));

    // Narrowed to what the store actually holds, the same command succeeds.
    var ok = try runZit(gpa, io, b_dir, &.{ "tarball", "tip", "in-cone.tar.gz", "--include", "src/*" });
    defer freeResult(gpa, &ok);
    try expectExitDiag(ok.term, 0, ok);
    try std.testing.expect(fileExists(io, b_dir, "in-cone.tar.gz"));
}

test "cli cone test: names the deciding member, and the exit status is a predicate" {
    // Goal: `cone test` end to end. The verdict alone cannot answer "why isn't this
    // file here" under longest match, so each line names the member that
    // decided it -- bare, because the verdict column already carries the
    // polarity and the token has to paste into `zit cone remove`. Method:
    // the same nested cone as the exclusions test above (an inclusion, an
    // exclusion inside it, a re-inclusion inside that), probed for all three
    // outcomes plus the exit codes, stdin, --quiet, and --store.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "src/vendor", .default_dir);
    try tmp_a.dir.createDir(io, "src/vendor/keep", .default_dir);
    try writeFile(io, tmp_a.dir, "README", "readme\n");
    try writeFile(io, tmp_a.dir, "src/a.zig", "a\n");
    try writeFile(io, tmp_a.dir, "src/vendor/big.bin", "vendored\n");
    try writeFile(io, tmp_a.dir, "src/vendor/keep/gem.txt", "kept\n");
    try addManaged(gpa, io, tmp_a.dir, &.{
        "README", "src/a.zig", "src/vendor/big.bin", "src/vendor/keep/gem.txt",
    });
    var c1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "base" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    try initRepo(gpa, io, tmp_b.dir);
    try adoptStore(gpa, io, tmp_a.dir, tmp_b.dir);
    var opened = try runZit(gpa, io, tmp_b.dir, &.{
        "open", "--cone=src", "--cone-not=src/vendor", "--cone=src/vendor/keep",
    });
    defer freeResult(gpa, &opened);
    try expectExitDiag(opened.term, 0, opened);

    // All four outcomes in one call, so the alignment is exercised across
    // paths of very different lengths.
    var all = try runZit(gpa, io, tmp_b.dir, &.{
        "cone",               "test",                    "src/a.zig",
        "src/vendor/big.bin", "src/vendor/keep/gem.txt", "README",
    });
    defer freeResult(gpa, &all);
    // Something is elided, so the predicate says so.
    try expectExitDiag(all.term, 1, all);

    var lines = std.mem.splitScalar(u8, std.mem.trimEnd(u8, all.stdout, "\n"), '\n');
    const l1 = try parseConeTestLine(lines.next().?);
    try std.testing.expectEqualStrings("covered", l1.verdict);
    try std.testing.expectEqualStrings("src/a.zig", l1.path);
    try std.testing.expectEqualStrings("src", l1.reason);

    // The exclusion is LONGER than the inclusion containing it, so it
    // decides -- the case a last-match-wins reading of the member list gets
    // wrong, and the reason this column exists.
    const l2 = try parseConeTestLine(lines.next().?);
    try std.testing.expectEqualStrings("elided", l2.verdict);
    try std.testing.expectEqualStrings("src/vendor/big.bin", l2.path);
    try std.testing.expectEqualStrings("src/vendor", l2.reason);

    // Re-inclusion deeper still: the longest member wins again, and the line
    // names it rather than the exclusion it overrode.
    const l3 = try parseConeTestLine(lines.next().?);
    try std.testing.expectEqualStrings("covered", l3.verdict);
    try std.testing.expectEqualStrings("src/vendor/keep/gem.txt", l3.path);
    try std.testing.expectEqualStrings("src/vendor/keep", l3.reason);

    // The one verdict with no member to name.
    const l4 = try parseConeTestLine(lines.next().?);
    try std.testing.expectEqualStrings("elided", l4.verdict);
    try std.testing.expectEqualStrings("README", l4.path);
    try std.testing.expectEqualStrings("(no member covers)", l4.reason);
    try std.testing.expect(lines.next() == null);

    // The member column lines up: the property, not one instance's spaces.
    try std.testing.expectEqual(l1.reason_at, l2.reason_at);
    try std.testing.expectEqual(l1.reason_at, l3.reason_at);
    try std.testing.expectEqual(l1.reason_at, l4.reason_at);
    // ...and it clears the longest path rather than merely the first.
    try std.testing.expect(l1.reason_at > l3.path.len);

    // Every path covered: exit 0, so `zit cone test X && ...` is usable.
    var ok = try runZit(gpa, io, tmp_b.dir, &.{
        "cone", "test", "src/a.zig", "src/vendor/keep/gem.txt",
    });
    defer freeResult(gpa, &ok);
    try expectExitDiag(ok.term, 0, ok);

    // --quiet leaves only the status.
    var quiet = try runZit(gpa, io, tmp_b.dir, &.{ "cone", "test", "--quiet", "README" });
    defer freeResult(gpa, &quiet);
    try expectExit(quiet.term, 1);
    try std.testing.expectEqualStrings("", quiet.stdout);

    // `-` reads the path list from stdin, one per line.
    try writeFile(io, tmp_b.dir, "paths.txt", "src/a.zig\nsrc/vendor/big.bin\n");
    var piped = try runZitStdin(gpa, io, tmp_b.dir, "paths.txt", &.{ "cone", "test", "-" });
    defer freeResult(gpa, &piped);
    try expectExitDiag(piped.term, 1, piped);
    try std.testing.expect(std.mem.indexOf(u8, piped.stdout, "src/a.zig") != null);
    try std.testing.expect(std.mem.indexOf(u8, piped.stdout, "src/vendor") != null);

    // A path that could never be tracked is a usage error (2), not a silent
    // `elided` -- the answer "no" would be true but useless.
    var bad = try runZit(gpa, io, tmp_b.dir, &.{ "cone", "test", "../escape" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, bad.stderr, "repository-relative") != null);

    // --store asks the other question. This checkout adopted a dense store,
    // so the store cone covers what the worktree cone elides -- which is the
    // distinct condition the flag exists to expose.
    var store = try runZit(gpa, io, tmp_b.dir, &.{ "cone", "test", "--store", "src/vendor/big.bin" });
    defer freeResult(gpa, &store);
    try expectExitDiag(store.term, 0, store);
    try std.testing.expect(std.mem.indexOf(u8, store.stdout, "covered") != null);
    try std.testing.expect(std.mem.indexOf(u8, store.stdout, "(dense)") != null);
}

test "cli clone --cone-not: the excluded subtree's blobs stay elided over the wire" {
    // Goal: section 6.1's cone-not pragma composes with clone's local belt:
    // an excluded subtree's exclusive blobs elide exactly as out-of-cone
    // blobs do. Method: serve a repo with an in-cone and an excluded file,
    // clone --cone/--cone-not, open, and probe both the tree and verify's
    // sparse summary.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "src/vendor", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "in-cone content\n");
    try writeFile(io, tmp_a.dir, "src/vendor/big.bin", "excluded vendored bytes\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "src/vendor/big.bin" });
    var c1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{
        "clone", "--no-open", "--cone=src", "--cone-not=src/vendor", url, "repo",
    });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    var vf = try runZit(gpa, io, b_dir, &.{"verify"});
    defer freeResult(gpa, &vf);
    try expectExitDiag(vf.term, 0, vf);
    try std.testing.expect(std.mem.indexOf(u8, vf.stdout, "sparse store") != null);
    try std.testing.expect(std.mem.indexOf(u8, vf.stdout, "deliberately elided") != null);

    var op = try runZit(gpa, io, b_dir, &.{ "open", "--cone=src", "--cone-not=src/vendor" });
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);
    try std.testing.expect(fileExists(io, b_dir, "src/a.zig"));
    try std.testing.expect(!fileExists(io, b_dir, "src/vendor/big.bin"));
}

test "cli clone --cone-file: members from a file, comments, blanks, and '!' exclusions" {
    // Goal: the --cone-file grammar end to end: one member per line, '#'
    // comments and blank lines skipped, a '!' prefix marking an exclusion;
    // the resulting sparse store matches the equivalent flag form. Method:
    // serve a three-path repo, clone through a cone file, open and probe.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "in\n");
    try writeFile(io, tmp_a.dir, "tools/t.bin", "out\n");
    try writeFile(io, tmp_a.dir, "README", "root file\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.bin", "README" });
    var c1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    try writeFile(io, tmp_b.dir, "cone.txt", "# my sparse profile\nsrc\n!src/generated\n\nREADME\n");
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", "--cone-file=cone.txt", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);
    var op = try runZit(gpa, io, b_dir, &.{ "open", "--cone=src", "--cone=README" });
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);
    try std.testing.expect(fileExists(io, b_dir, "src/a.zig"));
    try std.testing.expect(fileExists(io, b_dir, "README"));
    try std.testing.expect(!fileExists(io, b_dir, "tools/t.bin"));
}

test "cli serve on a sparse store: dense and uncovered clones refuse, a covered cone clone succeeds" {
    // Goal: section 5.3 -- a sparse origin refuses to serve clones it does
    // not cover (dense requests and cones outside its coverage) with one
    // error card and zero content bytes; a clone wholly inside its cone is
    // served. Method: build a dense repo, clone --cone=src into B, serve B,
    // then attempt the three clone shapes against it.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try tmp_a.dir.createDir(io, "src", .default_dir);
    try tmp_a.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp_a.dir, "src/a.zig", "in\n");
    try writeFile(io, tmp_a.dir, "tools/t.bin", "out\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "src/a.zig", "tools/t.bin" });
    var c1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    const port_a = try freshLoopbackPort(io);
    var server_a = try spawnServe(gpa, io, tmp_a.dir, port_a);
    defer server_a.kill(io);
    try waitForListen(io, port_a);
    const url_a = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port_a});
    defer gpa.free(url_a);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone_b = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--cone=src", url_a, "repo" });
    defer freeResult(gpa, &clone_b);
    try expectExitDiag(clone_b.term, 0, clone_b);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    const port_b = try freshLoopbackPort(io);
    var server_b = try spawnServe(gpa, io, b_dir, port_b);
    defer server_b.kill(io);
    try waitForListen(io, port_b);
    const url_b = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port_b});
    defer gpa.free(url_b);

    var tmp_c = std.testing.tmpDir(.{});
    defer tmp_c.cleanup();
    // A dense clone from the sparse origin refuses.
    var dense = try runZit(gpa, io, tmp_c.dir, &.{ "clone", url_b, "dense" });
    defer freeResult(gpa, &dense);
    try expectNonzeroExit(dense.term);
    // An uncovered cone refuses the same way.
    var wide = try runZit(gpa, io, tmp_c.dir, &.{ "clone", "--cone=tools", url_b, "wide" });
    defer freeResult(gpa, &wide);
    try expectNonzeroExit(wide.term);
    // A cone inside the origin's coverage is served.
    var narrow = try runZit(gpa, io, tmp_c.dir, &.{ "clone", "--no-open", "--cone=src", url_b, "narrow" });
    defer freeResult(gpa, &narrow);
    try expectExitDiag(narrow.term, 0, narrow);
    var n_dir = try tmp_c.dir.openDir(io, "narrow", .{});
    defer n_dir.close(io);
    var op = try runZit(gpa, io, n_dir, &.{ "open", "--cone=src" });
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);
    try std.testing.expect(fileExists(io, n_dir, "src/a.zig"));
}

test "cli annotate VERSION and --full: bare-repo annotate at a named check-in, full IDs join against info" {
    // Goal: annotate's two halves. VERSION resolves like cat's (here: a
    // 64-hex hash and 'tip'), works with -R and no working tree, and the
    // default bare invocation is unchanged. --full swaps the 10-char prefix
    // for the full artifact ID, byte-identical lines otherwise. Method: a
    // two-commit history where the second commit appends a line, then
    // annotate at both versions and cross-check prefix vs full output.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "alpha\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});
    const c1 = try annotateCommit(gpa, io, tmp.dir, "1577836800", "one"); // 2020-01-01
    defer gpa.free(c1);
    try writeFile(io, tmp.dir, "f.txt", "alpha\nbeta\n");
    const c2 = try annotateCommit(gpa, io, tmp.dir, "1577923200", "two"); // 2020-01-02
    defer gpa.free(c2);

    // VERSION = the first check-in: only its one line, attributed to c1.
    var v1 = try runZit(gpa, io, tmp.dir, &.{ "annotate", "f.txt", c1 });
    defer freeResult(gpa, &v1);
    try expectExitDiag(v1.term, 0, v1);
    const v1_expected = try std.fmt.allocPrint(gpa, "{s} 2020-01-01     1: alpha\n", .{c1[0..12]});
    defer gpa.free(v1_expected);
    try std.testing.expectEqualStrings(v1_expected, v1.stdout);

    // Bare default (no VERSION) still annotates the current checkout.
    var cur = try runZit(gpa, io, tmp.dir, &.{ "annotate", "f.txt" });
    defer freeResult(gpa, &cur);
    try expectExitDiag(cur.term, 0, cur);

    // 'tip' + --full from a BARE store (-R, no working tree): both lines,
    // full 64-hex IDs joining exactly against the commit hashes.
    var bare = std.testing.tmpDir(.{});
    defer bare.cleanup();
    const cwd = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd);
    const repo_arg = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &tmp.sub_path });
    defer gpa.free(repo_arg);
    var full = try runZit(gpa, io, bare.dir, &.{ "annotate", "-R", repo_arg, "f.txt", "tip", "--full" });
    defer freeResult(gpa, &full);
    try expectExitDiag(full.term, 0, full);
    const full_expected = try std.fmt.allocPrint(
        gpa,
        "{s} 2020-01-01     1: alpha\n{s} 2020-01-02     2: beta\n",
        .{ c1, c2 },
    );
    defer gpa.free(full_expected);
    try std.testing.expectEqualStrings(full_expected, full.stdout);

    // The full IDs are prefix-consistent with the default rendering.
    try std.testing.expect(std.mem.startsWith(u8, full.stdout, cur.stdout[0..12]));

    // An unresolvable VERSION refuses with the resolve diagnostic, exit 1.
    var bad = try runZit(gpa, io, tmp.dir, &.{ "annotate", "f.txt", "no-such-branch" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, bad.stderr, "not found") != null);
}

/// Parse the `cursor CURSOR TOKEN` trailer off a frames invocation's stdout,
/// returning gpa-owned copies. The trailer is the final line by contract.
fn framesTrailer(gpa: std.mem.Allocator, stdout: []const u8) !struct { cursor: []const u8, token: []const u8 } {
    var last: []const u8 = "";
    var it = std.mem.splitScalar(u8, stdout, '\n');
    while (it.next()) |line| {
        if (std.mem.startsWith(u8, line, "cursor ")) last = line;
    }
    try std.testing.expect(last.len > 0);
    var fields = std.mem.splitScalar(u8, last, ' ');
    _ = fields.next(); // "cursor"
    const cursor = fields.next() orelse return error.TestUnexpectedResult;
    const token = fields.next() orelse return error.TestUnexpectedResult;
    try std.testing.expect(fields.next() == null);
    return .{ .cursor = try gpa.dupe(u8, cursor), .token = try gpa.dupe(u8, token) };
}

/// Count `artifact HASH SIZE` header lines in a frames stdout. Counts only
/// well-formed header positions (start of stream or just past a payload), but
/// a plain prefix count suffices for these fixtures (no payload embeds one at
/// line start followed by hex + decimal).
fn framesArtifactCount(stdout: []const u8) usize {
    var n: usize = 0;
    var it = std.mem.splitScalar(u8, stdout, '\n');
    while (it.next()) |line| {
        if (std.mem.startsWith(u8, line, "artifact ")) n += 1;
    }
    return n;
}

test "cli frames: over a packed delta chain every artifact still matches artifact --raw" {
    // Goal: the reader that a plaintext memo sits behind. `pack` re-deltas
    // the store, so the stream's artifacts resolve through delta bases rather
    // than standing alone — the path where a wrong or stale memo would show
    // up. Method: commit a growing file repeatedly so successive blobs delta
    // well, pack to force that encoding, then compare EVERY streamed payload
    // against `artifact --raw` for the same hash.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // A file that grows by an appended line each time: consecutive versions
    // share nearly all their bytes, which is what makes `pack` choose deltas
    // and what makes consecutive artifacts share a base.
    var body: std.ArrayList(u8) = .empty;
    defer body.deinit(gpa);
    try body.appendSlice(gpa, "line 0 — enough shared text that a delta beats a fresh frame\n");
    try writeFile(io, tmp.dir, "a.txt", body.items);
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var round: usize = 1;
    while (round < 8) : (round += 1) {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "grow" });
        defer freeResult(gpa, &c);
        try expectExit(c.term, 0);
        try body.print(gpa, "line {d} — enough shared text that a delta beats a fresh frame\n", .{round});
        try writeFile(io, tmp.dir, "a.txt", body.items);
    }
    var last = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "grow" });
    defer freeResult(gpa, &last);
    try expectExit(last.term, 0);

    var packed_run = try runZit(gpa, io, tmp.dir, &.{"pack"});
    defer freeResult(gpa, &packed_run);
    try expectExit(packed_run.term, 0);

    var stream = try runZit(gpa, io, tmp.dir, &.{"frames"});
    defer freeResult(gpa, &stream);
    try expectExitDiag(stream.term, 0, stream);
    try std.testing.expect(framesArtifactCount(stream.stdout) >= 16); // 8 blobs + 8 check-ins

    // Walk the stream's `artifact HASH LEN` headers and check each payload
    // against the single-artifact reader. A memo that ever served the wrong
    // base would surface here as a mismatch on the first delta-resolved
    // artifact, not as a crash.
    var checked: usize = 0;
    var pos: usize = 0;
    while (std.mem.indexOfPos(u8, stream.stdout, pos, "artifact ")) |hit| {
        // Only a header at a line start counts; payload bytes may contain the
        // same word.
        if (hit != 0 and stream.stdout[hit - 1] != '\n') {
            pos = hit + 1;
            continue;
        }
        const line_end = std.mem.indexOfScalarPos(u8, stream.stdout, hit, '\n') orelse break;
        var it = std.mem.tokenizeScalar(u8, stream.stdout[hit..line_end], ' ');
        _ = it.next(); // "artifact"
        const hash = it.next() orelse break;
        const len_text = it.next() orelse break;
        const len = std.fmt.parseInt(usize, len_text, 10) catch break;
        const payload = stream.stdout[line_end + 1 ..][0..len];

        var raw = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", hash });
        defer freeResult(gpa, &raw);
        try expectExit(raw.term, 0);
        try std.testing.expectEqualStrings(raw.stdout, payload);

        checked += 1;
        pos = line_end + 1 + len;
    }
    try std.testing.expect(checked >= 16);
}

test "cli frames: bootstrap census, idle poll, incremental append, byte-identity with artifact --raw" {
    // Goal: the incremental read loop. --since=0 streams the census; a current
    // cursor answers exit 3 with a trailer and no artifacts; an append is
    // seen exactly once; each payload is byte-identical to
    // `artifact HASH --raw`. Method: drive one repo through two commits,
    // polling between, and cross-check one payload.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "alpha\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    var boot = try runZit(gpa, io, tmp.dir, &.{"frames"});
    defer freeResult(gpa, &boot);
    try expectExitDiag(boot.term, 0, boot);
    try std.testing.expectEqual(@as(usize, 2), framesArtifactCount(boot.stdout)); // blob + check-in
    const t0 = try framesTrailer(gpa, boot.stdout);
    defer gpa.free(t0.cursor);
    defer gpa.free(t0.token);

    // Idle poll: exit 3, no artifacts, the trailer still names the position.
    const since0 = try std.fmt.allocPrint(gpa, "--since={s}", .{t0.cursor});
    defer gpa.free(since0);
    const verify0 = try std.fmt.allocPrint(gpa, "--verify={s}", .{t0.token});
    defer gpa.free(verify0);
    var idle = try runZit(gpa, io, tmp.dir, &.{ "frames", since0, verify0 });
    defer freeResult(gpa, &idle);
    try expectExit(idle.term, 3);
    try std.testing.expectEqual(@as(usize, 0), framesArtifactCount(idle.stdout));
    const t_idle = try framesTrailer(gpa, idle.stdout);
    defer gpa.free(t_idle.cursor);
    defer gpa.free(t_idle.token);
    try std.testing.expectEqualStrings(t0.cursor, t_idle.cursor);

    // Append two artifacts (a blob and its check-in); the poll sees exactly
    // those, exit 0.
    try writeFile(io, tmp.dir, "b.txt", "beta\n");
    var ad = try runZit(gpa, io, tmp.dir, &.{ "add", "b.txt" });
    defer freeResult(gpa, &ad);
    try expectExitDiag(ad.term, 0, ad);
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    var incr = try runZit(gpa, io, tmp.dir, &.{ "frames", since0, verify0 });
    defer freeResult(gpa, &incr);
    try expectExitDiag(incr.term, 0, incr);
    try std.testing.expectEqual(@as(usize, 2), framesArtifactCount(incr.stdout));

    // Byte-identity: the first emitted payload equals `artifact HASH --raw`.
    var lines = std.mem.splitScalar(u8, incr.stdout, '\n');
    const header = lines.next().?;
    try std.testing.expect(std.mem.startsWith(u8, header, "artifact "));
    var hf = std.mem.splitScalar(u8, header, ' ');
    _ = hf.next();
    const hash = hf.next().?;
    const size = try std.fmt.parseInt(usize, hf.next().?, 10);
    const payload_start = header.len + 1;
    const payload = incr.stdout[payload_start .. payload_start + size];
    var raw = try runZit(gpa, io, tmp.dir, &.{ "artifact", hash, "--raw" });
    defer freeResult(gpa, &raw);
    try expectExitDiag(raw.term, 0, raw);
    try std.testing.expectEqualStrings(raw.stdout, payload);
}

test "cli frames: sync arrivals appear after the cursor; a shun reclaim invalidates it, re-read recovers" {
    // Goal: the read loop's other acceptance half. Artifacts arriving by pull are
    // appends the next poll emits; a reclaiming shun rewrites the store and
    // the poll answers the distinct cursor-invalid exit (4) with the
    // re-read remedy; --since=0 then works. Method: two repos over HTTP.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "a.txt", "alpha\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    var boot = try runZit(gpa, io, b_dir, &.{"frames"});
    defer freeResult(gpa, &boot);
    try expectExitDiag(boot.term, 0, boot);
    const t0 = try framesTrailer(gpa, boot.stdout);
    defer gpa.free(t0.cursor);
    defer gpa.free(t0.token);
    const since0 = try std.fmt.allocPrint(gpa, "--since={s}", .{t0.cursor});
    defer gpa.free(since0);
    const verify0 = try std.fmt.allocPrint(gpa, "--verify={s}", .{t0.token});
    defer gpa.free(verify0);

    // New history lands upstream; a pull appends it here; the poll sees it.
    try writeFile(io, tmp_a.dir, "b.txt", "beta\n");
    var ad = try runZit(gpa, io, tmp_a.dir, &.{ "add", "b.txt" });
    defer freeResult(gpa, &ad);
    try expectExitDiag(ad.term, 0, ad);
    var c2 = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    var pull = try runZit(gpa, io, b_dir, &.{"pull"});
    defer freeResult(gpa, &pull);
    try expectExitDiag(pull.term, 0, pull);
    var incr = try runZit(gpa, io, b_dir, &.{ "frames", since0, verify0 });
    defer freeResult(gpa, &incr);
    try expectExitDiag(incr.term, 0, incr);
    try std.testing.expectEqual(@as(usize, 2), framesArtifactCount(incr.stdout));
    const t1 = try framesTrailer(gpa, incr.stdout);
    defer gpa.free(t1.cursor);
    defer gpa.free(t1.token);
    const since1 = try std.fmt.allocPrint(gpa, "--since={s}", .{t1.cursor});
    defer gpa.free(since1);
    const verify1 = try std.fmt.allocPrint(gpa, "--verify={s}", .{t1.token});
    defer gpa.free(verify1);

    // A reclaiming shun (of the first blob) rewrites the store: the cursor
    // dies with the distinct exit code and the stated remedy.
    var boot_lines = std.mem.splitScalar(u8, boot.stdout, '\n');
    const first_header = boot_lines.next().?;
    var hf = std.mem.splitScalar(u8, first_header, ' ');
    _ = hf.next();
    const first_hash = hf.next().?;
    var sh = try runZit(gpa, io, b_dir, &.{ "shun", "--force", first_hash });
    defer freeResult(gpa, &sh);
    try expectExitDiag(sh.term, 0, sh);
    var dead = try runZit(gpa, io, b_dir, &.{ "frames", since1, verify1 });
    defer freeResult(gpa, &dead);
    try expectExit(dead.term, 4);
    try std.testing.expect(std.mem.indexOf(u8, dead.stderr, "re-read from --since=0") != null);

    // The from-zero re-read works and skips the reclaimed artifact: its
    // header line is gone (the hash still APPEARS inside the surviving
    // check-in's F card, which is correct -- the reference outlives the
    // bytes; only the emission is suppressed).
    var reread = try runZit(gpa, io, b_dir, &.{"frames"});
    defer freeResult(gpa, &reread);
    try expectExitDiag(reread.term, 0, reread);
    const reclaimed_header = try std.fmt.allocPrint(gpa, "artifact {s} ", .{first_hash});
    defer gpa.free(reclaimed_header);
    try std.testing.expect(std.mem.indexOf(u8, reread.stdout, reclaimed_header) == null);
}

test "cli help topics: names/sparse/repo/exit-codes/options answer, typos refuse, verbs fall back" {
    // Goal: the help-topic surface. Each topic prints its own text on
    // stdout (exit 0); a token that is neither verb nor topic refuses with
    // exit 2 and a diagnostic on stderr; a real verb without dedicated help
    // (version) keeps the command-list fallback; the top-level help lists
    // the topics.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const topics = [_]struct { name: []const u8, marker: []const u8 }{
        .{ .name = "names", .marker = "how a check-in name resolves" },
        .{ .name = "sparse", .marker = "sparse checkouts and sparse stores" },
        .{ .name = "repo", .marker = "without a working tree" },
        .{ .name = "exit-codes", .marker = "the exit-status contract" },
        .{ .name = "options", .marker = "end-of-options marker" },
    };
    for (topics) |t| {
        var res = try runZit(gpa, io, tmp.dir, &.{ "help", t.name });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, t.marker) != null);
        try std.testing.expectEqualStrings("", res.stderr);
    }

    // The names topic documents the timestamp forms (the drift fix).
    var names = try runZit(gpa, io, tmp.dir, &.{ "help", "names" });
    defer freeResult(gpa, &names);
    try expectExitDiag(names.term, 0, names);
    try std.testing.expect(std.mem.indexOf(u8, names.stdout, "BRANCH:TIMESTAMP") != null);

    // A typo refuses: exit 2, stderr names it, nothing on stdout.
    var bogus = try runZit(gpa, io, tmp.dir, &.{ "help", "bogus-topic" });
    defer freeResult(gpa, &bogus);
    try expectExit(bogus.term, 2);
    try std.testing.expectEqualStrings("", bogus.stdout);
    try std.testing.expect(std.mem.indexOf(u8, bogus.stderr, "no command or topic named 'bogus-topic'") != null);

    // A real verb with no dedicated help still gets the command list.
    var ver = try runZit(gpa, io, tmp.dir, &.{ "help", "version" });
    defer freeResult(gpa, &ver);
    try expectExitDiag(ver.term, 0, ver);
    try std.testing.expect(std.mem.indexOf(u8, ver.stdout, "commands:") != null);

    // The top-level help advertises the topics.
    var top = try runZit(gpa, io, tmp.dir, &.{"help"});
    defer freeResult(gpa, &top);
    try expectExitDiag(top.term, 0, top);
    try std.testing.expect(std.mem.indexOf(u8, top.stdout, "topics ('zit help TOPIC'):") != null);
}

test "store lock: openStoreAppend holds the store's writer lock and releases it on close" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // While an appender's handle is open, the lock file exists and a second
    // writer is excluded: in another process it waits, and in this one it is
    // refused at once rather than waiting for itself.
    var appender = try cli.openStoreAppend(io, tmp.dir);
    try tmp.dir.access(io, ".zit.lock", .{});
    try std.testing.expectError(error.StoreLocked, cli.openStoreAppend(io, tmp.dir));

    // A plain read takes no lock and is never excluded (verify's promise).
    var reader = try tmp.dir.openFile(io, ".zit", .{});
    reader.close(io);

    // Close releases: the lock file is gone and the next writer gets in.
    appender.close(io);
    try std.testing.expectError(error.FileNotFound, tmp.dir.access(io, ".zit.lock", .{}));
    var again = try cli.openStoreAppend(io, tmp.dir);
    again.close(io);
}

test "store lock: a writer killed while holding the lock does not block the next one" {
    // The cost of a lock file over `flock`: it outlives a crashed holder. A
    // lock naming a process that no longer exists is broken by the next writer.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    // What a writer killed mid-append leaves behind: its lock, naming it.
    var host_buf: [std.posix.HOST_NAME_MAX]u8 = undefined;
    const host = try std.posix.gethostname(&host_buf);
    var rec_buf: [256]u8 = undefined;
    const rec = try std.fmt.bufPrint(&rec_buf, "pid={d} host={s} time=1\n", .{ std.math.maxInt(std.posix.pid_t), host });
    try tmp.dir.writeFile(io, .{ .sub_path = ".zit.lock", .data = rec });

    var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice" }, &.{ "commit", "-m", "after a crash" });
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 0, res);
    try std.testing.expectError(error.FileNotFound, tmp.dir.access(io, ".zit.lock", .{}));
    try std.testing.expectError(error.FileNotFound, tmp.dir.access(io, ".zit.lock.break", .{}));
}

/// Hold the generation-rewrite lock from the test process, standing in for a
/// concurrent `pack` or `shun --force`.
fn holdRewriteLock(io: std.Io, dir: std.Io.Dir) !zit.storelock.Lock {
    var sink = std.Io.Writer.Discarding.init(&.{});
    return zit.storelock.acquire(io, dir, ".zit-tmp-pack.lock", 0, &sink.writer);
}

test "worktrees: a detached checkout opens, follows commits both ways, and shares the store" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "main", .default_dir);
    try tmp.dir.createDir(io, "wt", .default_dir);
    var main_dir = try tmp.dir.openDir(io, "main", .{});
    defer main_dir.close(io);
    var wt_dir = try tmp.dir.openDir(io, "wt", .{});
    defer wt_dir.close(io);

    // The ordinary co-located checkout: one file, one check-in.
    try initRepo(gpa, io, main_dir);
    try writeFile(io, main_dir, "greeting", "hello from main\n");
    try addManaged(gpa, io, main_dir, &.{"greeting"});
    var c1 = try runCommitEnv(gpa, io, main_dir, .{ .user = "alice" }, &.{ "commit", "-m", "first" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    const tip1 = try checkinHash(c1.stdout);

    // A detached worktree of the same store: `open --store` materializes the
    // tip with no `.zit` of its own.
    var opened = try runZit(gpa, io, wt_dir, &.{ "open", "--store=../main" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);
    try std.testing.expectError(error.FileNotFound, wt_dir.statFile(io, ".zit", .{}));
    {
        const got = try wt_dir.readFileAlloc(io, "greeting", gpa, .limited(256));
        defer gpa.free(got);
        try std.testing.expectEqualStrings("hello from main\n", got);
    }

    // A commit in the primary is visible from the worktree through the shared
    // store: update B to the new tip and read the new bytes.
    try writeFile(io, main_dir, "greeting", "hello again\n");
    var c2 = try runCommitEnv(gpa, io, main_dir, .{ .user = "alice" }, &.{ "commit", "-m", "second" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    const tip2 = try checkinHash(c2.stdout);
    try std.testing.expect(!std.mem.eql(u8, tip1, tip2));
    var up = try runZit(gpa, io, wt_dir, &.{ "update", tip2 });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);
    {
        const got = try wt_dir.readFileAlloc(io, "greeting", gpa, .limited(256));
        defer gpa.free(got);
        try std.testing.expectEqualStrings("hello again\n", got);
    }

    // And the reverse: a commit FROM the worktree appends to the shared store
    // (under its lock), and the primary can update to it.
    try writeFile(io, wt_dir, "greeting", "hello from the worktree\n");
    var c3 = try runCommitEnv(gpa, io, wt_dir, .{ .user = "bob" }, &.{ "commit", "-m", "third" });
    defer freeResult(gpa, &c3);
    try expectExit(c3.term, 0);
    const tip3 = try checkinHash(c3.stdout);
    var up_main = try runZit(gpa, io, main_dir, &.{ "update", tip3 });
    defer freeResult(gpa, &up_main);
    try expectExit(up_main.term, 0);
    {
        const got = try main_dir.readFileAlloc(io, "greeting", gpa, .limited(256));
        defer gpa.free(got);
        try std.testing.expectEqualStrings("hello from the worktree\n", got);
    }
}

/// One racing committer for the worktree stress: `count` commits of distinct
/// files from `dir`, every one under `--allow-fork` because racing tips
/// legitimately fork — the race under test is the STORE lock, not branch
/// topology. Any nonzero exit is recorded, never asserted mid-thread.
fn countFailure(failures: *std.atomic.Value(u32), term: std.process.Child.Term) void {
    switch (term) {
        .exited => |c| if (c != 0) {
            _ = failures.fetchAdd(1, .monotonic);
        },
        else => _ = failures.fetchAdd(1, .monotonic),
    }
}

/// `count` tags named `storm-tag-N` on `target`, one `zit tag add` each.
fn tagStorm(io: std.Io, dir: std.Io.Dir, target: []const u8, count: usize, failures: *std.atomic.Value(u32)) void {
    const gpa = std.heap.page_allocator;
    var i: usize = 0;
    while (i < count) : (i += 1) {
        var name_buf: [64]u8 = undefined;
        const name = std.fmt.bufPrint(&name_buf, "storm-tag-{d}", .{i}) catch unreachable;
        var res = runZit(gpa, io, dir, &.{ "tag", "add", name, target }) catch {
            _ = failures.fetchAdd(1, .monotonic);
            return;
        };
        defer freeResult(gpa, &res);
        countFailure(failures, res.term);
    }
}

/// `count` user rows, logins `storm-user-N`, one `zit config merge` each.
fn userStorm(io: std.Io, dir: std.Io.Dir, count: usize, failures: *std.atomic.Value(u32)) void {
    const gpa = std.heap.page_allocator;
    var i: usize = 0;
    while (i < count) : (i += 1) {
        var payload_buf: [128]u8 = undefined;
        const payload = std.fmt.bufPrint(&payload_buf, "1700000000 'storm-user-{d}' pw '' cap 'v' info ''", .{i}) catch unreachable;
        var file_buf: [192]u8 = undefined;
        const file = std.fmt.bufPrint(&file_buf, "config /user {d}\n{s}\n", .{ payload.len, payload }) catch unreachable;
        var fname_buf: [48]u8 = undefined;
        const fname = std.fmt.bufPrint(&fname_buf, "users-{d}.txt", .{i}) catch unreachable;
        writeFile(io, dir, fname, file) catch {
            _ = failures.fetchAdd(1, .monotonic);
            return;
        };
        var res = runZit(gpa, io, dir, &.{ "config", "merge", fname }) catch {
            _ = failures.fetchAdd(1, .monotonic);
            return;
        };
        defer freeResult(gpa, &res);
        countFailure(failures, res.term);
    }
}

test "store lock: commits, tags and user-table writes racing on one store all land" {
    // Four writers of three different kinds on one store at once: commits from
    // two worktrees, tags, and user-table merges. Every write must survive,
    // not merely leave a log that verifies. An unserialised append writes over
    // another writer's frames with frames that are themselves valid, so
    // `verify` alone cannot tell a clean race from a lost write; counting what
    // landed can.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "main", .default_dir);
    try tmp.dir.createDir(io, "wt", .default_dir);
    var main_dir = try tmp.dir.openDir(io, "main", .{});
    defer main_dir.close(io);
    var wt_dir = try tmp.dir.openDir(io, "wt", .{});
    defer wt_dir.close(io);
    try initRepo(gpa, io, main_dir);
    try writeFile(io, main_dir, "seed", "seed");
    try addManaged(gpa, io, main_dir, &.{"seed"});
    const seed = try commitHashDup(gpa, io, main_dir, "seed");
    defer gpa.free(seed);
    var opened = try runZit(gpa, io, wt_dir, &.{ "open", "--store=../main" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);

    const per_writer = 5;
    var failures = std.atomic.Value(u32).init(0);
    var a = try std.Thread.spawn(.{}, worktreeStorm, .{ io, main_dir, "main-file", per_writer, &failures });
    var b = try std.Thread.spawn(.{}, worktreeStorm, .{ io, wt_dir, "wt-file", per_writer, &failures });
    var c = try std.Thread.spawn(.{}, tagStorm, .{ io, main_dir, seed, per_writer, &failures });
    var d = try std.Thread.spawn(.{}, userStorm, .{ io, wt_dir, per_writer, &failures });
    a.join();
    b.join();
    c.join();
    d.join();
    try std.testing.expectEqual(@as(u32, 0), failures.load(.monotonic));

    var timeline = try runZit(gpa, io, main_dir, &.{ "timeline", "-n", "200" });
    defer freeResult(gpa, &timeline);
    try expectExitDiag(timeline.term, 0, timeline);
    // Counted by comment alone: the checkout's own check-in carries a
    // `*CURRENT*` marker between the hash and the comment.
    const main_landed = std.mem.count(u8, timeline.stdout, " main-file-");
    const wt_landed = std.mem.count(u8, timeline.stdout, " wt-file-");
    if (main_landed != per_writer or wt_landed != per_writer) std.debug.print("\ntimeline:\n{s}\n", .{timeline.stdout});
    try std.testing.expectEqual(@as(usize, per_writer), main_landed);
    try std.testing.expectEqual(@as(usize, per_writer), wt_landed);

    var tags = try runZit(gpa, io, main_dir, &.{ "tag", "list" });
    defer freeResult(gpa, &tags);
    try expectExitDiag(tags.term, 0, tags);
    try std.testing.expectEqual(@as(usize, per_writer), std.mem.count(u8, tags.stdout, "storm-tag-"));

    var users = try runZit(gpa, io, main_dir, &.{"users"});
    defer freeResult(gpa, &users);
    try expectExitDiag(users.term, 0, users);
    try std.testing.expectEqual(@as(usize, per_writer), std.mem.count(u8, users.stdout, "storm-user-"));

    var ver = try runZit(gpa, io, main_dir, &.{"verify"});
    defer freeResult(gpa, &ver);
    try expectExitDiag(ver.term, 0, ver);
    try std.testing.expect(std.mem.indexOf(u8, ver.stdout, "0 faults") != null);
}

fn worktreeStorm(io: std.Io, dir: std.Io.Dir, tag: []const u8, count: usize, failures: *std.atomic.Value(u32)) void {
    const gpa = std.heap.page_allocator;
    var i: usize = 0;
    while (i < count) : (i += 1) {
        var name_buf: [64]u8 = undefined;
        const name = std.fmt.bufPrint(&name_buf, "{s}-{d}", .{ tag, i }) catch unreachable;
        writeFile(io, dir, name, name) catch {
            _ = failures.fetchAdd(1, .monotonic);
            return;
        };
        var add = runZit(gpa, io, dir, &.{ "add", name }) catch {
            _ = failures.fetchAdd(1, .monotonic);
            return;
        };
        freeResult(gpa, &add);
        var res = runCommitEnv(gpa, io, dir, .{ .user = "storm" }, &.{ "commit", "-m", name, "--allow-fork", "--no-sync", "-q" }) catch {
            _ = failures.fetchAdd(1, .monotonic);
            return;
        };
        defer freeResult(gpa, &res);
        switch (res.term) {
            .exited => |c| if (c != 0) {
                _ = failures.fetchAdd(1, .monotonic);
            },
            else => _ = failures.fetchAdd(1, .monotonic),
        }
    }
}

test "worktrees: concurrent commits from two worktrees serialize on the store lock, verify clean" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "main", .default_dir);
    try tmp.dir.createDir(io, "wt", .default_dir);
    var main_dir = try tmp.dir.openDir(io, "main", .{});
    defer main_dir.close(io);
    var wt_dir = try tmp.dir.openDir(io, "wt", .{});
    defer wt_dir.close(io);

    try initRepo(gpa, io, main_dir);
    try writeFile(io, main_dir, "seed", "seed");
    try addManaged(gpa, io, main_dir, &.{"seed"});
    var c0 = try runCommitEnv(gpa, io, main_dir, .{ .user = "alice" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c0);
    try expectExit(c0.term, 0);
    var opened = try runZit(gpa, io, wt_dir, &.{ "open", "--store=../main" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);

    var failures = std.atomic.Value(u32).init(0);
    const per_side = 5;
    var thread_a = try std.Thread.spawn(.{}, worktreeStorm, .{ io, main_dir, "main-file", per_side, &failures });
    var thread_b = try std.Thread.spawn(.{}, worktreeStorm, .{ io, wt_dir, "wt-file", per_side, &failures });
    thread_a.join();
    thread_b.join();
    try std.testing.expectEqual(@as(u32, 0), failures.load(.monotonic));

    // The claim under test: ten racing appends, zero torn frames. verify
    // reconstructs every artifact through its delta chain and re-hashes it
    // against its name, so a torn log cannot pass.
    var ver = try runZit(gpa, io, main_dir, &.{"verify"});
    defer freeResult(gpa, &ver);
    try expectExit(ver.term, 0);
    var ver_wt = try runZit(gpa, io, wt_dir, &.{"verify"});
    defer freeResult(gpa, &ver_wt);
    try expectExit(ver_wt.term, 0);
}

test "worktrees: a commit blocked by a held store lock waits and then succeeds" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "blocked", "blocked");
    try addManaged(gpa, io, tmp.dir, &.{"blocked"});

    // Hold the store's write lock the way any competing writer would, release
    // it from a timer thread, and demand the commit spawned meanwhile lands
    // AFTER the release instead of failing or tearing.
    var appender = try cli.openStoreAppend(io, tmp.dir);
    const Releaser = struct {
        fn run(io_inner: std.Io, held: *cli.StoreAppend) void {
            std.Io.sleep(io_inner, .fromMilliseconds(400), .awake) catch {};
            held.close(io_inner);
        }
    };
    var releaser = try std.Thread.spawn(.{}, Releaser.run, .{ io, &appender });
    var res = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice" }, &.{ "commit", "-m", "waited" });
    defer freeResult(gpa, &res);
    releaser.join();
    try expectExit(res.term, 0);
}

/// An editor that stands in for a person writing a comment: it says it has
/// started, then waits until the test releases it (at most 30s), then writes
/// the comment. Runs with the checkout root as cwd.
const slow_editor =
    "#!/bin/sh\n" ++
    ": > in-editor\n" ++
    "i=0; while [ ! -e release ] && [ $i -lt 300 ]; do sleep 0.1; i=$((i+1)); done\n" ++
    "printf 'from a slow editor\\n' > \"$1\"\n";

/// Starts `zit commit --editor ./slow-ed.sh` in `dir` and returns once the
/// editor is open. Its stdout and stderr go to commit.out and commit.err.
fn startCommitInSlowEditor(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !std.process.Child {
    try writeFile(io, dir, "slow-ed.sh", slow_editor);
    try chmodFile(io, dir, "slow-ed.sh", 0o755);
    var env = std.process.Environ.Map.init(gpa);
    defer env.deinit();
    try env.put("TZ", "UTC");
    try env.put("HOME", "/zit-e2e-isolated-home-does-not-exist");
    try env.put("PATH", "/usr/bin:/bin");
    try env.put("USER", "alice");
    const cwd_abs = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd_abs);
    const abs_exe = try std.fs.path.resolve(gpa, &.{ cwd_abs, zit_exe });
    defer gpa.free(abs_exe);
    var child = try std.process.spawn(io, .{
        .argv = &.{ "/bin/sh", "-c", "exec \"$0\" commit --editor ./slow-ed.sh >commit.out 2>commit.err", abs_exe },
        .cwd = .{ .dir = dir },
        .environ_map = &env,
        .stdout = .ignore,
        .stderr = .ignore,
    });
    errdefer child.kill(io);
    var waited: u32 = 0;
    while (true) : (waited += 1) {
        if (dir.statFile(io, "in-editor", .{})) |_| break else |_| {}
        if (waited == 200) return error.EditorNeverOpened;
        io.sleep(.fromMilliseconds(50), .awake) catch {};
    }
    return child;
}

test "store lock: a commit waiting in its editor holds no lock, and other writers go ahead" {
    // The comment is composed after the tree is captured, and a person can
    // take as long as they like. Holding the writer lock through it turned
    // every other writer away after a minute: a tag, a pull, a push to a
    // server on this checkout.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var root = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);
    try writeFile(io, tmp.dir, "a.txt", "two\n");

    var child = try startCommitInSlowEditor(gpa, io, tmp.dir);
    var reaped = false;
    defer if (!reaped) child.kill(io);

    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, ".zit.lock", .{}));
    var tag = try runZit(gpa, io, tmp.dir, &.{ "tag", "add", "rel-1", "trunk" });
    defer freeResult(gpa, &tag);
    try expectExitDiag(tag.term, 0, tag);

    // The store grew under the prepared commit, but its parent is still the
    // tip, so it publishes.
    try writeFile(io, tmp.dir, "release", "");
    const term = try child.wait(io);
    reaped = true;
    const said = try readFile(gpa, io, tmp.dir, "commit.err");
    defer gpa.free(said);
    expectExit(term, 0) catch |e| {
        std.debug.print("\ncommit stderr:\n{s}\n", .{said});
        return e;
    };
    var tl = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-n", "2" });
    defer freeResult(gpa, &tl);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "from a slow editor") != null);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "rel-1") != null);
    var v = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &v);
    try expectExitDiag(v.term, 0, v);
}

test "store lock: a commit whose parent moved while its editor was open is refused, writes nothing, and keeps the comment" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "main", .default_dir);
    try tmp.dir.createDir(io, "wt", .default_dir);
    var main_dir = try tmp.dir.openDir(io, "main", .{});
    defer main_dir.close(io);
    var wt_dir = try tmp.dir.openDir(io, "wt", .{});
    defer wt_dir.close(io);

    try initRepo(gpa, io, main_dir);
    try writeFile(io, main_dir, "a.txt", "one\n");
    try addManaged(gpa, io, main_dir, &.{"a.txt"});
    var root = try runCommitEnv(gpa, io, main_dir, .{ .user = "alice" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &root);
    try expectExitDiag(root.term, 0, root);
    var opened = try runZit(gpa, io, wt_dir, &.{ "open", "--store=../main" });
    defer freeResult(gpa, &opened);
    try expectExitDiag(opened.term, 0, opened);

    try writeFile(io, main_dir, "a.txt", "two\n");
    var child = try startCommitInSlowEditor(gpa, io, main_dir);
    var reaped = false;
    defer if (!reaped) child.kill(io);

    // Another worktree of the same store lands a check-in on trunk.
    try writeFile(io, wt_dir, "c.txt", "c\n");
    try addManaged(gpa, io, wt_dir, &.{"c.txt"});
    var other = try runCommitEnv(gpa, io, wt_dir, .{ .user = "bob" }, &.{ "commit", "-m", "landed first" });
    defer freeResult(gpa, &other);
    try expectExitDiag(other.term, 0, other);

    // The prepared check-in names the old tip as its parent. Publishing it
    // would fork trunk silently, and re-parenting it would change bytes that
    // may be signed, so it is refused.
    try writeFile(io, main_dir, "release", "");
    const term = try child.wait(io);
    reaped = true;
    try expectExit(term, 1);
    const said = try readFile(gpa, io, main_dir, "commit.err");
    defer gpa.free(said);
    try std.testing.expect(std.mem.indexOf(u8, said, "not the tip of branch 'trunk'") != null);
    try std.testing.expect(std.mem.indexOf(u8, said, "your comment is kept in .zit-ci-comment") != null);
    const kept = try readFile(gpa, io, main_dir, ".zit-ci-comment");
    defer gpa.free(kept);
    try std.testing.expectEqualStrings("from a slow editor\n", kept);

    var tl = try runZit(gpa, io, main_dir, &.{ "timeline", "-n", "5" });
    defer freeResult(gpa, &tl);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "landed first") != null);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "from a slow editor") == null);
    // Nothing of the refused commit was stored: its edited blob included.
    var two_digest: [32]u8 = undefined;
    std.crypto.hash.sha3.Sha3_256.hash("two\n", &two_digest, .{});
    const two_name = std.fmt.bytesToHex(two_digest, .lower);
    try std.testing.expect(!try storeHoldsArtifact(gpa, io, main_dir, &two_name));
    var v = try runZit(gpa, io, main_dir, &.{"verify"});
    defer freeResult(gpa, &v);
    try expectExitDiag(v.term, 0, v);
}

test "worktrees re-open: a moved store is re-pointed in place, an unrelated store is refused" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "main", .default_dir);
    try tmp.dir.createDir(io, "wt", .default_dir);
    try tmp.dir.createDir(io, "other", .default_dir);
    var main_dir = try tmp.dir.openDir(io, "main", .{});
    defer main_dir.close(io);
    var wt_dir = try tmp.dir.openDir(io, "wt", .{});
    defer wt_dir.close(io);
    var other_dir = try tmp.dir.openDir(io, "other", .{});
    defer other_dir.close(io);

    try initRepo(gpa, io, main_dir);
    try writeFile(io, main_dir, "greeting", "hello\n");
    try addManaged(gpa, io, main_dir, &.{"greeting"});
    var c1 = try runCommitEnv(gpa, io, main_dir, .{ .user = "alice" }, &.{ "commit", "-m", "first" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    var opened = try runZit(gpa, io, wt_dir, &.{ "open", "--store=../main" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);

    // An unrelated repository never passes the containment gate, no matter
    // how healthy it is: re-pointing must mean "same store, new address".
    try initRepo(gpa, io, other_dir);
    var refused = try runZit(gpa, io, wt_dir, &.{ "open", "--store=../other" });
    defer freeResult(gpa, &refused);
    try expectExit(refused.term, 1);

    // The store moves; the stranded worktree holds uncommitted work; the
    // repair re-points in place and both survive.
    try writeFile(io, wt_dir, "uncommitted", "precious\n");
    try tmp.dir.rename("main", tmp.dir, "main2", io);
    var stranded = try runZit(gpa, io, wt_dir, &.{"stat"});
    defer freeResult(gpa, &stranded);
    try expectExit(stranded.term, 1);
    var repaired = try runZit(gpa, io, wt_dir, &.{ "open", "--store=../main2" });
    defer freeResult(gpa, &repaired);
    try expectExit(repaired.term, 0);
    var alive = try runZit(gpa, io, wt_dir, &.{"stat"});
    defer freeResult(gpa, &alive);
    try expectExit(alive.term, 0);
    {
        const got = try wt_dir.readFileAlloc(io, "uncommitted", gpa, .limited(64));
        defer gpa.free(got);
        try std.testing.expectEqualStrings("precious\n", got);
    }
    var c2 = try runCommitEnv(gpa, io, wt_dir, .{ .user = "bob" }, &.{ "add", "uncommitted" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    var c3 = try runCommitEnv(gpa, io, wt_dir, .{ .user = "bob" }, &.{ "commit", "-m", "rescued" });
    defer freeResult(gpa, &c3);
    try expectExit(c3.term, 0);
}

test "cli diff -R: the check-in pair works from a bare store, worktree mode still refuses" {
    // Goal: `diff --from A --to B` touches no working tree, so
    // -R serves it from a bare store (hunks and --brief both); the worktree
    // comparison keeps refusing -R with the works-in-a-checkout diagnostic.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "alpha\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});
    const c1 = try commitHashDup(gpa, io, tmp.dir, "one");
    defer gpa.free(c1);
    try writeFile(io, tmp.dir, "f.txt", "alpha\nbeta\n");
    const c2 = try commitHashDup(gpa, io, tmp.dir, "two");
    defer gpa.free(c2);

    var away = std.testing.tmpDir(.{}); // no checkout anywhere in sight
    defer away.cleanup();
    const cwd = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd);
    const store_abs = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &tmp.sub_path });
    defer gpa.free(store_abs);

    var hunks = try runZit(gpa, io, away.dir, &.{ "diff", "-R", store_abs, "--from", c1, "--to", c2 });
    defer freeResult(gpa, &hunks);
    try expectExitDiag(hunks.term, 0, hunks);
    try std.testing.expect(std.mem.indexOf(u8, hunks.stdout, "+beta") != null);

    var brief = try runZit(gpa, io, away.dir, &.{ "diff", "-R", store_abs, "--brief", "--from", c1, "--to", c2 });
    defer freeResult(gpa, &brief);
    try expectExitDiag(brief.term, 0, brief);
    try std.testing.expectEqualStrings("M f.txt\n", brief.stdout);

    // In-checkout from/to with -R agrees byte-for-byte with the plain form.
    var plain = try runZit(gpa, io, tmp.dir, &.{ "diff", "--from", c1, "--to", c2 });
    defer freeResult(gpa, &plain);
    try expectExitDiag(plain.term, 0, plain);
    try std.testing.expectEqualStrings(plain.stdout, hunks.stdout);

    // The worktree comparison refuses -R, naming the remedy.
    var wt = try runZit(gpa, io, away.dir, &.{ "diff", "-R", store_abs });
    defer freeResult(gpa, &wt);
    try expectExit(wt.term, 1);
    try std.testing.expectEqualStrings("", wt.stdout);
    try std.testing.expect(std.mem.indexOf(u8, wt.stderr, "needs --from/--to, --checkin or --branch") != null);
}

test "cli submanifest analyze -R: a bare store measures at its tip; settings actions refuse -R" {
    // Goal: the conversion pre-flight. `analyze -R` over a store with NO
    // checkout measures the tip check-in's file set and (with --replay-all)
    // replays the whole history; `enable -R` refuses naming the rule;
    // `--replay-all` outside analyze is a usage error.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, "src", .default_dir);
    try writeFile(io, tmp.dir, "src/a.zig", "a\n");
    try writeFile(io, tmp.dir, "src/b.zig", "b\n");
    try writeFile(io, tmp.dir, "README", "r\n");
    try addManaged(gpa, io, tmp.dir, &.{ "src/a.zig", "src/b.zig", "README" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    try writeFile(io, tmp.dir, "src/b.zig", "b2\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);

    // A BARE copy: the store adopted into a fresh dir, never opened.
    var bare = std.testing.tmpDir(.{});
    defer bare.cleanup();
    try initRepo(gpa, io, bare.dir);
    try adoptStore(gpa, io, tmp.dir, bare.dir);
    const cwd = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd);
    const bare_abs = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &bare.sub_path });
    defer gpa.free(bare_abs);
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();

    var an = try runZit(gpa, io, away.dir, &.{ "submanifest", "analyze", "-R", bare_abs, "--replay-all" });
    defer freeResult(gpa, &an);
    try expectExitDiag(an.term, 0, an);
    try std.testing.expect(std.mem.indexOf(u8, an.stdout, "analyzing the tip check-in's file set") != null);
    try std.testing.expect(std.mem.indexOf(u8, an.stdout, "names: 3") != null);

    var en = try runZit(gpa, io, away.dir, &.{ "submanifest", "enable", "-R", bare_abs });
    defer freeResult(gpa, &en);
    try expectExit(en.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, en.stderr, "works with 'analyze' only") != null);

    var ra = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "show", "--replay-all" });
    defer freeResult(gpa, &ra);
    try expectExit(ra.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, ra.stderr, "--replay-all is an 'analyze' option") != null);
}

test "cli import-fossil: the mode is mandatory, --compatible clones verbatim, a one-file history has nothing to convert into trees" {
    // Goal: the import-time choice surface. No mode = usage refusal that
    // names both consequences; both modes = usage refusal; --compatible over
    // the wire preserves the corpus verbatim (same artifacts as a clone);
    // --submanifest on a one-file history refuses: no k partitions one name
    // into a tree, and the diagnostic names --k and --compatible.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "alpha\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    // No mode: exit 2, the diagnostic teaches the difference.
    var nomode = try runZit(gpa, io, tmp.dir, &.{ "import-fossil", "src", "dst" });
    defer freeResult(gpa, &nomode);
    try expectExit(nomode.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, nomode.stderr, "permanent") != null);
    // Both modes: exit 2.
    var both = try runZit(gpa, io, tmp.dir, &.{ "import-fossil", "src", "dst", "--compatible", "--submanifest" });
    defer freeResult(gpa, &both);
    try expectExit(both.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, both.stderr, "exclusive") != null);

    // --compatible over the wire = the clone, corpus verbatim.
    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    var compat = try runZit(gpa, io, away.dir, &.{ "import-fossil", url, "repo", "--compatible" });
    defer freeResult(gpa, &compat);
    try expectExitDiag(compat.term, 0, compat);
    try std.testing.expect(std.mem.indexOf(u8, compat.stdout, "preserving the corpus verbatim") != null);
    var repo_dir = try away.dir.openDir(io, "repo", .{});
    defer repo_dir.close(io);
    var vf = try runZit(gpa, io, repo_dir, &.{"verify"});
    defer freeResult(gpa, &vf);
    try expectExitDiag(vf.term, 0, vf);

    // --submanifest straight from the fossil's URL: a transient
    // flat store is cloned beside DIR, converted, and removed.
    var from_url = try runZit(gpa, io, away.dir, &.{ "import-fossil", url, "converted-url", "--submanifest", "--k", "2" });
    defer freeResult(gpa, &from_url);
    try expectExitDiag(from_url.term, 0, from_url);
    try std.testing.expect(std.mem.indexOf(u8, from_url.stdout, "cloning the fossil into a transient flat store first") != null);
    try std.testing.expect(std.mem.indexOf(u8, from_url.stdout, "1 check-ins re-minted") != null);
    try std.testing.expect(std.mem.indexOf(u8, from_url.stdout, "every pair verified file-set-equivalent") != null);
    try std.testing.expect(!fileExists(io, away.dir, "converted-url.zit-tmp-flat"));
    var url_dst = try away.dir.openDir(io, "converted-url", .{});
    defer url_dst.close(io);
    var url_vf = try runZit(gpa, io, url_dst, &.{"verify"});
    defer freeResult(gpa, &url_vf);
    try expectExitDiag(url_vf.term, 0, url_vf);

    // --submanifest against a one-file history: no k partitions one name
    // into a tree, so there is nothing to convert; --k and --compatible are
    // the named remedies. No replay ran.
    var sub = try runZit(gpa, io, away.dir, &.{ "import-fossil", "repo", "converted", "--submanifest" });
    defer freeResult(gpa, &sub);
    try expectExit(sub.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, sub.stderr, "no k partitions") != null);
    try std.testing.expect(std.mem.indexOf(u8, sub.stderr, "--k") != null);
    try std.testing.expect(std.mem.indexOf(u8, sub.stderr, "--compatible") != null);
    try std.testing.expect(std.mem.indexOf(u8, sub.stdout, "gate") == null);
    // --force is gone with the gate.
    var forced = try runZit(gpa, io, away.dir, &.{ "import-fossil", "repo", "converted", "--submanifest", "--force" });
    defer freeResult(gpa, &forced);
    try expectExit(forced.term, 2);

    // --k does not belong to --compatible.
    var wrongk = try runZit(gpa, io, tmp.dir, &.{ "import-fossil", "s", "d", "--compatible", "--k", "2" });
    defer freeResult(gpa, &wrongk);
    try expectExit(wrongk.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, wrongk.stderr, "belongs to --submanifest") != null);
}

test "cli import-fossil --submanifest: a corpus converts, verifies, and opens byte-identical under a new identity" {
    // Goal: the conversion acceptance. A multi-directory two-commit source
    // with a tag converts (--k 2 pins the parameter for a small tree): the result
    // opens at the re-minted tip, verifies clean, materializes the same
    // bytes, resolves the remapped tag to the NEW hash, pins k, and carries
    // a different project code (the identity break).
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "src-repo", .default_dir);
    var src = try tmp.dir.openDir(io, "src-repo", .{});
    defer src.close(io);
    try initRepo(gpa, io, src);
    inline for (.{ "alpha", "beta", "gamma" }) |d| {
        try src.createDir(io, d, .default_dir);
        try writeFile(io, src, d ++ "/one.txt", d ++ " one\n");
        try writeFile(io, src, d ++ "/two.txt", d ++ " two\n");
    }
    var ad = try runZit(gpa, io, src, &.{ "add", "." });
    defer freeResult(gpa, &ad);
    try expectExitDiag(ad.term, 0, ad);
    var c1 = try runCommitEnv(gpa, io, src, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    try writeFile(io, src, "alpha/one.txt", "alpha one v2\n");
    var c2 = try runCommitEnv(gpa, io, src, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    var tg = try runZit(gpa, io, src, &.{ "tag", "add", "release", "tip" });
    defer freeResult(gpa, &tg);
    try expectExitDiag(tg.term, 0, tg);
    const old_tip = try checkinHash(c2.stdout);

    var conv = try runZit(gpa, io, tmp.dir, &.{ "import-fossil", "src-repo", "converted", "--submanifest", "--k", "2" });
    defer freeResult(gpa, &conv);
    try expectExitDiag(conv.term, 0, conv);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "every pair verified file-set-equivalent") != null);

    var dst = try tmp.dir.openDir(io, "converted", .{});
    defer dst.close(io);
    var op = try runZit(gpa, io, dst, &.{"open"});
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);
    var vf = try runZit(gpa, io, dst, &.{"verify"});
    defer freeResult(gpa, &vf);
    try expectExitDiag(vf.term, 0, vf);
    try std.testing.expect(std.mem.indexOf(u8, vf.stdout, "0 faults") != null);

    // Same bytes, new identity: the tag resolves to a hash that is NOT the
    // source tip's, and the changed file carries its v2 content.
    var info = try runZit(gpa, io, dst, &.{ "info", "release" });
    defer freeResult(gpa, &info);
    try expectExitDiag(info.term, 0, info);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, old_tip) == null);
    var cat = try runZit(gpa, io, dst, &.{ "cat", "alpha/one.txt" });
    defer freeResult(gpa, &cat);
    try expectExitDiag(cat.term, 0, cat);
    try std.testing.expectEqualStrings("alpha one v2\n", cat.stdout);

    // Sub-manifest settings arrived pinned; the S indicator is set.
    var show = try runZit(gpa, io, dst, &.{ "submanifest", "show" });
    defer freeResult(gpa, &show);
    try expectExitDiag(show.term, 0, show);
    try std.testing.expect(std.mem.indexOf(u8, show.stdout, "enable-submanifests: on") != null);
    try std.testing.expect(std.mem.indexOf(u8, show.stdout, "seen-submanifest:    yes") != null);

    // The provenance technote resolves the OLD tip hash to the NEW one, by
    // lookup inside the converted corpus itself (non-negotiable).
    var fr = try runZit(gpa, io, dst, &.{"frames"});
    defer freeResult(gpa, &fr);
    try expectExitDiag(fr.term, 0, fr);
    const pair_at = std.mem.indexOf(u8, fr.stdout, old_tip) orelse return error.TestUnexpectedResult;
    const line_end = std.mem.indexOfScalarPos(u8, fr.stdout, pair_at, '\n').?;
    const pair_line = fr.stdout[pair_at..line_end];
    var pf = std.mem.splitScalar(u8, pair_line, ' ');
    _ = pf.next(); // the old hash
    const mapped = pf.next() orelse return error.TestUnexpectedResult;
    var info2 = try runZit(gpa, io, dst, &.{ "info", mapped });
    defer freeResult(gpa, &info2);
    try expectExitDiag(info2.term, 0, info2);

    // Staged publication: success left no staging residue beside the
    // destination, and the refused conversion below leaves neither a
    // destination nor residue -- the rename is the only commit point.
    {
        // A dedicated iterate-permitted handle, scoped to the scan: Linux's
        // dirRead re-seeks the fd per batch, so iterating a handle owned
        // elsewhere (or after any close) is a BADF there even where macOS
        // tolerates it.
        var scan_dir = try tmp.dir.openDir(io, ".", .{ .iterate = true });
        defer scan_dir.close(io);
        var scan = scan_dir.iterate();
        while (try scan.next(io)) |entry| {
            try std.testing.expect(!std.mem.startsWith(u8, entry.name, ".zit-tmp-"));
        }
    }
    var refused = try runZit(gpa, io, tmp.dir, &.{ "import-fossil", "converted", "reconverted", "--submanifest", "--k", "2" });
    defer freeResult(gpa, &refused);
    // A submanifest source re-converts or refuses; either way, on failure
    // nothing may remain. (This source HAS S artifacts already; the census
    // counts them and conversion proceeds — assert only the residue rule.)
    if (exitOf(refused.term) != 0) {
        try std.testing.expect(tmp.dir.statFile(io, "reconverted", .{}) == error.FileNotFound);
    }
    {
        var scan_dir = try tmp.dir.openDir(io, ".", .{ .iterate = true });
        defer scan_dir.close(io);
        var scan2 = scan_dir.iterate();
        while (try scan2.next(io)) |entry| {
            try std.testing.expect(!std.mem.startsWith(u8, entry.name, ".zit-tmp-"));
        }
    }
}

fn exitOf(term: std.process.Child.Term) u8 {
    return switch (term) {
        .exited => |c| c,
        else => 255,
    };
}

test "cli submanifest convert: the converted store is delta-encoded as it is written, near its packed size" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDirPath(io, "flat/src/x");
    try tmp.dir.createDirPath(io, "flat/src/y");
    try tmp.dir.createDirPath(io, "flat/doc");
    var flat = try tmp.dir.openDir(io, "flat", .{});
    defer flat.close(io);
    try initRepo(gpa, io, flat);
    // Forty versions of three files: every blob has a same-path predecessor
    // and every manifest a parent, which is what a delta base needs.
    var text: std.ArrayList(u8) = .empty;
    defer text.deinit(gpa);
    var i: usize = 1;
    while (i <= 40) : (i += 1) {
        try text.print(gpa, "line {d} {s}\n", .{ i, "x" ** 200 });
        try writeFile(io, flat, "src/x/a.txt", text.items);
        try writeFile(io, flat, "src/y/b.txt", text.items);
        try writeFile(io, flat, "doc/c.txt", text.items);
        if (i == 1) try addManaged(gpa, io, flat, &.{ "src/x/a.txt", "src/y/b.txt", "doc/c.txt" });
        var c = try runCommitEnv(gpa, io, flat, .{ .user = "u" }, &.{ "commit", "-m", "v" });
        defer freeResult(gpa, &c);
        try expectExitDiag(c.term, 0, c);
    }
    var conv = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "convert", "flat", "sub", "--k", "1" });
    defer freeResult(gpa, &conv);
    try expectExitDiag(conv.term, 0, conv);
    var st = try runZit(gpa, io, tmp.dir, &.{ "stat", "-R", "sub" });
    defer freeResult(gpa, &st);
    try expectExitDiag(st.term, 0, st);
    const deltas = try statField(st.stdout, "deltas");
    const before = try statField(st.stdout, "store-bytes");
    // At least as many deltas as the flat source carries: the source's own
    // chains and each manifest's parent are the hints, so nothing is lost.
    var flat_st = try runZit(gpa, io, tmp.dir, &.{ "stat", "-R", "flat" });
    defer freeResult(gpa, &flat_st);
    const flat_deltas = try statField(flat_st.stdout, "deltas");
    try std.testing.expect(deltas > 0 and deltas >= flat_deltas);
    var sub_dir = try tmp.dir.openDir(io, "sub", .{});
    defer sub_dir.close(io);
    var pk = try runZit(gpa, io, sub_dir, &.{"pack"});
    defer freeResult(gpa, &pk);
    try expectExitDiag(pk.term, 0, pk);
    var st2 = try runZit(gpa, io, tmp.dir, &.{ "stat", "-R", "sub" });
    defer freeResult(gpa, &st2);
    const after = try statField(st2.stdout, "store-bytes");
    try std.testing.expect(before < after * 2); // pack trims, it does not transform
}

test "cli import-fossil --submanifest: an SSH-signed check-in converts with its signature stripped, --k pins the parameter, and the summary points at pack" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "src-repo", .default_dir);
    var src = try tmp.dir.openDir(io, "src-repo", .{});
    defer src.close(io);
    try initRepo(gpa, io, src);
    inline for (.{ "alpha", "beta" }) |d| {
        try src.createDir(io, d, .default_dir);
        try writeFile(io, src, d ++ "/one.txt", d ++ " one\n");
        try writeFile(io, src, d ++ "/two.txt", d ++ " two\n");
    }
    try addManaged(gpa, io, src, &.{ "alpha/one.txt", "alpha/two.txt", "beta/one.txt", "beta/two.txt" });
    var c1 = try runCommitEnv(gpa, io, src, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);
    try writeFile(io, src, "beta/two.txt", "beta two v2\n");
    // The SSH armor differs from the PGP one; the converter used to strip
    // only PGP, re-mint the armor lines as manifest text, and fail its own
    // verify without a word.
    var c2 = try runCommitEnv(gpa, io, src, .{ .user = "u", .pgp = ssh_stub_signer }, &.{ "commit", "-m", "two", "--sign" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);

    var conv = try runZit(gpa, io, tmp.dir, &.{ "import-fossil", "src-repo", "converted", "--submanifest", "--k", "2" });
    defer freeResult(gpa, &conv);
    try expectExitDiag(conv.term, 0, conv);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "k=2 pinned by --k") != null);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "gate") == null);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "store: flat source ") != null);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "(1 signed: signatures strip)") != null);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "1 signature(s) stripped") != null);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "every pair verified file-set-equivalent") != null);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "delta-encoded as written") != null);
    try std.testing.expect(std.mem.indexOf(u8, conv.stderr, "import-fossil: re-minted and verified check-ins (") != null);

    var dst = try tmp.dir.openDir(io, "converted", .{});
    defer dst.close(io);
    var op = try runZit(gpa, io, dst, &.{"open"});
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);
    var vf = try runZit(gpa, io, dst, &.{"verify"});
    defer freeResult(gpa, &vf);
    try expectExitDiag(vf.term, 0, vf);
    try std.testing.expect(std.mem.indexOf(u8, vf.stdout, "0 faults") != null);
    var cat = try runZit(gpa, io, dst, &.{ "cat", "beta/two.txt" });
    defer freeResult(gpa, &cat);
    try expectExitDiag(cat.term, 0, cat);
    try std.testing.expectEqualStrings("beta two v2\n", cat.stdout);
}

test "cli import-fossil --submanifest: trees built from the parent survive a rename and a perm change — the pair proof passes and the files read back" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "src-repo", .default_dir);
    var src = try tmp.dir.openDir(io, "src-repo", .{});
    defer src.close(io);
    try initRepo(gpa, io, src);
    inline for (.{ "alpha", "beta", "gamma" }) |d| {
        try src.createDir(io, d, .default_dir);
        try writeFile(io, src, d ++ "/one.txt", d ++ " one\n");
        try writeFile(io, src, d ++ "/two.txt", d ++ " two\n");
    }
    try addManaged(gpa, io, src, &.{ "alpha/one.txt", "alpha/two.txt", "beta/one.txt", "beta/two.txt", "gamma/one.txt", "gamma/two.txt" });
    var c1 = try runCommitEnv(gpa, io, src, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);
    // A rename: the F card carries an old-name token in THIS check-in only.
    var mv = try runZit(gpa, io, src, &.{ "mv", "beta/one.txt", "beta/uno.txt" });
    defer freeResult(gpa, &mv);
    try expectExitDiag(mv.term, 0, mv);
    var c2 = try runCommitEnv(gpa, io, src, .{ .user = "u" }, &.{ "commit", "-m", "rename" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);
    // The child inherits the renamed file WITHOUT the annotation, and flips a
    // perm elsewhere: both are changes a parent-based build must see.
    try chmodFile(io, src, "gamma/two.txt", 0o755);
    try writeFile(io, src, "alpha/one.txt", "alpha one v2\n");
    var c3 = try runCommitEnv(gpa, io, src, .{ .user = "u" }, &.{ "commit", "-m", "perm" });
    defer freeResult(gpa, &c3);
    try expectExitDiag(c3.term, 0, c3);
    var rm = try runZit(gpa, io, src, &.{ "rm", "--hard", "gamma/one.txt" });
    defer freeResult(gpa, &rm);
    try expectExitDiag(rm.term, 0, rm);
    var c4 = try runCommitEnv(gpa, io, src, .{ .user = "u" }, &.{ "commit", "-m", "delete" });
    defer freeResult(gpa, &c4);
    try expectExitDiag(c4.term, 0, c4);

    var conv = try runZit(gpa, io, tmp.dir, &.{ "import-fossil", "src-repo", "converted", "--submanifest", "--k", "2" });
    defer freeResult(gpa, &conv);
    try expectExitDiag(conv.term, 0, conv);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "4 check-ins re-minted") != null);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "every pair verified file-set-equivalent") != null);

    var dst = try tmp.dir.openDir(io, "converted", .{});
    defer dst.close(io);
    var op = try runZit(gpa, io, dst, &.{"open"});
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);
    var ls = try runZit(gpa, io, dst, &.{ "ls", "--long" });
    defer freeResult(gpa, &ls);
    try expectExitDiag(ls.term, 0, ls);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "beta/uno.txt") != null);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "beta/one.txt") == null);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "gamma/one.txt") == null);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, " x text gamma/two.txt") != null);
    var cat = try runZit(gpa, io, dst, &.{ "cat", "alpha/one.txt" });
    defer freeResult(gpa, &cat);
    try expectExitDiag(cat.term, 0, cat);
    try std.testing.expectEqualStrings("alpha one v2\n", cat.stdout);
}

test "cli submanifest convert: the converter under its proper name — no checkout needed, diagnostics carry the verb, enable says what it does not do" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "src-repo", .default_dir);
    var src = try tmp.dir.openDir(io, "src-repo", .{});
    defer src.close(io);
    try initRepo(gpa, io, src);
    inline for (.{ "alpha", "beta" }) |d| {
        try src.createDir(io, d, .default_dir);
        try writeFile(io, src, d ++ "/one.txt", d ++ " one\n");
        try writeFile(io, src, d ++ "/two.txt", d ++ " two\n");
    }
    try addManaged(gpa, io, src, &.{ "alpha/one.txt", "alpha/two.txt", "beta/one.txt", "beta/two.txt" });
    var c1 = try runCommitEnv(gpa, io, src, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);

    // From the parent directory, with no checkout of its own.
    var conv = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "convert", "src-repo", "converted", "--k", "2" });
    defer freeResult(gpa, &conv);
    try expectExitDiag(conv.term, 0, conv);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "k=2 pinned by --k") != null);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "1 check-ins re-minted as sub-manifest trees (k=2)") != null);
    try std.testing.expect(std.mem.indexOf(u8, conv.stdout, "store: flat source ") != null);
    try std.testing.expect(std.mem.indexOf(u8, conv.stderr, "import-fossil:") != null); // the phase lines keep the converter's name
    var dst = try tmp.dir.openDir(io, "converted", .{});
    defer dst.close(io);
    var vf = try runZit(gpa, io, dst, &.{"verify"});
    defer freeResult(gpa, &vf);
    try expectExitDiag(vf.term, 0, vf);

    // Usage: missing operands, a source that is neither URL nor store.
    var noargs = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "convert" });
    defer freeResult(gpa, &noargs);
    try expectExit(noargs.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, noargs.stderr, "zit submanifest convert: wants SRC") != null);
    var nosrc = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "convert", "nowhere", "x" });
    defer freeResult(gpa, &nosrc);
    try expectExit(nosrc.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, nosrc.stderr, "zit submanifest convert: 'nowhere' is neither a sync URL nor a zit repository") != null);

    // enable on the flat source says what it does NOT do, and names convert.
    var en = try runZit(gpa, io, src, &.{ "submanifest", "enable", "--k", "2" });
    defer freeResult(gpa, &en);
    try expectExitDiag(en.term, 0, en);
    try std.testing.expect(std.mem.indexOf(u8, en.stdout, "existing check-ins stay flat") != null);
    try std.testing.expect(std.mem.indexOf(u8, en.stdout, "zit submanifest convert SRC DEST") != null);
}

test "cli verb dispatch: prefixes, aliases, ambiguity, and a two-line unknown-verb error" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "ci", "-m", "one" }); // alias
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);

    // A prefix dispatches with its arguments intact.
    var tl = try runZit(gpa, io, tmp.dir, &.{ "tim", "-n", "1" });
    defer freeResult(gpa, &tl);
    try expectExitDiag(tl.term, 0, tl);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "one") != null);
    // `changes` is `status`.
    var ch = try runZit(gpa, io, tmp.dir, &.{"changes"});
    defer freeResult(gpa, &ch);
    try expectExitDiag(ch.term, 0, ch);
    // ...minus Fossil's checkout block, exactly as Fossil's `changes` is.
    try std.testing.expect(std.mem.indexOf(u8, ch.stdout, "repository:") == null);
    // An exact verb wins over the prefix rule: `stat` is the census.
    var st = try runZit(gpa, io, tmp.dir, &.{"stat"});
    defer freeResult(gpa, &st);
    try expectExitDiag(st.term, 0, st);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "store-bytes") != null);
    // Ambiguity names the candidates and stops.
    var amb = try runZit(gpa, io, tmp.dir, &.{ "ti", "-n", "1" });
    defer freeResult(gpa, &amb);
    try expectExit(amb.term, 2);
    try std.testing.expectEqualStrings("", amb.stdout);
    try std.testing.expect(std.mem.indexOf(u8, amb.stderr, "ambiguous command 'ti' (timeline, ticket)") != null);
    // Unknown: two lines, no suggestion for a distant token, never the verb list.
    var unk = try runZit(gpa, io, tmp.dir, &.{"florble"});
    defer freeResult(gpa, &unk);
    try expectExit(unk.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, unk.stderr, "unknown command 'florble'") != null);
    try std.testing.expect(std.mem.indexOf(u8, unk.stderr, "try 'zit help'") != null);
    try std.testing.expect(std.mem.indexOf(u8, unk.stderr, "commands:") == null);
    try std.testing.expect(std.mem.count(u8, unk.stderr, "\n") == 2);
    // A short typo suggests only at distance 1: `tmi` is not `rm`.
    var tmi = try runZit(gpa, io, tmp.dir, &.{"tmi"});
    defer freeResult(gpa, &tmi);
    try expectExit(tmi.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, tmi.stderr, "did you mean 'rm'") == null);
    // help resolves a prefix and names an ambiguity.
    var ht = try runZit(gpa, io, tmp.dir, &.{ "help", "tim" });
    defer freeResult(gpa, &ht);
    try expectExitDiag(ht.term, 0, ht);
    try std.testing.expect(std.mem.indexOf(u8, ht.stdout, "usage: zit timeline") != null);
    var hti = try runZit(gpa, io, tmp.dir, &.{ "help", "ti" });
    defer freeResult(gpa, &hti);
    try expectExit(hti.term, 2);
    try std.testing.expect(std.mem.indexOf(u8, hti.stderr, "ambiguous") != null);
}

test "cli update: the receipt names every moved file and the check-ins, -n changes nothing, --latest names the branch tip" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try writeFile(io, tmp.dir, "gone.txt", "x\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "gone.txt" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "base" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);
    const base = try checkinHash(c1.stdout);
    try writeFile(io, tmp.dir, "a.txt", "two\n");
    try writeFile(io, tmp.dir, "new.txt", "n\n");
    var rm = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "gone.txt" });
    defer freeResult(gpa, &rm);
    try expectExitDiag(rm.term, 0, rm);
    try addManaged(gpa, io, tmp.dir, &.{"new.txt"});
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "second" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);
    const second = try checkinHash(c2.stdout);

    // Dry run: the same receipt, nothing written.
    var dry = try runZit(gpa, io, tmp.dir, &.{ "update", "-n", base });
    defer freeResult(gpa, &dry);
    try expectExitDiag(dry.term, 0, dry);
    try std.testing.expectEqualStrings("", dry.stdout);
    try std.testing.expect(std.mem.indexOf(u8, dry.stderr, "UPDATE a.txt\nADD gone.txt\nREMOVE new.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, dry.stderr, "(dry run: nothing was changed)") != null);
    const nt = try readFile(gpa, io, tmp.dir, "new.txt"); // still there, untouched
    defer gpa.free(nt);
    try std.testing.expectEqualStrings("n\n", nt);
    var still = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &still);
    try std.testing.expect(std.mem.indexOf(u8, still.stdout, cli.shortHash(second)) != null);

    // The real move: per-file lines, the from/to block, the undo hint — all on stderr.
    var up = try runZit(gpa, io, tmp.dir, &.{ "update", base });
    defer freeResult(gpa, &up);
    try expectExitDiag(up.term, 0, up);
    try std.testing.expectEqualStrings("", up.stdout);
    try std.testing.expect(std.mem.indexOf(u8, up.stderr, "UPDATE a.txt\nADD gone.txt\nREMOVE new.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, up.stderr, "updated-from: " ++ "") != null);
    try std.testing.expect(std.mem.indexOf(u8, up.stderr, second[0..12]) != null);
    try std.testing.expect(std.mem.indexOf(u8, up.stderr, "updated-to:   ") != null);
    try std.testing.expect(std.mem.indexOf(u8, up.stderr, base[0..12]) != null);
    try std.testing.expect(std.mem.indexOf(u8, up.stderr, "tags:         trunk") != null);
    try std.testing.expect(std.mem.indexOf(u8, up.stderr, "comment:      base (user: u)") != null);
    try std.testing.expect(std.mem.indexOf(u8, up.stderr, "changes:      1 file modified, 1 added, 1 removed.") != null);
    try std.testing.expect(std.mem.indexOf(u8, up.stderr, "\"zit undo\" is available") != null);

    // Already there: the block says so, no per-file lines, no undo hint.
    var noop = try runZit(gpa, io, tmp.dir, &.{ "update", base });
    defer freeResult(gpa, &noop);
    try expectExitDiag(noop.term, 0, noop);
    try std.testing.expect(std.mem.indexOf(u8, noop.stderr, "changes:      none. Already up-to-date.") != null);
    try std.testing.expect(std.mem.indexOf(u8, noop.stderr, "UPDATE") == null);
    try std.testing.expect(std.mem.indexOf(u8, noop.stderr, "zit undo") == null);

    // --latest: the branch tip, by name, in place of CHECKIN; -v lists the unchanged.
    var latest = try runZit(gpa, io, tmp.dir, &.{ "update", "--latest", "-v" });
    defer freeResult(gpa, &latest);
    try expectExitDiag(latest.term, 0, latest);
    try std.testing.expect(std.mem.indexOf(u8, latest.stderr, "updated-to:   " ++ "") != null);
    try std.testing.expect(std.mem.indexOf(u8, latest.stderr, second[0..12]) != null);
    try std.testing.expect(std.mem.indexOf(u8, latest.stderr, "UPDATE a.txt") != null);
    var both = try runZit(gpa, io, tmp.dir, &.{ "update", "--latest", base });
    defer freeResult(gpa, &both);
    try expectExit(both.term, 2);

    // A carried local edit is reported as MERGE (or CONFLICT when the
    // three-way merge cannot decide); either way the receipt names it.
    try writeFile(io, tmp.dir, "a.txt", "two\nlocal\n");
    var carried = try runZit(gpa, io, tmp.dir, &.{ "update", base });
    defer freeResult(gpa, &carried);
    switch (carried.term) {
        .exited => |c| try std.testing.expect(c == 0 or c == 1),
        else => return error.TestUnexpectedExitCode,
    }
    try std.testing.expect(std.mem.indexOf(u8, carried.stderr, "MERGE a.txt") != null or std.mem.indexOf(u8, carried.stderr, "CONFLICT a.txt") != null);
    try std.testing.expect(std.mem.indexOf(u8, carried.stderr, "\"zit undo\" is available") != null);
}

test "cli sync family: every network line names the peer, no-ops share one phrase, clone prints the project code, a dead peer is named" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "base" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);
    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}", .{port});
    defer gpa.free(url);
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();

    var cl = try runZit(gpa, io, away.dir, &.{ "clone", "--no-open", url, "work" });
    defer freeResult(gpa, &cl);
    try expectExitDiag(cl.term, 0, cl);
    try std.testing.expect(std.mem.indexOf(u8, cl.stdout, "clone from http://127.0.0.1:") != null);
    try std.testing.expect(std.mem.indexOf(u8, cl.stdout, "project-code: ") != null);
    var work = try away.dir.openDir(io, "work", .{});
    defer work.close(io);
    var op = try runZit(gpa, io, work, &.{"open"});
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);

    inline for (.{ "sync", "pull", "push" }, .{ "sync with ", "pull from ", "push to " }) |verb, lead| {
        var r = try runZit(gpa, io, work, &.{verb});
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.startsWith(u8, r.stdout, lead ++ "http://127.0.0.1:"));
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, ": already in sync\n") != null);
    }
    // A transfer names the peer too, and the autosync inside commit does.
    try writeFile(io, work, "a.txt", "two\n");
    var c2 = try runCommitEnv(gpa, io, work, .{ .user = "u" }, &.{ "commit", "-m", "c2" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);
    try std.testing.expect(std.mem.indexOf(u8, c2.stderr, "push to http://127.0.0.1:") != null);
    try std.testing.expect(std.mem.indexOf(u8, c2.stderr, ": sent ") != null);
    // A dead peer: the host is named, exit 1.
    var dead = try runZit(gpa, io, work, &.{ "sync", "http://127.0.0.1:1" });
    defer freeResult(gpa, &dead);
    try expectExit(dead.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, dead.stderr, "zit sync: cannot connect to http://127.0.0.1:1") != null);
}

test "cli commit: -M reads a file, --editor composes over a template, an empty comment aborts and keeps the file, -n writes nothing, --allow-empty records" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});

    // Off a terminal, no -m is still the usage error (unchanged contract).
    var bare = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{"commit"});
    defer freeResult(gpa, &bare);
    try expectExit(bare.term, 2);

    // Dry run: the change list, nothing recorded.
    var dry = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-n" });
    defer freeResult(gpa, &dry);
    try expectExitDiag(dry.term, 0, dry);
    try std.testing.expect(std.mem.indexOf(u8, dry.stderr, "ADDED a.txt") != null);
    try std.testing.expect(std.mem.indexOf(u8, dry.stderr, "nothing was written") != null);
    var tl0 = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-n", "0" });
    defer freeResult(gpa, &tl0);
    try std.testing.expectEqualStrings("", tl0.stdout);

    // --editor: the template names the change set; the editor's text is the
    // comment. The editor runs with the checkout root as cwd, so relative
    // paths reach it.
    try writeFile(io, tmp.dir, "ed.sh", "#!/bin/sh\ncp \"$1\" seen.txt\nprintf 'from editor\\n' >> \"$1\"\n");
    try chmodFile(io, tmp.dir, "ed.sh", 0o755);
    var ed = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "--editor", "./ed.sh" });
    defer freeResult(gpa, &ed);
    try expectExitDiag(ed.term, 0, ed);
    const seen = try readFile(gpa, io, tmp.dir, "seen.txt");
    defer gpa.free(seen);
    try std.testing.expect(std.mem.indexOf(u8, seen, "# branch: trunk") != null);
    try std.testing.expect(std.mem.indexOf(u8, seen, "# ADDED   a.txt") != null);
    try std.testing.expect(!fileExists(io, tmp.dir, ".zit-ci-comment")); // retired on success
    var tl1 = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-n", "1" });
    defer freeResult(gpa, &tl1);
    try std.testing.expect(std.mem.indexOf(u8, tl1.stdout, "from editor") != null);
    try std.testing.expect(std.mem.indexOf(u8, tl1.stdout, "# branch") == null);

    // An editor that leaves the comment empty aborts; the template stays for -M.
    try writeFile(io, tmp.dir, "a.txt", "two\n");
    try writeFile(io, tmp.dir, "noop.sh", "#!/bin/sh\n:\n");
    try chmodFile(io, tmp.dir, "noop.sh", 0o755);
    var empty = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "--editor", "./noop.sh" });
    defer freeResult(gpa, &empty);
    try expectExit(empty.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, empty.stderr, "empty check-in message") != null);
    try std.testing.expect(fileExists(io, tmp.dir, ".zit-ci-comment"));

    // -M reads the comment from a file.
    try writeFile(io, tmp.dir, "msg.txt", "via file\n");
    var mf = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-M", "msg.txt" });
    defer freeResult(gpa, &mf);
    try expectExitDiag(mf.term, 0, mf);
    var tl2 = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-n", "1" });
    defer freeResult(gpa, &tl2);
    try std.testing.expect(std.mem.indexOf(u8, tl2.stdout, "via file") != null);

    // --allow-empty records a check-in with no file changes; without it, refused.
    var refused = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "nothing" });
    defer freeResult(gpa, &refused);
    try expectExit(refused.term, 1);
    var allowed = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "nothing", "--allow-empty" });
    defer freeResult(gpa, &allowed);
    try expectExitDiag(allowed.term, 0, allowed);
}

test "cli names: current/ckout, prev/previous and next resolve against the checkout; a bare store has none; 'trunk^' is 'trunk' by the §8.3 retry" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);
    const first = try checkinHash(c1.stdout);
    try writeFile(io, tmp.dir, "a.txt", "two\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);
    const second = try checkinHash(c2.stdout);

    inline for (.{ "current", "ckout" }) |n| {
        var r = try runZit(gpa, io, tmp.dir, &.{ "info", n });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.startsWith(u8, r.stdout, second));
    }
    inline for (.{ "prev", "previous" }) |n| {
        var r = try runZit(gpa, io, tmp.dir, &.{ "info", n });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.startsWith(u8, r.stdout, first));
    }
    // `next` from the older check-in is the newer one.
    var up = try runZit(gpa, io, tmp.dir, &.{ "update", first });
    defer freeResult(gpa, &up);
    try expectExitDiag(up.term, 0, up);
    var nx = try runZit(gpa, io, tmp.dir, &.{ "info", "next" });
    defer freeResult(gpa, &nx);
    try expectExitDiag(nx.term, 0, nx);
    try std.testing.expect(std.mem.startsWith(u8, nx.stdout, second));
    // A root check-in has no prev.
    var np = try runZit(gpa, io, tmp.dir, &.{ "info", "prev" });
    defer freeResult(gpa, &np);
    try expectExit(np.term, 1);
    // §8.3: one trailing punctuation character is dropped, so 'trunk^' is 'trunk'.
    var caret = try runZit(gpa, io, tmp.dir, &.{ "info", "trunk^" });
    defer freeResult(gpa, &caret);
    try expectExitDiag(caret.term, 0, caret);
    try std.testing.expect(std.mem.startsWith(u8, caret.stdout, second));
    // On a bare store the checkout-relative names have no referent.
    var bare = try runZit(gpa, io, tmp.dir, &.{ "info", "current", "-R", "." });
    defer freeResult(gpa, &bare);
    try expectExit(bare.term, 1);
}

test "cli timeline: filters by branch, current branch, author and path; -r/--offset; medium and full formats mark the current check-in and list files" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try writeFile(io, tmp.dir, "b.txt", "b\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);
    try writeFile(io, tmp.dir, "a.txt", "a2\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "bob" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);
    try writeFile(io, tmp.dir, "b.txt", "b2\n");
    var c3 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice" }, &.{ "commit", "-m", "three", "--branch", "feat" });
    defer freeResult(gpa, &c3);
    try expectExitDiag(c3.term, 0, c3);

    const Case = struct { args: []const []const u8, want: []const []const u8, not: []const []const u8 };
    const cases = [_]Case{
        .{ .args = &.{ "timeline", "-F", "oneline", "-b", "trunk" }, .want = &.{ "two", "one" }, .not = &.{"three"} },
        .{ .args = &.{ "timeline", "-F", "oneline", "-c" }, .want = &.{"three"}, .not = &.{ "two", "one" } },
        .{ .args = &.{ "timeline", "-F", "oneline", "-u", "alice" }, .want = &.{ "three", "one" }, .not = &.{"two"} },
        .{ .args = &.{ "timeline", "-F", "oneline", "-p", "b.txt" }, .want = &.{ "three", "one" }, .not = &.{"two"} },
        .{ .args = &.{ "timeline", "-F", "oneline", "-r", "--offset", "1", "-n", "1" }, .want = &.{"two"}, .not = &.{ "one", "three" } },
        .{ .args = &.{ "timeline", "-F", "medium", "-n", "1" }, .want = &.{ "=== ", "*CURRENT* three (user: alice tags: feat" }, .not = &.{"two"} },
        .{ .args = &.{ "timeline", "-F", "oneline", "--full", "-n", "1" }, .want = &.{ "*CURRENT*", "   EDITED b.txt" }, .not = &.{"a.txt"} },
        .{ .args = &.{ "timeline", "-F", "oneline", "-v", "-n", "1" }, .want = &.{ "  three", "   EDITED b.txt" }, .not = &.{"user:"} },
    };
    for (cases) |cs| {
        var r = try runZit(gpa, io, tmp.dir, cs.args);
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        for (cs.want) |w| {
            if (std.mem.indexOf(u8, r.stdout, w) == null) {
                std.debug.print("timeline {s}: missing {s} in:\n{s}\n", .{ cs.args[1], w, r.stdout });
                return error.TestUnexpectedResult;
            }
        }
        for (cs.not) |n| {
            if (std.mem.indexOf(u8, r.stdout, n) != null) {
                std.debug.print("timeline {s}: unexpected {s} in:\n{s}\n", .{ cs.args[1], n, r.stdout });
                return error.TestUnexpectedResult;
            }
        }
    }
    // The default line is unchanged: no user, no tags, no header.
    var plain = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-n", "1" });
    defer freeResult(gpa, &plain);
    try expectExitDiag(plain.term, 0, plain);
    try std.testing.expect(std.mem.indexOf(u8, plain.stdout, "user:") == null);
    try std.testing.expect(std.mem.indexOf(u8, plain.stdout, "===") == null);
}

test "cli status: --classify, kind filters, --extra/--differ, -v header, CONFLICT under a pending merge" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "one\ntwo\n");
    try writeFile(io, tmp.dir, "gone.txt", "g\n");
    try addManaged(gpa, io, tmp.dir, &.{ "f.txt", "gone.txt" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "base" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);
    try writeFile(io, tmp.dir, "f.txt", "ONE\ntwo\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "b1", "--branch", "feat" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);
    var up = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &up);
    try expectExitDiag(up.term, 0, up);
    try writeFile(io, tmp.dir, "f.txt", "uno\ntwo\n");
    var c3 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "t1" });
    defer freeResult(gpa, &c3);
    try expectExitDiag(c3.term, 0, c3);
    var mg = try runZit(gpa, io, tmp.dir, &.{ "merge", "feat" });
    defer freeResult(gpa, &mg);
    try expectExit(mg.term, 1); // conflict in f.txt
    try tmp.dir.deleteFile(io, "gone.txt");
    try writeFile(io, tmp.dir, "new.txt", "n\n");
    try addManaged(gpa, io, tmp.dir, &.{"new.txt"});
    try writeFile(io, tmp.dir, "extra.txt", "x\n");

    var plain = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &plain);
    try expectExitDiag(plain.term, 0, plain);
    try std.testing.expect(std.mem.indexOf(u8, plain.stdout, "MERGED_WITH ") != null);
    try std.testing.expect(std.mem.indexOf(u8, plain.stdout, "CONFLICT   f.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, plain.stdout, "MISSING    gone.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, plain.stdout, "ADDED      new.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, plain.stdout, "extra.txt") == null);

    var cls = try runZit(gpa, io, tmp.dir, &.{ "status", "--classify" });
    defer freeResult(gpa, &cls);
    try expectExitDiag(cls.term, 0, cls);
    try std.testing.expect(std.mem.indexOf(u8, cls.stdout, "MERGED_WITH ") != null);
    try std.testing.expect(std.mem.indexOf(u8, cls.stdout, "CONFLICT   f.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, cls.stdout, "MISSING    gone.txt\n") != null);

    var only = try runZit(gpa, io, tmp.dir, &.{ "status", "--conflict" });
    defer freeResult(gpa, &only);
    try expectExitDiag(only.term, 0, only);
    try std.testing.expect(std.mem.indexOf(u8, only.stdout, "CONFLICT   f.txt") != null);
    try std.testing.expect(std.mem.indexOf(u8, only.stdout, "gone.txt") == null);
    try std.testing.expect(std.mem.indexOf(u8, only.stdout, "new.txt") == null);

    var differ = try runZit(gpa, io, tmp.dir, &.{ "status", "--differ" });
    defer freeResult(gpa, &differ);
    try expectExitDiag(differ.term, 0, differ);
    try std.testing.expect(std.mem.indexOf(u8, differ.stdout, "EXTRA      extra.txt") != null);
    try std.testing.expect(std.mem.indexOf(u8, differ.stdout, "gone.txt") == null);

    var verbose = try runZit(gpa, io, tmp.dir, &.{ "status", "-v" });
    defer freeResult(gpa, &verbose);
    try expectExitDiag(verbose.term, 0, verbose);
    try std.testing.expect(std.mem.indexOf(u8, verbose.stdout, "repository:   ") != null);
    try std.testing.expect(std.mem.indexOf(u8, verbose.stdout, "checkout:     ") != null);
    try std.testing.expect(std.mem.indexOf(u8, verbose.stdout, "tags:         trunk") != null);
    try std.testing.expect(std.mem.indexOf(u8, verbose.stdout, "comment:      t1 (user: u)") != null);
}

test "cli -R: pull, push, sync and remote work on a bare store with no working tree" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "base" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);
    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}", .{port});
    defer gpa.free(url);
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    var cl = try runZit(gpa, io, away.dir, &.{ "clone", url, "bare" });
    defer freeResult(gpa, &cl);
    try expectExitDiag(cl.term, 0, cl);
    // No `open`: the store has no working tree. Every sync verb still works by -R.
    var rem = try runZit(gpa, io, away.dir, &.{ "remote", "-R", "bare" });
    defer freeResult(gpa, &rem);
    try expectExitDiag(rem.term, 0, rem);
    try std.testing.expect(std.mem.startsWith(u8, rem.stdout, "http://127.0.0.1:"));
    inline for (.{ "pull", "push", "sync" }) |verb| {
        var r = try runZit(gpa, io, away.dir, &.{ verb, "-R", "bare" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expect(std.mem.indexOf(u8, r.stdout, ": already in sync") != null);
    }
    // And without -R, the same directory is not a checkout.
    var no = try runZit(gpa, io, away.dir, &.{"pull"});
    defer freeResult(gpa, &no);
    try expectExit(no.term, 1);
}

test "cli branch: new/current/info, close/reopen, hide/unhide, and the -c/-t/-r list flags" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);

    var nw = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "branch", "new", "feat" });
    defer freeResult(gpa, &nw);
    try expectExitDiag(nw.term, 0, nw);
    try std.testing.expect(std.mem.indexOf(u8, nw.stdout, " on feat") != null);
    var curb = try runZit(gpa, io, tmp.dir, &.{ "branch", "current" });
    defer freeResult(gpa, &curb);
    try expectExitDiag(curb.term, 0, curb);
    try std.testing.expectEqualStrings("feat\n", curb.stdout);
    var info = try runZit(gpa, io, tmp.dir, &.{ "branch", "info", "feat" });
    defer freeResult(gpa, &info);
    try expectExitDiag(info.term, 0, info);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "branch:    feat\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, info.stdout, "check-ins: 1\n") != null);

    var cl = try runZit(gpa, io, tmp.dir, &.{ "branch", "close", "feat" });
    defer freeResult(gpa, &cl);
    try expectExitDiag(cl.term, 0, cl);
    var closed = try runZit(gpa, io, tmp.dir, &.{ "branch", "-c" });
    defer freeResult(gpa, &closed);
    try expectExitDiag(closed.term, 0, closed);
    try std.testing.expect(std.mem.indexOf(u8, closed.stdout, "feat  (closed)") != null);
    try std.testing.expect(std.mem.indexOf(u8, closed.stdout, "trunk") == null);
    var ro = try runZit(gpa, io, tmp.dir, &.{ "branch", "reopen", "feat" });
    defer freeResult(gpa, &ro);
    try expectExitDiag(ro.term, 0, ro);
    var none = try runZit(gpa, io, tmp.dir, &.{ "branch", "-c" });
    defer freeResult(gpa, &none);
    try expectExitDiag(none.term, 0, none);
    try std.testing.expectEqualStrings("", none.stdout);

    var hd = try runZit(gpa, io, tmp.dir, &.{ "branch", "hide", "feat" });
    defer freeResult(gpa, &hd);
    try expectExitDiag(hd.term, 0, hd);
    var raw = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "--raw", "tip" });
    defer freeResult(gpa, &raw);
    try expectExitDiag(raw.term, 0, raw);
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "hidden") != null);
    var uh = try runZit(gpa, io, tmp.dir, &.{ "branch", "unhide", "feat" });
    defer freeResult(gpa, &uh);
    try expectExitDiag(uh.term, 0, uh);

    // -t orders by recency (feat is newest), -r reverses.
    var bt = try runZit(gpa, io, tmp.dir, &.{ "branch", "-t" });
    defer freeResult(gpa, &bt);
    try expectExitDiag(bt.term, 0, bt);
    try std.testing.expect(std.mem.startsWith(u8, bt.stdout, "* feat"));
    var br = try runZit(gpa, io, tmp.dir, &.{ "branch", "-t", "-r" });
    defer freeResult(gpa, &br);
    try expectExitDiag(br.term, 0, br);
    try std.testing.expect(std.mem.startsWith(u8, br.stdout, "  trunk"));
    // A BASIS that is not the checkout is refused with the remedy.
    var bad = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "branch", "new", "other", "trunk" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, bad.stderr, "run 'zit update trunk' first") != null);
}

test "cli import-fossil --compatible: the imported corpus is byte-exact, artifact for artifact" {
    // Goal: the round-trip prerequisite. A compatible import carries every
    // artifact byte-exact (same names, same bytes, same project identity),
    // so a later fossil export reproduces the origin exactly. Method: frames
    // streams both corpora in full and the outputs must agree artifact for
    // artifact (persisted order is append order on both sides of one clone,
    // so the streams are directly comparable after their trailers drop).
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, "src", .default_dir);
    try writeFile(io, tmp.dir, "src/a.zig", "alpha\n");
    try writeFile(io, tmp.dir, "README", "readme\n");
    try addManaged(gpa, io, tmp.dir, &.{ "src/a.zig", "README" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    try writeFile(io, tmp.dir, "src/a.zig", "alpha v2\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);
    var tg = try runZit(gpa, io, tmp.dir, &.{ "tag", "add", "v1", "tip" });
    defer freeResult(gpa, &tg);
    try expectExitDiag(tg.term, 0, tg);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    var compat = try runZit(gpa, io, away.dir, &.{ "import-fossil", url, "repo", "--compatible" });
    defer freeResult(gpa, &compat);
    try expectExitDiag(compat.term, 0, compat);
    var repo_dir = try away.dir.openDir(io, "repo", .{});
    defer repo_dir.close(io);

    // The byte census, both sides: every artifact header and payload must
    // agree. The trailer lines differ (cursor tokens are store-layout
    // facts), so they are stripped before the comparison.
    var origin_frames = try runZit(gpa, io, tmp.dir, &.{"frames"});
    defer freeResult(gpa, &origin_frames);
    try expectExitDiag(origin_frames.term, 0, origin_frames);
    var import_frames = try runZit(gpa, io, repo_dir, &.{"frames"});
    defer freeResult(gpa, &import_frames);
    try expectExitDiag(import_frames.term, 0, import_frames);
    const origin_body = origin_frames.stdout[0..std.mem.lastIndexOf(u8, origin_frames.stdout, "cursor ").?];
    const import_body = import_frames.stdout[0..std.mem.lastIndexOf(u8, import_frames.stdout, "cursor ").?];
    try std.testing.expectEqualStrings(origin_body, import_body);

    // Same project identity: the two stores are one sync world.
    var pull = try runZit(gpa, io, repo_dir, &.{"pull"});
    defer freeResult(gpa, &pull);
    try expectExitDiag(pull.term, 0, pull);
}

test "D card: every minted artifact carries millisecond precision; SOURCE_DATE_EPOCH pins .000" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f", "x");
    try addManaged(gpa, io, tmp.dir, &.{"f"});

    // Deterministic run: the fraction is part of the canonical form and a
    // pinned epoch pins it to .000, so reproducible builds stay reproducible.
    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice", .epoch = "1234567890" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c);
    try expectExit(c.term, 0);
    const hash = try checkinHash(c.stdout);
    var art = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", hash });
    defer freeResult(gpa, &art);
    try expectExit(art.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, art.stdout, "D 2009-02-13T23:31:30.000\n") != null);

    // A live-clock wiki artifact carries the same 23-byte writer form — the
    // millisecond fix reaching the machine verbs whose consumer surfaced the bug.
    try writeFile(io, tmp.dir, "body.txt", "b");
    var w = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{ "wiki", "commit", "T", "--user", "quarry" });
    defer freeResult(gpa, &w);
    try expectExit(w.term, 0);
    var wa = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", w.stdout[0..64] });
    defer freeResult(gpa, &wa);
    try expectExit(wa.term, 0);
    // A wiki artifact's D card is its FIRST line (D < L < N < U < W).
    const d_at = if (std.mem.startsWith(u8, wa.stdout, "D ")) @as(usize, 2) else std.mem.indexOf(u8, wa.stdout, "\nD ").? + 3;
    const d_end = std.mem.indexOfScalarPos(u8, wa.stdout, d_at, '\n').?;
    try std.testing.expectEqual(@as(usize, 23), d_end - d_at); // YYYY-MM-DDTHH:MM:SS.SSS
    try std.testing.expectEqual(@as(u8, '.'), wa.stdout[d_at + 19]);
}

test "cli technote: --comment mints the C card and --tag mints a sorted sym T run" {
    // Goal: the cards fossil writes and zit could not. Authoring the same note
    // in fossil 2.28 with --technote-tags "alpha beta" yields exactly
    // `C <title>` plus `T +sym-alpha *` / `T +sym-beta *`, so that is the
    // shape pinned here — including the ascending order §4.5.2 requires,
    // which is why the tags go in reversed.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "body.txt", "note body\n");

    var t = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{
        "technote",  "add",               "--user", "quarry",
        "--comment", "My Technote Title", "--tag",  "beta",
        "--tag",     "alpha",
    });
    defer freeResult(gpa, &t);
    try expectExit(t.term, 0);
    try std.testing.expectEqualStrings("", t.stderr);
    // stdout is "<event-id> <artifact-hash>"; the hash is the machine result.
    const space = std.mem.indexOfScalar(u8, t.stdout, ' ').?;
    const hash = t.stdout[space + 1 ..][0..64];

    var art = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{ "artifact", "--raw", hash });
    defer freeResult(gpa, &art);
    try expectExit(art.term, 0);
    // The C card carries the title with §4.3 escapes, so spaces are `\s`.
    try std.testing.expect(std.mem.indexOf(u8, art.stdout, "C My\\sTechnote\\sTitle\n") != null);
    // Self-referential `*` target, singleton `+`, sym- prefixed, ascending.
    const alpha = std.mem.indexOf(u8, art.stdout, "T +sym-alpha *\n").?;
    const beta = std.mem.indexOf(u8, art.stdout, "T +sym-beta *\n").?;
    try std.testing.expect(alpha < beta);

    // Omitting them stays exactly as before: no C card, no T run.
    var plain = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{ "technote", "add", "--user", "quarry" });
    defer freeResult(gpa, &plain);
    try expectExit(plain.term, 0);
    const p_space = std.mem.indexOfScalar(u8, plain.stdout, ' ').?;
    var p_art = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{ "artifact", "--raw", plain.stdout[p_space + 1 ..][0..64] });
    defer freeResult(gpa, &p_art);
    try expectExit(p_art.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, p_art.stdout, "\nC ") == null);
    try std.testing.expect(!std.mem.startsWith(u8, p_art.stdout, "C "));
    try std.testing.expect(std.mem.indexOf(u8, p_art.stdout, "\nT ") == null);
}

test "cli technote: an empty --comment, a repeated --tag, and a whitespace tag are refused" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "body.txt", "note body\n");

    // The C card is unattachable after the fact, so an empty title would be
    // permanent; a repeated tag would mint a non-ascending run the artifact's
    // own admission check rejects; whitespace could not survive fossil's
    // space-separated tag field.
    const cases = [_][]const []const u8{
        &.{ "technote", "add", "--user", "quarry", "--comment", "" },
        &.{ "technote", "add", "--user", "quarry", "--tag", "x", "--tag", "x" },
        &.{ "technote", "add", "--user", "quarry", "--tag", "a b" },
        &.{ "technote", "add", "--user", "quarry", "--tag", "" },
    };
    for (cases) |args| {
        var res = try runZitStdin(gpa, io, tmp.dir, "body.txt", args);
        defer freeResult(gpa, &res);
        try expectNonzeroExit(res.term);
        try std.testing.expectEqualStrings("", res.stdout);
        try std.testing.expect(res.stderr.len != 0);
    }
}

test "cli forum: --mimetype mints the N card on both post and reply" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "body.txt", "post body\n");

    var post = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{
        "forum", "post", "--user", "quarry", "--title", "Thread", "--mimetype", "text/x-markdown",
    });
    defer freeResult(gpa, &post);
    try expectExit(post.term, 0);
    const post_hash = post.stdout[0..64];

    var post_art = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{ "artifact", "--raw", post_hash });
    defer freeResult(gpa, &post_art);
    try std.testing.expect(std.mem.indexOf(u8, post_art.stdout, "N text/x-markdown\n") != null);

    // A reply declares its own mimetype: it is a separate artifact with its
    // own body, and nothing propagates the thread root's N card to it.
    var reply = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{
        "forum", "reply", post_hash, "--user", "quarry", "--mimetype", "text/plain",
    });
    defer freeResult(gpa, &reply);
    try expectExit(reply.term, 0);

    var reply_art = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{ "artifact", "--raw", reply.stdout[0..64] });
    defer freeResult(gpa, &reply_art);
    try std.testing.expect(std.mem.indexOf(u8, reply_art.stdout, "N text/plain\n") != null);

    // Omitted, no N card is minted — the reader applies its own default.
    var bare = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{ "forum", "post", "--user", "quarry", "--title", "T2" });
    defer freeResult(gpa, &bare);
    try expectExit(bare.term, 0);
    var bare_art = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{ "artifact", "--raw", bare.stdout[0..64] });
    defer freeResult(gpa, &bare_art);
    try std.testing.expect(std.mem.indexOf(u8, bare_art.stdout, "\nN ") == null);
}

test "cli worktrees: trees stamp at open/update, listings agree from every tree, dead trees prune lazily" {
    // Goal: the store-side registry lifecycle. A co-located tree and a
    // detached tree both stamp; `worktrees` from either lists both with the
    // invoking tree starred; deleting a tree with plain rm prunes it from
    // the next listing; bare -R answers from nowhere without a marker.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "main-tree", .default_dir);
    var main_tree = try tmp.dir.openDir(io, "main-tree", .{});
    defer main_tree.close(io);
    try initRepo(gpa, io, main_tree);
    try writeFile(io, main_tree, "f.txt", "x\n");
    try addManaged(gpa, io, main_tree, &.{"f.txt"});
    var c1 = try runCommitEnv(gpa, io, main_tree, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    var up = try runZit(gpa, io, main_tree, &.{ "update", "tip" });
    defer freeResult(gpa, &up);
    try expectExitDiag(up.term, 0, up); // the stamp rides update

    try tmp.dir.createDir(io, "tree-b", .default_dir);
    var tree_b = try tmp.dir.openDir(io, "tree-b", .{});
    defer tree_b.close(io);
    var det = try runZit(gpa, io, tree_b, &.{ "open", "--store=../main-tree" });
    defer freeResult(gpa, &det);
    try expectExitDiag(det.term, 0, det);

    // Both trees listed from the detached side; the invoker is starred.
    var from_b = try runZit(gpa, io, tree_b, &.{"worktrees"});
    defer freeResult(gpa, &from_b);
    try expectExitDiag(from_b.term, 0, from_b);
    try std.testing.expect(std.mem.indexOf(u8, from_b.stdout, "main-tree") != null);
    try std.testing.expect(std.mem.indexOf(u8, from_b.stdout, "* ") != null);
    try std.testing.expect(std.mem.indexOf(u8, from_b.stdout, "tree-b") != null);

    // ... and from the co-located side.
    var from_main = try runZit(gpa, io, main_tree, &.{"worktrees"});
    defer freeResult(gpa, &from_main);
    try expectExitDiag(from_main.term, 0, from_main);
    try std.testing.expect(std.mem.indexOf(u8, from_main.stdout, "tree-b") != null);

    // A tree removed with plain rm prunes from the next listing.
    try tmp.dir.deleteTree(io, "tree-b");
    var pruned = try runZit(gpa, io, main_tree, &.{"worktrees"});
    defer freeResult(gpa, &pruned);
    try expectExitDiag(pruned.term, 0, pruned);
    try std.testing.expect(std.mem.indexOf(u8, pruned.stdout, "tree-b") == null);
    try std.testing.expect(std.mem.indexOf(u8, pruned.stdout, "main-tree") != null);

    // Bare -R from an unrelated cwd: the listing answers, nothing starred.
    const cwd = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd);
    const store_abs = try std.fs.path.join(gpa, &.{ cwd, ".zig-cache", "tmp", &tmp.sub_path, "main-tree" });
    defer gpa.free(store_abs);
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    var bare = try runZit(gpa, io, away.dir, &.{ "worktrees", "-R", store_abs });
    defer freeResult(gpa, &bare);
    try expectExitDiag(bare.term, 0, bare);
    try std.testing.expect(std.mem.indexOf(u8, bare.stdout, "main-tree") != null);
    try std.testing.expect(std.mem.indexOf(u8, bare.stdout, "* ") == null);
}

/// Deterministic stand-ins for the signer, honoring FOSSIL's calling
/// convention (`COMMAND OUTFILE INFILE`), which is what zit invokes: the
/// stub's own `$0`/`$1` are those two paths. No gpg, no keyring, no expiry.
const stub_signer = "sh -c 'printf -- \"-----BEGIN PGP SIGNED MESSAGE-----\\nHash: SHA256\\n\\n\" > \"$0\"; " ++
    "cat \"$1\" >> \"$0\"; printf -- \"-----BEGIN PGP SIGNATURE-----\\nstubsig\\n-----END PGP SIGNATURE-----\\n\" >> \"$0\"'";

/// A signer that MANGLES: same armor, but it rewrites a card on the way
/// through, so the wrapped text no longer strips back to what was signed.
const mangling_signer = "sh -c 'printf -- \"-----BEGIN PGP SIGNED MESSAGE-----\\nHash: SHA256\\n\\n\" > \"$0\"; " ++
    "sed \"s/^C /C x/\" \"$1\" >> \"$0\"; printf -- \"-----BEGIN PGP SIGNATURE-----\\nstubsig\\n-----END PGP SIGNATURE-----\\n\" >> \"$0\"'";

/// The SSH family: fossil detects a `ssh*` basename, passes only INFILE, and
/// expects the DETACHED signature at INFILE.sig — this stub writes exactly
/// that, so the armor assembly under test is zit's, as it is in fossil.
const ssh_stub_signer = "ssh_stub=1 sh -c 'printf -- \"-----BEGIN SSH SIGNATURE-----\\nstubsig\\n-----END SSH SIGNATURE-----\\n\" > \"$0.sig\"'";

test "PGP: commit --sign stores the wrapped manifest, cards still read, a mangling signer is refused" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "hello\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});

    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice", .pgp = stub_signer }, &.{ "commit", "-m", "signed", "--sign" });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);
    const hash = try checkinHash(c.stdout);

    // The STORED artifact is the wrapped text — its name hashes the wrapper.
    var raw = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", hash });
    defer freeResult(gpa, &raw);
    try expectExit(raw.term, 0);
    try std.testing.expect(std.mem.startsWith(u8, raw.stdout, "-----BEGIN PGP SIGNED MESSAGE-----\n"));
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "-----BEGIN PGP SIGNATURE-----") != null);

    // And every reader still sees a manifest: the summary classifies it,
    // reports the signature, and the Z card verifies over the stripped body.
    var sum = try runZit(gpa, io, tmp.dir, &.{ "artifact", hash });
    defer freeResult(gpa, &sum);
    try expectExit(sum.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, sum.stdout, "type: check-in") != null or
        std.mem.indexOf(u8, sum.stdout, "type: manifest") != null);
    try std.testing.expect(std.mem.indexOf(u8, sum.stdout, "signature: clearsigned") != null);
    try std.testing.expect(std.mem.indexOf(u8, sum.stdout, "Z: verified") != null);

    // Derived state is unaffected: the check-in appears, resolves, and its
    // file is materializable — the wrapper never reaches the card layer.
    var tl = try runZit(gpa, io, tmp.dir, &.{"timeline"});
    defer freeResult(gpa, &tl);
    try expectExit(tl.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, hash[0..12]) != null);
    var ls = try runZit(gpa, io, tmp.dir, &.{ "ls", hash });
    defer freeResult(gpa, &ls);
    try expectExit(ls.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "f.txt") != null);

    // A signer whose output does not strip back to the manifest it was given
    // is refused: signing bytes the reader will not interpret is worse than
    // not signing.
    try writeFile(io, tmp.dir, "f.txt", "second\n");
    var bad = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice", .pgp = mangling_signer }, &.{ "commit", "-m", "mangled", "--sign" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, bad.stderr, "--sign failed") != null);
    // Refused means nothing landed: the tip is still the signed check-in.
    var tl2 = try runZit(gpa, io, tmp.dir, &.{"timeline"});
    defer freeResult(gpa, &tl2);
    try expectExit(tl2.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, tl2.stdout, "mangled") == null);
}

test "PGP carry: a signed artifact survives pack and a clone byte-exact" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "carry\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});
    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice", .pgp = stub_signer }, &.{ "commit", "-m", "signed", "--sign" });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);
    const hash = try checkinHash(c.stdout);

    var before = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", hash });
    defer freeResult(gpa, &before);
    try expectExit(before.term, 0);

    // Pack rewrites the whole generation (re-deltas, recompresses). The
    // artifact's bytes are its identity, so they must come back identical.
    var packed_res = try runZit(gpa, io, tmp.dir, &.{"pack"});
    defer freeResult(gpa, &packed_res);
    try expectExit(packed_res.term, 0);
    var after = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", hash });
    defer freeResult(gpa, &after);
    try expectExit(after.term, 0);
    try std.testing.expectEqualStrings(before.stdout, after.stdout);
    // Belt and braces: verify re-hashes every artifact against its name, so
    // a wrapper lost or altered anywhere in the pack is a fault here.
    var ver = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &ver);
    try expectExit(ver.term, 0);
}

test "PGP carry: a signed manifest crosses the sync wire byte-exact (clone over HTTP)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Repo A holds a SIGNED check-in. The wrapper is part of the artifact's
    // bytes and therefore of its name, so a wire path that dropped or
    // rewrote it could not even produce the same hash — this test asserts
    // the bytes, which is the stronger statement.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "hello.txt", "signed over http\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"hello.txt"});
    var commit = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "alice", .pgp = stub_signer }, &.{ "commit", "-m", "signed", "--sign" });
    defer freeResult(gpa, &commit);
    try expectExitDiag(commit.term, 0, commit);
    const hash = try checkinHash(commit.stdout);

    var origin = try runZit(gpa, io, tmp_a.dir, &.{ "artifact", "--raw", hash });
    defer freeResult(gpa, &origin);
    try expectExit(origin.term, 0);
    try std.testing.expect(std.mem.startsWith(u8, origin.stdout, "-----BEGIN PGP SIGNED MESSAGE-----\n"));

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var url_buf: [64]u8 = undefined;
    const url = try std.fmt.bufPrint(&url_buf, "http://127.0.0.1:{d}/", .{port});
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);

    var cloned_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer cloned_dir.close(io);
    var landed = try runZit(gpa, io, cloned_dir, &.{ "artifact", "--raw", hash });
    defer freeResult(gpa, &landed);
    try expectExit(landed.term, 0);
    try std.testing.expectEqualStrings(origin.stdout, landed.stdout);
}

test "PGP: the ssh signer family gets fossil's detached handling and assembled armor" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "ssh signed\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});

    // A command whose basename starts with `ssh` is the detached case: zit
    // passes only the input file, the signer writes INFILE.sig, and zit
    // assembles the armor — fossil's layout, since 2025 ssh still cannot
    // produce a non-detached signature itself.
    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice", .pgp = ssh_stub_signer }, &.{ "commit", "-m", "ssh-signed", "--sign" });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);
    const hash = try checkinHash(c.stdout);

    var raw = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", hash });
    defer freeResult(gpa, &raw);
    try expectExit(raw.term, 0);
    // Fossil's exact assembly: armor line, ONE blank line (no `Hash:`
    // header, unlike the gpg path), the manifest, then the detached sig.
    try std.testing.expect(std.mem.startsWith(u8, raw.stdout, "-----BEGIN SSH SIGNED MESSAGE-----\n\n"));
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "-----BEGIN SSH SIGNATURE-----") != null);

    // And it still reads as a check-in through the stripper.
    var sum = try runZit(gpa, io, tmp.dir, &.{ "artifact", hash });
    defer freeResult(gpa, &sum);
    try expectExit(sum.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, sum.stdout, "signature: clearsigned") != null);
    try std.testing.expect(std.mem.indexOf(u8, sum.stdout, "Z: verified") != null);
}

test "PGP: a false-ish pgp-command means do not sign (fossil's is_false), and the commit is refused rather than silently unsigned" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "x\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});

    // `off` is fossil's disable spelling. Asking for --sign with signing
    // disabled is a contradiction the operator must see: zit refuses rather
    // than committing an unsigned check-in that was asked to be signed.
    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice", .pgp = "off" }, &.{ "commit", "-m", "nope", "--sign" });
    defer freeResult(gpa, &c);
    try expectExit(c.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, c.stderr, "--sign failed") != null);
}

test "settings: clearsign and pgp-command persist locally, drive commit without --sign, and the environment still wins" {
    // PGP left two open questions; settings settled them with fossil's own answer:
    // `clearsign` is a plain local boolean in no config group, `pgp-command` is
    // SENSITIVE and propagates nowhere. Both therefore live in the repo-local
    // config frame — which is what this exercises, through the real binary and
    // a real second process reading the frame back.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f.txt", "x\n");
    try addManaged(gpa, io, tmp.dir, &.{"f.txt"});

    // Defaults first, so the changes below are attributable.
    var bare = try runZit(gpa, io, tmp.dir, &.{"settings"});
    defer freeResult(gpa, &bare);
    try expectExitDiag(bare.term, 0, bare);
    try std.testing.expect(std.mem.indexOf(u8, bare.stdout, "clearsign off\n") != null);
    // Unset now displays the DEFAULT in force, not an empty value: the listing
    // answers "what is this setting" rather than "what has this repository
    // stored", which is the whole point of resolving across tiers.
    try std.testing.expect(std.mem.indexOf(u8, bare.stdout, "pgp-command gpg --clearsign -o \n") != null);

    var set_cmd = try runZit(gpa, io, tmp.dir, &.{ "settings", "pgp-command", stub_signer });
    defer freeResult(gpa, &set_cmd);
    try expectExitDiag(set_cmd.term, 0, set_cmd);
    var set_flag = try runZit(gpa, io, tmp.dir, &.{ "settings", "clearsign", "on" });
    defer freeResult(gpa, &set_flag);
    try expectExitDiag(set_flag.term, 0, set_flag);

    // A FRESH process reads the v4 config frame back: this is the storage
    // round trip, not an in-memory one.
    var back = try runZit(gpa, io, tmp.dir, &.{"settings"});
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);
    try std.testing.expect(std.mem.indexOf(u8, back.stdout, "clearsign on\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, back.stdout, stub_signer) != null);

    // The point of `clearsign`: no --sign on the command line, signed anyway.
    // `.pgp = null` leaves $ZIT_PGP_COMMAND unset, so the stored setting is the
    // only thing that can be supplying the signer.
    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice", .pgp = null }, &.{ "commit", "-m", "by setting" });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);
    const hash = try checkinHash(c.stdout);
    var art = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", hash });
    defer freeResult(gpa, &art);
    try expectExitDiag(art.term, 0, art);
    try std.testing.expect(std.mem.startsWith(u8, art.stdout, "-----BEGIN PGP SIGNED MESSAGE-----"));
    // §4.1.1 still reaches the cards through the wrapper: the summary names the
    // signature AND lists the cards it read from the stripped body.
    var summary = try runZit(gpa, io, tmp.dir, &.{ "artifact", hash });
    defer freeResult(gpa, &summary);
    try expectExitDiag(summary.term, 0, summary);
    try std.testing.expect(std.mem.indexOf(u8, summary.stdout, "signature: clearsigned") != null);
    try std.testing.expect(std.mem.indexOf(u8, summary.stdout, "C by setting") != null);

    // Precedence: the environment overrides the stored command, so an operator
    // can redirect one invocation without editing the repository. `off` is
    // fossil's disable spelling, and disabling while `clearsign` demands a
    // signature is the contradiction that must be refused, not silently
    // committed unsigned.
    try writeFile(io, tmp.dir, "f.txt", "y\n");
    var over = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice", .pgp = "off" }, &.{ "commit", "-m", "env wins" });
    defer freeResult(gpa, &over);
    try expectExit(over.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, over.stderr, "--sign failed") != null);

    // Turning it back off restores the unsigned default.
    var off = try runZit(gpa, io, tmp.dir, &.{ "settings", "clearsign", "off" });
    defer freeResult(gpa, &off);
    try expectExitDiag(off.term, 0, off);
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice", .pgp = null }, &.{ "commit", "-m", "plain" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);
    const hash2 = try checkinHash(c2.stdout);
    var art2 = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", hash2 });
    defer freeResult(gpa, &art2);
    try expectExitDiag(art2.term, 0, art2);
    try std.testing.expect(!std.mem.startsWith(u8, art2.stdout, "-----BEGIN"));

    // A boolean setting's vocabulary is fixed and documented, so a bad value is
    // a usage error (2), not a failure (1).
    var bad = try runZit(gpa, io, tmp.dir, &.{ "settings", "clearsign", "yes" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 2);
}

test "settings: the global tier drives commits, and a repository value overrides it" {
    // The cross-surface agreement, pinned because it was briefly WRONG: the
    // listing resolved across tiers while the commit path still read the config
    // frame directly, so `zit settings` reported `clearsign on` from the global
    // file and the commit quietly did not sign. A settings display that
    // disagrees with the behaviour it describes is worse than no global tier.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try initRepo(gpa, io, repo);
    try writeFile(io, repo, "f.txt", "x\n");
    try addManaged(gpa, io, repo, &.{"f.txt"});

    // Relative to the child's cwd (the repo), so it lands beside it in the
    // test's tmp tree rather than inside the repository -- and, more to the
    // point, nowhere near the operator's real settings file.
    const cfg = "../zit-global-settings";
    const env: []const [2][]const u8 = &.{.{ "ZIT_CONFIG", cfg }};

    var set_flag = try runZitEnv(gpa, io, repo, env, &.{ "settings", "--global", "clearsign", "on" });
    defer freeResult(gpa, &set_flag);
    try expectExitDiag(set_flag.term, 0, set_flag);
    var set_cmd = try runZitEnv(gpa, io, repo, env, &.{ "settings", "--global", "pgp-command", stub_signer });
    defer freeResult(gpa, &set_cmd);
    try expectExitDiag(set_cmd.term, 0, set_cmd);

    // The listing reports the global tier as the source.
    var listed = try runZitEnv(gpa, io, repo, env, &.{ "settings", "clearsign" });
    defer freeResult(gpa, &listed);
    try expectExitDiag(listed.term, 0, listed);
    try std.testing.expectEqualStrings("clearsign on\n", listed.stdout);
    try std.testing.expect(std.mem.indexOf(u8, listed.stderr, "global tier") != null);

    // Nothing is stored in the repository, so a signed commit can only mean the
    // commit path consulted the same global tier the listing just reported.
    var c = try runCommitEnv(gpa, io, repo, .{ .user = "alice", .pgp = null, .zit_config = cfg }, &.{ "commit", "-m", "from global" });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);
    const hash = try checkinHash(c.stdout);
    var art = try runZit(gpa, io, repo, &.{ "artifact", "--raw", hash });
    defer freeResult(gpa, &art);
    try expectExitDiag(art.term, 0, art);
    try std.testing.expect(std.mem.startsWith(u8, art.stdout, "-----BEGIN PGP SIGNED MESSAGE-----"));

    // A repository value shadows the global one -- and does so for a BOOLEAN
    // whose stored value equals its own default, which is only representable
    // because the config frame records an explicit-set bit beside the value.
    var off = try runZitEnv(gpa, io, repo, env, &.{ "settings", "clearsign", "off" });
    defer freeResult(gpa, &off);
    try expectExitDiag(off.term, 0, off);
    try writeFile(io, repo, "f.txt", "y\n");
    var c2 = try runCommitEnv(gpa, io, repo, .{ .user = "alice", .pgp = null, .zit_config = cfg }, &.{ "commit", "-m", "repo off" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);
    const hash2 = try checkinHash(c2.stdout);
    var art2 = try runZit(gpa, io, repo, &.{ "artifact", "--raw", hash2 });
    defer freeResult(gpa, &art2);
    try expectExitDiag(art2.term, 0, art2);
    try std.testing.expect(!std.mem.startsWith(u8, art2.stdout, "-----BEGIN"));
}

test "cli commit: content policy is opt-in, grandfathers what it inherits, and refuses what a commit introduces" {
    // Goal: the half of content policy that a before-commit hook cannot do —
    // a rule that binds a fresh clone with nothing trusted, because it
    // executes nothing. Three properties, in the order they bite an adopter:
    // undeclared means unchecked; declaring does not retroactively break a
    // tree that already violates; what the commit INTRODUCES is refused.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // A repository that never declared anything commits binary and CR/LF
    // exactly as before — which is why adding this feature broke nothing.
    try writeFile(io, tmp.dir, "seed.txt", "plain\n");
    try writeFile(io, tmp.dir, "legacy.bin", "a\x00b\n");
    try addManaged(gpa, io, tmp.dir, &.{ "seed.txt", "legacy.bin" });
    var seed = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &seed);
    try expectExitDiag(seed.term, 0, seed);

    // Declaring the policy as a TRACKED file — no `zit settings`, no trust —
    // succeeds even though legacy.bin already violates it. Adoption in a real
    // repository has to be possible without first fixing the whole tree.
    try tmp.dir.createDir(io, ".zit-settings", .default_dir);
    try writeFile(io, tmp.dir, ".zit-settings/binary-glob", "*.ok\n");
    try addManaged(gpa, io, tmp.dir, &.{".zit-settings/binary-glob"});
    var declare = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "declare" });
    defer freeResult(gpa, &declare);
    try expectExitDiag(declare.term, 0, declare);

    // A NEW violating file is refused, naming the file, the glob that would
    // permit it, and the one-off waiver.
    try writeFile(io, tmp.dir, "fresh.bin", "a\x00b\n");
    try addManaged(gpa, io, tmp.dir, &.{"fresh.bin"});
    var refused = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "fresh" });
    defer freeResult(gpa, &refused);
    try expectNonzeroExit(refused.term);
    for ([_][]const u8{ "fresh.bin", "binary-glob", "--no-checks" }) |needle| {
        try std.testing.expect(std.mem.indexOf(u8, refused.stderr, needle) != null);
    }

    // Nothing was minted, and the refusal is sticky while the file stays
    // tracked — a policy that forgot its objection after one try is no policy.
    var again = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "fresh" });
    defer freeResult(gpa, &again);
    try expectNonzeroExit(again.term);
    var tl = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-n", "9" });
    defer freeResult(gpa, &tl);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "declare") != null);
    try std.testing.expect(std.mem.indexOf(u8, tl.stdout, "fresh") == null);

    // Clear the pending violation first: the refusal names the FIRST offender
    // and stops, so leaving fresh.bin tracked would mask what the next arm is
    // actually testing.
    var untrack = try runZit(gpa, io, tmp.dir, &.{ "rm", "fresh.bin" });
    defer freeResult(gpa, &untrack);
    try expectExit(untrack.term, 0);

    // Grandfathering is per-content, not per-path: editing the inherited file
    // re-checks it, so nothing stays permanently exempt.
    try writeFile(io, tmp.dir, "legacy.bin", "a\x00b\x00c\n");
    var edited = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "edit" });
    defer freeResult(gpa, &edited);
    try expectNonzeroExit(edited.term);
    try std.testing.expect(std.mem.indexOf(u8, edited.stderr, "legacy.bin") != null);

    // --no-checks waives the whole check for one commit.
    var waived = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "waived", "--no-checks" });
    defer freeResult(gpa, &waived);
    try expectExitDiag(waived.term, 0, waived);
}

test "cli ls: --long reports the permission token, size and content class; --under bounds the listing" {
    // Goal: the three facts a file listing needs. The permission token is the
    // one that cannot be recovered by spending more — it lives in the F card,
    // and the blob it names does not know it is executable — so it is the
    // assertion that matters most here.
    if (comptime !File.Permissions.has_executable_bit) return error.SkipZigTest;
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "readme.md", "plain text\n");
    try tmp.dir.createDir(io, "tools", .default_dir);
    try writeFile(io, tmp.dir, "tools/run.sh", "#!/bin/sh\necho hi\n");
    try tmp.dir.createDir(io, "src", .default_dir);
    try writeFile(io, tmp.dir, "src/blob.bin", "a\x00b\n");
    try chmodFile(io, tmp.dir, "tools/run.sh", 0o755);
    try addManaged(gpa, io, tmp.dir, &.{ "readme.md", "tools/run.sh", "src/blob.bin" });
    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);

    // The default listing is untouched: two fields, no content read.
    var plain = try runZit(gpa, io, tmp.dir, &.{"ls"});
    defer freeResult(gpa, &plain);
    try expectExit(plain.term, 0);
    var plain_lines = std.mem.tokenizeScalar(u8, plain.stdout, '\n');
    while (plain_lines.next()) |line| {
        var f = std.mem.tokenizeScalar(u8, line, ' ');
        _ = f.next(); // hash
        _ = f.next(); // filename
        try std.testing.expect(f.next() == null); // and nothing else
    }

    var long = try runZit(gpa, io, tmp.dir, &.{ "ls", "--long" });
    defer freeResult(gpa, &long);
    try expectExitDiag(long.term, 0, long);

    // The executable carries its token; the others carry the '-' placeholder.
    try std.testing.expect(std.mem.indexOf(u8, long.stdout, " x text tools/run.sh") != null);
    try std.testing.expect(std.mem.indexOf(u8, long.stdout, " - text readme.md") != null);
    // A NUL in the content classifies as binary, not text.
    try std.testing.expect(std.mem.indexOf(u8, long.stdout, " - binary src/blob.bin") != null);
    // Size is measured, not guessed: "plain text\n" is 11 bytes.
    try std.testing.expect(std.mem.indexOf(u8, long.stdout, " 11 - text readme.md") != null);

    // --under bounds the listing to one directory, on a segment boundary.
    var under = try runZit(gpa, io, tmp.dir, &.{ "ls", "--long", "--under=src" });
    defer freeResult(gpa, &under);
    try expectExit(under.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, under.stdout, "src/blob.bin") != null);
    try std.testing.expect(std.mem.indexOf(u8, under.stdout, "readme.md") == null);
    try std.testing.expect(std.mem.indexOf(u8, under.stdout, "tools/run.sh") == null);
}

test "cli log: --long answers a file-history page in one invocation" {
    // `log CHECKIN PATH` already selects the check-ins that changed PATH; what
    // was missing is their facts, which cost a subprocess per row otherwise.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try writeFile(io, tmp.dir, "b.txt", "untouched\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "alice" }, &.{ "commit", "-m", "first commit" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);

    try writeFile(io, tmp.dir, "a.txt", "two\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "bob" }, &.{ "commit", "-m", "second commit" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);

    var res = try runZit(gpa, io, tmp.dir, &.{ "log", "--long", "tip", "a.txt" });
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 0, res);

    // Both authors and both comments arrive without a second invocation.
    for ([_][]const u8{ "alice", "bob", "first commit", "second commit" }) |needle| {
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, needle) != null);
    }
    // The path filter still holds: b.txt changed only in the first commit.
    var only_b = try runZit(gpa, io, tmp.dir, &.{ "log", "--long", "tip", "b.txt" });
    defer freeResult(gpa, &only_b);
    try expectExit(only_b.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, only_b.stdout, "second commit") == null);

    // Default output is unchanged — two fields, no metadata.
    var plain = try runZit(gpa, io, tmp.dir, &.{ "log", "tip", "a.txt" });
    defer freeResult(gpa, &plain);
    try expectExit(plain.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, plain.stdout, "alice") == null);
}

test "cli grep/annotate: -i -c -l -v --once shape grep's report; annotate -l lists versions and -n bounds the look-back" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "f", "Alpha\nbeta\nALPHA two\n");
    try writeFile(io, tmp.dir, "g", "gamma\n");
    try addManaged(gpa, io, tmp.dir, &.{ "f", "g" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c1" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    try writeFile(io, tmp.dir, "f", "Alpha\nbeta\nALPHA two\ndelta\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "c2" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);

    var ci = try runZit(gpa, io, tmp.dir, &.{ "grep", "-i", "alpha", "f" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    // Both versions of f match twice; grep reports every version.
    try std.testing.expectEqual(@as(usize, 4), std.mem.count(u8, ci.stdout, "\n"));
    try std.testing.expect(std.mem.indexOf(u8, ci.stdout, "f:3: ALPHA two") != null);
    var cnt = try runZit(gpa, io, tmp.dir, &.{ "grep", "-i", "-c", "alpha", "f", "g" });
    defer freeResult(gpa, &cnt);
    try expectExitDiag(cnt.term, 0, cnt);
    try std.testing.expect(std.mem.indexOf(u8, cnt.stdout, " f:2\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, cnt.stdout, " g:0\n") != null);
    var names = try runZit(gpa, io, tmp.dir, &.{ "grep", "-i", "-l", "alpha", "f", "g" });
    defer freeResult(gpa, &names);
    try expectExitDiag(names.term, 0, names);
    try std.testing.expect(std.mem.endsWith(u8, names.stdout, " f\n"));
    try std.testing.expect(std.mem.indexOf(u8, names.stdout, " g") == null);
    var inv = try runZit(gpa, io, tmp.dir, &.{ "grep", "-v", "-i", "alpha", "f" });
    defer freeResult(gpa, &inv);
    try expectExitDiag(inv.term, 0, inv);
    try std.testing.expect(std.mem.indexOf(u8, inv.stdout, "f:2: beta") != null);
    try std.testing.expect(std.mem.indexOf(u8, inv.stdout, "Alpha") == null);
    var once = try runZit(gpa, io, tmp.dir, &.{ "grep", "-i", "--once", "alpha", "f" });
    defer freeResult(gpa, &once);
    try expectExitDiag(once.term, 0, once);
    try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, once.stdout, "\n")); // one line per version

    var log = try runZit(gpa, io, tmp.dir, &.{ "annotate", "-l", "f" });
    defer freeResult(gpa, &log);
    try expectExitDiag(log.term, 0, log);
    try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, log.stdout, "version "));
    try std.testing.expect(std.mem.indexOf(u8, log.stdout, "\n---\n") != null);
    // -n 1 sees only c2, so every line is credited to it.
    var lim = try runZit(gpa, io, tmp.dir, &.{ "annotate", "-n", "1", "f" });
    defer freeResult(gpa, &lim);
    try expectExitDiag(lim.term, 0, lim);
    const c2_hash = try checkinHash(c2.stdout);
    var lines = std.mem.splitScalar(u8, lim.stdout, '\n');
    while (lines.next()) |line| {
        if (line.len == 0) continue;
        try std.testing.expect(std.mem.startsWith(u8, line, c2_hash[0..10]));
    }
    var bad = try runZit(gpa, io, tmp.dir, &.{ "annotate", "-n", "x", "f" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 2);
}

test "cli grep: a string added then removed is still found in the versions that carried it" {
    // The question this verb exists for: "when did this exist, and where?" —
    // unanswerable in zit before, because the bytes are not on disk. The
    // security-relevant shape is the one driven here: a secret committed and
    // later removed is still in history and still findable.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "cfg.txt", "alpha\nSECRET_TOKEN=1\ngamma\n");
    try writeFile(io, tmp.dir, "other.txt", "nothing to see\n");
    try addManaged(gpa, io, tmp.dir, &.{ "cfg.txt", "other.txt" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "add secret" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);

    try writeFile(io, tmp.dir, "cfg.txt", "alpha\nstill here SECRET_TOKEN\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "keep" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);

    try writeFile(io, tmp.dir, "cfg.txt", "alpha\nremoved\n");
    var c3 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "remove" });
    defer freeResult(gpa, &c3);
    try expectExitDiag(c3.term, 0, c3);

    var hit = try runZit(gpa, io, tmp.dir, &.{ "grep", "SECRET_TOKEN", "cfg.txt" });
    defer freeResult(gpa, &hit);
    try expectExit(hit.term, 0); // 0 means found — the shell convention

    // Both carrying versions are reported, with their line numbers; the
    // version that removed it is not.
    try std.testing.expect(std.mem.indexOf(u8, hit.stdout, "cfg.txt:2: SECRET_TOKEN=1") != null);
    try std.testing.expect(std.mem.indexOf(u8, hit.stdout, "cfg.txt:2: still here SECRET_TOKEN") != null);
    try std.testing.expect(std.mem.indexOf(u8, hit.stdout, "removed") == null);

    // A run of check-ins that did not touch the file is ONE version, not one
    // report per check-in: other.txt was unchanged across all three.
    var once = try runZit(gpa, io, tmp.dir, &.{ "grep", "nothing", "other.txt" });
    defer freeResult(gpa, &once);
    try expectExit(once.term, 0);
    var lines: usize = 0;
    var it = std.mem.tokenizeScalar(u8, once.stdout, '\n');
    while (it.next()) |_| lines += 1;
    try std.testing.expectEqual(@as(usize, 1), lines);

    // Absent string: exit 1, nothing on stdout — distinguishable from a failure
    // to search, which exits 1 with a diagnostic.
    var miss = try runZit(gpa, io, tmp.dir, &.{ "grep", "NEVER_PRESENT", "cfg.txt" });
    defer freeResult(gpa, &miss);
    try expectNonzeroExit(miss.term);
    try std.testing.expectEqualStrings("", miss.stdout);

    // Operand errors are usage errors, not empty results.
    var no_path = try runZit(gpa, io, tmp.dir, &.{ "grep", "SECRET_TOKEN" });
    defer freeResult(gpa, &no_path);
    try expectExit(no_path.term, 2);
}

test "cli grep -S: the pickaxe reports the check-in that added a string and the one that removed it, and nothing between" {
    // Slice 2's acceptance criterion, and the line that separates the
    // pickaxe from slice 1: a string that sat unchanged through intervening
    // check-ins is reported for EVERY version by `grep` and for exactly TWO
    // check-ins by `grep -S`. A `-S` that reported the quiet middle would be
    // answering the question plain grep already answers.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "cfg.txt", "alpha\nbeta\n");
    try addManaged(gpa, io, tmp.dir, &.{"cfg.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);

    try writeFile(io, tmp.dir, "cfg.txt", "alpha\nSECRET_TOKEN=1\nbeta\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "add secret" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);

    // The quiet middle: the file changes, the string does not move.
    try writeFile(io, tmp.dir, "cfg.txt", "alpha\nSECRET_TOKEN=1\nbeta\nextra\n");
    var c3 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "unrelated edit" });
    defer freeResult(gpa, &c3);
    try expectExitDiag(c3.term, 0, c3);

    try writeFile(io, tmp.dir, "cfg.txt", "alpha\nbeta\nextra\n");
    var c4 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "remove secret" });
    defer freeResult(gpa, &c4);
    try expectExitDiag(c4.term, 0, c4);

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "grep", "-S", "SECRET_TOKEN" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        var lines = std.mem.tokenizeScalar(u8, res.stdout, '\n');
        var n: usize = 0;
        while (lines.next()) |_| n += 1;
        try std.testing.expectEqual(@as(usize, 2), n);
        // Newest first, so the removal leads and the introduction follows;
        // the signs are what make the two lines distinguishable.
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "-1 cfg.txt") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "+1 cfg.txt") != null);
        try std.testing.expect(std.mem.startsWith(u8, res.stdout[13..], "-1"));
    }
    {
        // Deleting a whole file removes its occurrences too — the half of
        // "when did it go" that a walk over only the child's files misses,
        // because the path is not there to be compared.
        try writeFile(io, tmp.dir, "doomed.txt", "carries SECRET_TOKEN here\n");
        try addManaged(gpa, io, tmp.dir, &.{"doomed.txt"});
        var add = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "add doomed" });
        defer freeResult(gpa, &add);
        try expectExitDiag(add.term, 0, add);

        var rm = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "doomed.txt" });
        defer freeResult(gpa, &rm);
        try expectExitDiag(rm.term, 0, rm);
        var del = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "delete doomed" });
        defer freeResult(gpa, &del);
        try expectExitDiag(del.term, 0, del);

        var res = try runZit(gpa, io, tmp.dir, &.{ "grep", "-S", "SECRET_TOKEN", "doomed.txt" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "+1 doomed.txt") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "-1 doomed.txt") != null);
        // Narrowed to one path: cfg.txt's two hits are not in this answer.
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "cfg.txt") == null);
    }
    {
        // Nothing matched is exit 1 with empty stdout, distinguishable from a
        // failure to search — the contract the issue asked for.
        var res = try runZit(gpa, io, tmp.dir, &.{ "grep", "-S", "NEVER_PRESENT_ANYWHERE" });
        defer freeResult(gpa, &res);
        try expectNonzeroExit(res.term);
        try std.testing.expectEqualStrings("", res.stdout);
    }
    {
        // Unlike plain grep, -S needs no PATH: demanding one would be
        // demanding the answer to "where did this appear".
        var res = try runZit(gpa, io, tmp.dir, &.{ "grep", "-S" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2); // ...but PATTERN is still required
    }
}

test "cli grep -G: a changed line that mentions the pattern is a hit even when the count did not move" {
    // WHY BOTH PICKAXES EXIST. Renaming `x = SECRET` to `y = SECRET` leaves
    // exactly one occurrence before and after, so -S is silent — correctly, by
    // its own definition — while the line plainly changed. If -G did not
    // report it, -G would be a slower -S.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "a.txt", "x = SECRET\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "introduce" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);

    try writeFile(io, tmp.dir, "a.txt", "y = SECRET\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "rename, count unchanged" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);

    // -S sees one event: the introduction. The rename moved no count.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "grep", "-S", "SECRET" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        var lines = std.mem.tokenizeScalar(u8, res.stdout, '\n');
        var n: usize = 0;
        while (lines.next()) |_| n += 1;
        try std.testing.expectEqual(@as(usize, 1), n);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "+1 a.txt") != null);
    }
    // -G sees two: the introduction, and the rename whose removed and added
    // lines both mention it.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "grep", "-G", "SECRET" });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        var lines = std.mem.tokenizeScalar(u8, res.stdout, '\n');
        var n: usize = 0;
        while (lines.next()) |_| n += 1;
        try std.testing.expectEqual(@as(usize, 2), n);
        // '~' marks a -G line, so the two modes are distinguishable by a
        // consumer that did not see the command line.
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "~2 a.txt") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "~1 a.txt") != null);
    }
    {
        // A pattern that only matches the diff's own file header must not
        // report anything: `+++ a.txt` is not a changed line.
        var res = try runZit(gpa, io, tmp.dir, &.{ "grep", "-G", "a.txt" });
        defer freeResult(gpa, &res);
        try expectNonzeroExit(res.term);
        try std.testing.expectEqualStrings("", res.stdout);
    }
    {
        // The two pickaxes ask different questions of one walk; combining them
        // is refused rather than letting one silently win.
        var res = try runZit(gpa, io, tmp.dir, &.{ "grep", "-S", "-G", "SECRET" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 2);
    }
}

test "cli precedent: a resolution is remembered at commit and replayed on the same conflict" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nY\nC\n", "A\nX\nC\n");
    defer fx.deinit(gpa);

    // 1. The conflict, and the pending precedent it records.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "precedent", "list" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        // Named as pending, not shown as an empty resolution.
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "poem  pending") != null);
    }

    // 2. Resolve by hand and commit: the resolution is learned only here, from
    //    bytes that passed the same unresolved-marker gate the check-in did.
    try writeFile(io, tmp.dir, "poem", "A\nRESOLVED\nC\n");
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "commit", "-m", "resolve", "--user", "t" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "remembered 1 conflict resolution") != null);
    }

    // 3. Reproduce the identical conflict: the resolution is replayed, the
    //    merge is clean, and the replay is NAMED rather than merely counted.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "update", fx.current });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0); // no longer a conflict
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "poem resolved from precedent") != null);
    }
    const poem = try readFile(gpa, io, tmp.dir, "poem");
    defer gpa.free(poem);
    try std.testing.expectEqualStrings("A\nRESOLVED\nC\n", poem);
    // The replay wrote a resolution, not markers.
    try std.testing.expect(std.mem.indexOf(u8, poem, "<<<<<<< ") == null);
}

test "cli precedent: --no-precedent declines replay without forgetting; forget restores the conflict" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nY\nC\n", "A\nX\nC\n");
    defer fx.deinit(gpa);

    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
    }
    try writeFile(io, tmp.dir, "poem", "A\nRESOLVED\nC\n");
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "commit", "-m", "resolve", "--user", "t" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "update", fx.current });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    // Declining behaves as it did before the feature existed: a conflict.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other, "--no-precedent" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "resolved from precedent") == null);
    }
    // Declining is not forgetting: the precedent is still there.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "precedent", "list" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "poem") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "pending") == null);
    }
    // Forgetting does forget: the next such merge conflicts normally again.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"undo"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "precedent", "forget", "--all" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "resolved from precedent") == null);
    }
}

test "cli precedent: the sidecar is never an extra, so it cannot become an artifact" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nY\nC\n", "A\nX\nC\n");
    defer fx.deinit(gpa);
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1); // writes .zit-precedent
    }
    var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // A reserved name: invisible to `extras`, so `addremove` can never sweep it
    // into the tracked set and no check-in can carry it.
    try std.testing.expect(std.mem.indexOf(u8, res.stdout, ".zit-precedent") == null);
}

test "cli stash: the sidecar is a reserved name, so stashed work cannot be committed" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "f.txt", "v1\n");
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "add", "f.txt" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "commit", "-m", "base", "--user", "t" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    try writeFile(io, tmp.dir, "f.txt", "v2\n");
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "stash", "-m", "wip" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }

    // `.zit-stash` carries the full contents of work deliberately set aside.
    // Before it was reserved, `extras` listed it and `add` accepted it, so an
    // ordinary `add`/`addremove` could put that content in a check-in and sync
    // it to every peer — recoverable only by shunning.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, ".zit-stash") == null);
    }
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "add", ".zit-stash" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "control file") != null);
    }
    // Reserving the name must not disturb the verb that owns it.
    {
        var res = try runZit(gpa, io, tmp.dir, &.{ "stash", "apply" });
        defer freeResult(gpa, &res);
        try expectExit(res.term, 0);
    }
    const f = try readFile(gpa, io, tmp.dir, "f.txt");
    defer gpa.free(f);
    try std.testing.expectEqualStrings("v2\n", f);
}

// ==========================================================================
// The reference's FLAG-level pin.
//
// `src/reference_test.zig` pins that every verb has a section. That is not
// enough, and the gap was not hypothetical: an audit of all 57 verbs found
// `zit pack --drop-elided` — the sparse spec's narrow-then-compact deletion,
// destructive and irreversible — present in the parser, doing real work, and
// named in NO help output at all. The help-topic guard could not see it either, since
// that one checks options-table flags against the usage block and this flag
// was in neither. A verb can be documented while its flags are not.
//
// So this holds each verb's real `--help` against its reference section, both
// directions. It lives in the e2e lane because it needs the help TEXT, which
// only the spawned binary produces.
// ==========================================================================

const reference_doc = @embedFile("reference_doc");

/// The shared conventions block: everything before the first group heading.
/// `-R` and `--no-cache` are documented once there rather than on each of the
/// dozen-plus verbs that accept them, so a flag found there counts as covered.
fn conventionsBlock() []const u8 {
    const end = std.mem.indexOf(u8, reference_doc, "<h2 class=\"grp-head\"") orelse reference_doc.len;
    return reference_doc[0..end];
}

/// One verb's `<section class="cmd" id="...">` body, or null when absent.
fn referenceSection(id: []const u8, buf: []u8) ?[]const u8 {
    const open = std.fmt.bufPrint(buf, "<section class=\"cmd\" id=\"{s}\">", .{id}) catch return null;
    const at = std.mem.indexOf(u8, reference_doc, open) orelse return null;
    const from = at + open.len;
    const to = std.mem.indexOfPos(u8, reference_doc, from, "</section>") orelse return null;
    return reference_doc[from..to];
}

/// A flag token starts at a `-` that is not preceded by a word character, and
/// runs while the bytes stay flag-shaped. Rejects `--` and bare `-`.
fn flagAt(s: []const u8, i: usize) ?[]const u8 {
    if (s[i] != '-') return null;
    if (i > 0 and (std.ascii.isAlphanumeric(s[i - 1]) or s[i - 1] == '-' or s[i - 1] == '_')) return null;
    var j = i;
    while (j < s.len and s[j] == '-') j += 1;
    if (j - i > 2 or j == s.len or !std.ascii.isAlphabetic(s[j])) return null;
    while (j < s.len and (std.ascii.isAlphanumeric(s[j]) or s[j] == '-')) j += 1;
    return s[i..j];
}

fn mentionsFlag(hay: []const u8, flag: []const u8) bool {
    var i: usize = 0;
    while (std.mem.indexOfPos(u8, hay, i, flag)) |hit| {
        i = hit + 1;
        const after = hit + flag.len;
        // A whole token: `--cone` must not satisfy a search for `--cone-not`'s
        // prefix, and vice versa.
        if (after < hay.len and (std.ascii.isAlphanumeric(hay[after]) or hay[after] == '-')) continue;
        if (hit > 0 and (std.ascii.isAlphanumeric(hay[hit - 1]) or hay[hit - 1] == '-')) continue;
        return true;
    }
    return false;
}

test "cli reference: every flag a verb accepts is documented in its section" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const conv = conventionsBlock();
    var missing: usize = 0;
    inline for (comptime std.enums.values(cli.Verb)) |v| {
        if (v != .unknown and v != .help and v != .version) {
            var id_buf: [64]u8 = undefined;
            const tag = @tagName(v);
            @memcpy(id_buf[0..tag.len], tag);
            for (id_buf[0..tag.len]) |*c| {
                if (c.* == '_') c.* = '-';
            }
            const id = id_buf[0..tag.len];

            var res = try runZit(gpa, io, tmp.dir, &.{ id, "--help" });
            defer freeResult(gpa, &res);

            var sec_buf: [96]u8 = undefined;
            // A missing section is reference_test.zig's failure, not this one.
            const section = referenceSection(id, &sec_buf) orelse &[_]u8{};
            // Only the options table: prose legitimately names other verbs'
            // flags, and a usage line repeats what the table already lists.
            const opts_at = std.mem.indexOf(u8, res.stdout, "\noptions:\n") orelse res.stdout.len;
            var rest: []const u8 = if (opts_at == res.stdout.len) &[_]u8{} else res.stdout[opts_at + "\noptions:\n".len ..];
            if (std.mem.indexOf(u8, rest, "\nexit status:")) |stop| rest = rest[0..stop];
            if (section.len == 0) rest = &[_]u8{};

            var lines = std.mem.splitScalar(u8, rest, '\n');
            while (lines.next()) |line| {
                const t = std.mem.trimStart(u8, line, " ");
                if (t.len == 0 or t[0] != '-') continue; // a wrapped description line
                // The flag column ends at the first run of two spaces.
                const col = if (std.mem.indexOf(u8, t, "  ")) |e| t[0..e] else t;
                var k: usize = 0;
                while (k < col.len) {
                    if (flagAt(col, k)) |flag| {
                        k += flag.len;
                        if (std.mem.eql(u8, flag, "-h") or std.mem.eql(u8, flag, "--help")) continue;
                        if (mentionsFlag(section, flag) or mentionsFlag(conv, flag)) continue;
                        std.debug.print(
                            "\nreference pin: `zit {s} {s}` is accepted but www/reference.html's " ++
                                "\"{s}\" section never names it. A flag can ship undocumented while " ++
                                "the verb looks covered -- that is how `pack --drop-elided` stayed " ++
                                "invisible. Document it in the same commit that adds it.\n",
                            .{ id, flag, id },
                        );
                        missing += 1;
                    } else k += 1;
                }
            }
        }
    }
    try std.testing.expectEqual(@as(usize, 0), missing);
}

test "cli reference: every flag the reference documents is one the verb accepts" {
    // The other direction. A reference that promises a flag is worse than one
    // that omits it: the omission is discoverable from `--help`, the promise
    // sends someone looking for something that is not there.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const conv = conventionsBlock();
    var stale: usize = 0;
    inline for (comptime std.enums.values(cli.Verb)) |v| {
        if (v != .unknown and v != .help and v != .version) {
            var id_buf: [64]u8 = undefined;
            const tag = @tagName(v);
            @memcpy(id_buf[0..tag.len], tag);
            for (id_buf[0..tag.len]) |*c| {
                if (c.* == '_') c.* = '-';
            }
            const id = id_buf[0..tag.len];

            var sec_buf: [96]u8 = undefined;
            const section = referenceSection(id, &sec_buf) orelse &[_]u8{};
            var res = try runZit(gpa, io, tmp.dir, &.{ id, "--help" });
            defer freeResult(gpa, &res);

            // Only `<td class="f">` cells: the flag table is the reference's
            // CLAIM about this verb, while its prose may mention neighbours.
            var scan: usize = 0;
            while (std.mem.indexOfPos(u8, section, scan, "<td class=\"f\">")) |cell| {
                const from = cell + "<td class=\"f\">".len;
                const to = std.mem.indexOfPos(u8, section, from, "</td>") orelse break;
                scan = to;
                const text = section[from..to];
                var k: usize = 0;
                while (k < text.len) {
                    if (flagAt(text, k)) |flag| {
                        k += flag.len;
                        if (mentionsFlag(res.stdout, flag) or mentionsFlag(conv, flag)) continue;
                        std.debug.print(
                            "\nreference pin: www/reference.html's \"{s}\" section documents `{s}`, " ++
                                "which `zit {s} --help` never mentions. Either the flag was removed " ++
                                "and its row outlived it, or the help lost it.\n",
                            .{ id, flag, id },
                        );
                        stale += 1;
                    } else k += 1;
                }
            }
        }
    }
    try std.testing.expectEqual(@as(usize, 0), stale);
}

test "cli frames: --structural emits only what the crosslink links, signed manifests included, cursor unchanged" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Commit one: an ordinary blob + manifest.
    try writeFile(io, tmp.dir, "a.txt", "alpha\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    // The cursor between the commits, taken from an UNFILTERED poll.
    var mid = try runZit(gpa, io, tmp.dir, &.{"frames"});
    defer freeResult(gpa, &mid);
    try expectExitDiag(mid.term, 0, mid);
    const tm = try framesTrailer(gpa, mid.stdout);
    defer gpa.free(tm.cursor);
    defer gpa.free(tm.token);

    // Commit two: a blob + a CLEARSIGNED manifest. The signed case is pinned
    // deliberately: a wrapped artifact's first bytes are armor, not a card,
    // and the first draft of the structural gate's pre-screen rejected
    // exactly these — 1,087 of them on the sqlite corpus.
    try writeFile(io, tmp.dir, "b.txt", "beta\n");
    var ad = try runZit(gpa, io, tmp.dir, &.{ "add", "b.txt" });
    defer freeResult(gpa, &ad);
    try expectExitDiag(ad.term, 0, ad);
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u", .pgp = stub_signer }, &.{ "commit", "-m", "two", "--sign" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);

    // Full census: 4 artifacts. Structural census: only the two manifests —
    // and its trailer is IDENTICAL to the full stream's, because the cursor
    // advances over skipped frames exactly as over emitted ones.
    var full = try runZit(gpa, io, tmp.dir, &.{"frames"});
    defer freeResult(gpa, &full);
    try expectExitDiag(full.term, 0, full);
    try std.testing.expectEqual(@as(usize, 4), framesArtifactCount(full.stdout));
    var structural = try runZit(gpa, io, tmp.dir, &.{ "frames", "--structural" });
    defer freeResult(gpa, &structural);
    try expectExitDiag(structural.term, 0, structural);
    try std.testing.expectEqual(@as(usize, 2), framesArtifactCount(structural.stdout));
    const tf = try framesTrailer(gpa, full.stdout);
    defer gpa.free(tf.cursor);
    defer gpa.free(tf.token);
    const ts = try framesTrailer(gpa, structural.stdout);
    defer gpa.free(ts.cursor);
    defer gpa.free(ts.token);
    try std.testing.expectEqualStrings(tf.cursor, ts.cursor);
    try std.testing.expectEqualStrings(tf.token, ts.token);

    // Every structural payload is byte-identical to its frame in the full
    // stream: the flag filters, it never rewrites.
    try std.testing.expect(framesStructuralSubset(structural.stdout, full.stdout));

    // Incremental: from the mid-point cursor, --structural sees exactly the
    // signed manifest — the blob is traversed, not emitted.
    const since = try std.fmt.allocPrint(gpa, "--since={s}", .{tm.cursor});
    defer gpa.free(since);
    const verify = try std.fmt.allocPrint(gpa, "--verify={s}", .{tm.token});
    defer gpa.free(verify);
    var incr = try runZit(gpa, io, tmp.dir, &.{ "frames", since, verify, "--structural" });
    defer freeResult(gpa, &incr);
    try expectExitDiag(incr.term, 0, incr);
    try std.testing.expectEqual(@as(usize, 1), framesArtifactCount(incr.stdout));
    try std.testing.expect(std.mem.indexOf(u8, incr.stdout, "-----BEGIN PGP SIGNED MESSAGE-----") != null);
}

/// Every `artifact HASH SIZE` frame of `subset_stream` appears byte-identical
/// (header and payload) in `full_stream`.
fn framesStructuralSubset(subset_stream: []const u8, full_stream: []const u8) bool {
    var pos: usize = 0;
    while (pos < subset_stream.len) {
        const nl = std.mem.indexOfScalarPos(u8, subset_stream, pos, '\n') orelse return true;
        const header = subset_stream[pos..nl];
        if (!std.mem.startsWith(u8, header, "artifact ")) return true; // trailer reached
        var hf = std.mem.splitScalar(u8, header, ' ');
        _ = hf.next();
        _ = hf.next();
        const size = std.fmt.parseInt(usize, hf.next() orelse return false, 10) catch return false;
        const frame_end = nl + 1 + size + 1; // payload + trailing newline
        if (frame_end > subset_stream.len) return false;
        if (std.mem.indexOf(u8, full_stream, subset_stream[pos..frame_end]) == null) return false;
        pos = frame_end;
    }
    return true;
}

test "cli crosslink: derived records with changed files; cache and derive byte-identical" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // c1 (root): two adds. c2: modify + delete + add. c3: a rename, which the
    // change set spells as delete + add. c4: SIGNED, pinning that user and
    // files survive the clearsign wrapper.
    try writeFile(io, tmp.dir, "a.txt", "alpha\n");
    try writeFile(io, tmp.dir, "b.txt", "beta\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    try writeFile(io, tmp.dir, "a.txt", "alpha2\n");
    try writeFile(io, tmp.dir, "c.txt", "gamma\n");
    var rm = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "b.txt" });
    defer freeResult(gpa, &rm);
    try expectExitDiag(rm.term, 0, rm);
    var ad = try runZit(gpa, io, tmp.dir, &.{ "add", "c.txt" });
    defer freeResult(gpa, &ad);
    try expectExitDiag(ad.term, 0, ad);
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);

    var mv = try runZit(gpa, io, tmp.dir, &.{ "mv", "c.txt", "d.txt" });
    defer freeResult(gpa, &mv);
    try expectExitDiag(mv.term, 0, mv);
    var c3 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "three" });
    defer freeResult(gpa, &c3);
    try expectExit(c3.term, 0);

    try writeFile(io, tmp.dir, "a.txt", "alpha3\n");
    var c4 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "signer", .pgp = stub_signer }, &.{ "commit", "-m", "four", "--sign" });
    defer freeResult(gpa, &c4);
    try expectExit(c4.term, 0);

    var out = try runZit(gpa, io, tmp.dir, &.{"crosslink"});
    defer freeResult(gpa, &out);
    try expectExitDiag(out.term, 0, out);
    const t_ = std.testing;
    // The versioned header is the first line.
    try t_.expect(std.mem.startsWith(u8, out.stdout, "crosslink 1\n"));
    // c1: root — both files are adds.
    try t_.expect(std.mem.indexOf(u8, out.stdout, "file A a.txt\nfile A b.txt\n") != null);
    // c2: modify + add + delete, in canonical path order per record.
    try t_.expect(std.mem.indexOf(u8, out.stdout, "file M a.txt\nfile D b.txt\nfile A c.txt\n") != null);
    // c3: the rename spelled as delete + add.
    try t_.expect(std.mem.indexOf(u8, out.stdout, "file D c.txt\nfile A d.txt\n") != null);
    // Without --lines nothing is counted, printed or stored.
    try t_.expect(std.mem.indexOf(u8, out.stdout, "\nlines ") == null);
    try t_.expectError(error.FileNotFound, tmp.dir.statFile(io, ".zit-cache/lines", .{}));
    // c4: signed — the wrapper neither hides the user nor the change.
    try t_.expect(std.mem.indexOf(u8, out.stdout, "user signer\n") != null);
    try t_.expect(std.mem.indexOf(u8, out.stdout, "comment 4\nfour\n") != null);
    // Nothing in this repository is unknown.
    try t_.expect(std.mem.indexOf(u8, out.stdout, "files unknown") == null);
    // Four records, each closed.
    var n: usize = 0;
    var lines = std.mem.splitScalar(u8, out.stdout, '\n');
    while (lines.next()) |l| {
        if (std.mem.eql(u8, l, "end")) n += 1;
    }
    try t_.expectEqual(@as(usize, 4), n);

    // THE pin: cache-served output is byte-identical to the log derivation.
    var audit = try runZit(gpa, io, tmp.dir, &.{ "crosslink", "--no-cache" });
    defer freeResult(gpa, &audit);
    try expectExitDiag(audit.term, 0, audit);
    try t_.expectEqualStrings(out.stdout, audit.stdout);
}

test "cli crosslink: lines match diff --numstat; binary, mode-only and unreadable files" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    const t_ = std.testing;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "text.txt", "a\nb\nc\n");
    try writeFile(io, tmp.dir, "bin.dat", "x\x00y");
    try writeFile(io, tmp.dir, "run.sh", "echo\n");
    try addManaged(gpa, io, tmp.dir, &.{ "text.txt", "bin.dat", "run.sh" });
    const c1 = try commitHashDup(gpa, io, tmp.dir, "one");
    defer gpa.free(c1);
    try writeFile(io, tmp.dir, "text.txt", "a\nB\nc\nd\n");
    try writeFile(io, tmp.dir, "bin.dat", "x\x00z");
    try chmodFile(io, tmp.dir, "run.sh", 0o755);
    const c2 = try commitHashDup(gpa, io, tmp.dir, "two");
    defer gpa.free(c2);

    var out = try runZit(gpa, io, tmp.dir, &.{ "crosslink", "--lines" });
    defer freeResult(gpa, &out);
    try expectExitDiag(out.term, 0, out);
    try t_.expect(std.mem.indexOf(u8, recordOf(out.stdout, c1).?, "file A bin.dat\nlines - -\nfile A run.sh\nlines 1 0\nfile A text.txt\nlines 3 0") != null);
    // A binary file is - -, a mode-only change 0 0 (numstat leaves it out).
    try t_.expect(std.mem.indexOf(u8, recordOf(out.stdout, c2).?, "file M bin.dat\nlines - -\nfile M run.sh\nlines 0 0\nfile M text.txt\nlines 2 1") != null);
    var ns = try runZit(gpa, io, tmp.dir, &.{ "diff", "--checkin", c2, "--numstat" });
    defer freeResult(gpa, &ns);
    try expectExitDiag(ns.term, 0, ns);
    try t_.expectEqualStrings("-\t-\tbin.dat\n2\t1\ttext.txt\n", ns.stdout);

    // The counts are stored for the next run, which serves them unchanged,
    // and the derivation without any cache prints the same bytes.
    const stored = try tmp.dir.statFile(io, ".zit-cache/lines", .{});
    try t_.expect(stored.size > 0);
    var again = try runZit(gpa, io, tmp.dir, &.{ "crosslink", "--lines" });
    defer freeResult(gpa, &again);
    try expectExitDiag(again.term, 0, again);
    try t_.expectEqualStrings(out.stdout, again.stdout);
    var audit = try runZit(gpa, io, tmp.dir, &.{ "crosslink", "--lines", "--no-cache" });
    defer freeResult(gpa, &audit);
    try expectExitDiag(audit.term, 0, audit);
    try t_.expectEqualStrings(out.stdout, audit.stdout);

    // A check-in made after the counts were stored is counted on the next
    // run, beside the stored ones, and still matches the fresh derivation.
    try writeFile(io, tmp.dir, "text.txt", "a\nB\n");
    const c3 = try commitHashDup(gpa, io, tmp.dir, "three");
    defer gpa.free(c3);
    var grown = try runZit(gpa, io, tmp.dir, &.{ "crosslink", "--lines" });
    defer freeResult(gpa, &grown);
    try expectExitDiag(grown.term, 0, grown);
    try t_.expect(std.mem.endsWith(u8, recordOf(grown.stdout, c3).?, "file M text.txt\nlines 0 2"));
    var grown_audit = try runZit(gpa, io, tmp.dir, &.{ "crosslink", "--lines", "--no-cache" });
    defer freeResult(gpa, &grown_audit);
    try expectExitDiag(grown_audit.term, 0, grown_audit);
    try t_.expectEqualStrings(grown.stdout, grown_audit.stdout);

    // c4 touches only a path no shunned blob reaches. Its stored counts are
    // altered by hand, so the run after the shun shows whether they were
    // kept or counted again.
    try writeFile(io, tmp.dir, "other.txt", "o\n");
    try addManaged(gpa, io, tmp.dir, &.{"other.txt"});
    const c4 = try commitHashDup(gpa, io, tmp.dir, "four");
    defer gpa.free(c4);
    var c4_run = try runZit(gpa, io, tmp.dir, &.{ "crosslink", "--lines" });
    defer freeResult(gpa, &c4_run);
    try expectExitDiag(c4_run.term, 0, c4_run);
    try t_.expect(std.mem.endsWith(u8, recordOf(c4_run.stdout, c4).?, "file A other.txt\nlines 1 0"));
    {
        const sidecar = try tmp.dir.readFileAlloc(io, ".zit-cache/lines", gpa, .limited(1 << 20));
        defer gpa.free(sidecar);
        // name | n u32 (= 1) | added u32 | removed u32
        const added_at = std.mem.indexOf(u8, sidecar, c4).? + c4.len + 4;
        std.mem.writeInt(u32, sidecar[added_at..][0..4], 99, .little);
        try tmp.dir.writeFile(io, .{ .sub_path = ".zit-cache/lines", .data = sidecar });
    }

    // Shun c2's text.txt: its counts can no longer be computed, so its lines
    // line goes, and so does c3's, which read it as the old side, in both
    // outputs, while every other file keeps its own.
    var ls = try runZit(gpa, io, tmp.dir, &.{ "ls", "--long", c2 });
    defer freeResult(gpa, &ls);
    try expectExitDiag(ls.term, 0, ls);
    const text_line = lineEndingWith(ls.stdout, " text.txt").?;
    const blob = text_line[0..std.mem.indexOfScalar(u8, text_line, ' ').?];
    var sh = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", blob });
    defer freeResult(gpa, &sh);
    try expectExitDiag(sh.term, 0, sh);
    var gone = try runZit(gpa, io, tmp.dir, &.{ "crosslink", "--lines" });
    defer freeResult(gpa, &gone);
    try expectExitDiag(gone.term, 0, gone);
    try t_.expect(std.mem.endsWith(u8, recordOf(gone.stdout, c2).?, "file M run.sh\nlines 0 0\nfile M text.txt"));
    try t_.expect(std.mem.endsWith(u8, recordOf(gone.stdout, c3).?, "file M text.txt"));
    try t_.expect(std.mem.indexOf(u8, recordOf(gone.stdout, c1).?, "file A text.txt\nlines 3 0") != null);
    // c4 was not counted again: the shun recounts only what it can reach.
    try t_.expect(std.mem.endsWith(u8, recordOf(gone.stdout, c4).?, "file A other.txt\nlines 99 0"));
    try tmp.dir.deleteFile(io, ".zit-cache/lines");
    freeResult(gpa, &gone);
    gone = try runZit(gpa, io, tmp.dir, &.{ "crosslink", "--lines" });
    try expectExitDiag(gone.term, 0, gone);
    var gone_audit = try runZit(gpa, io, tmp.dir, &.{ "crosslink", "--lines", "--no-cache" });
    defer freeResult(gpa, &gone_audit);
    try expectExitDiag(gone_audit.term, 0, gone_audit);
    try t_.expectEqualStrings(gone.stdout, gone_audit.stdout);
}

/// The first line of `text` that ends with `suffix`.
fn lineEndingWith(text: []const u8, suffix: []const u8) ?[]const u8 {
    var lines = std.mem.splitScalar(u8, text, '\n');
    while (lines.next()) |l| if (std.mem.endsWith(u8, l, suffix)) return l;
    return null;
}

test "cli crosslink: a cherry-pick and a backout each name their Q card; a plain check-in names none" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "poem", "A\nB\nC\n");
    try addManaged(gpa, io, tmp.dir, &.{"poem"});
    const r0 = try commitHashDup(gpa, io, tmp.dir, "root");
    defer gpa.free(r0);
    try writeFile(io, tmp.dir, "poem", "A\nB\nZ\n");
    const r1 = try commitHashDup(gpa, io, tmp.dir, "rev");
    defer gpa.free(r1);
    var bo = try runZit(gpa, io, tmp.dir, &.{ "merge", "--backout", r1 });
    defer freeResult(gpa, &bo);
    try expectExit(bo.term, 0);
    const backout = try commitHashDup(gpa, io, tmp.dir, "backout");
    defer gpa.free(backout);

    var up = try runZit(gpa, io, tmp.dir, &.{ "update", r0 });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 0);
    try writeFile(io, tmp.dir, "poem", "Q\nB\nC\n");
    const fork = try commitForkDup(gpa, io, tmp.dir, "fork");
    defer gpa.free(fork);
    var cp = try runZit(gpa, io, tmp.dir, &.{ "merge", "--cherrypick", r1 });
    defer freeResult(gpa, &cp);
    try expectExit(cp.term, 0);
    const pick = try commitHashDup(gpa, io, tmp.dir, "pick");
    defer gpa.free(pick);

    var out = try runZit(gpa, io, tmp.dir, &.{"crosslink"});
    defer freeResult(gpa, &out);
    try expectExitDiag(out.term, 0, out);
    const t_ = std.testing;
    var buf: [80]u8 = undefined;
    try t_.expectEqualStrings(
        try std.fmt.bufPrint(&buf, "cherrypick -{s}", .{r1}),
        lineIn(recordOf(out.stdout, backout).?, "cherrypick ").?,
    );
    try t_.expectEqualStrings(
        try std.fmt.bufPrint(&buf, "cherrypick +{s}", .{r1}),
        lineIn(recordOf(out.stdout, pick).?, "cherrypick ").?,
    );
    for ([_][]const u8{ r0, r1, fork }) |plain| {
        try t_.expect(lineIn(recordOf(out.stdout, plain).?, "cherrypick ") == null);
    }

    var audit = try runZit(gpa, io, tmp.dir, &.{ "crosslink", "--no-cache" });
    defer freeResult(gpa, &audit);
    try expectExitDiag(audit.term, 0, audit);
    try t_.expectEqualStrings(out.stdout, audit.stdout);
}

/// The crosslink record for check-in `hash`: from its `checkin` line to its `end`.
fn recordOf(stream: []const u8, hash: []const u8) ?[]const u8 {
    var buf: [80]u8 = undefined;
    const head = std.fmt.bufPrint(&buf, "checkin {s} ", .{hash}) catch return null;
    const start = std.mem.indexOf(u8, stream, head) orelse return null;
    const end = std.mem.indexOfPos(u8, stream, start, "\nend\n") orelse return null;
    return stream[start..end];
}

/// The first line of `record` that starts with `prefix`.
fn lineIn(record: []const u8, prefix: []const u8) ?[]const u8 {
    var lines = std.mem.splitScalar(u8, record, '\n');
    while (lines.next()) |l| if (std.mem.startsWith(u8, l, prefix)) return l;
    return null;
}

test "cli timeline: --weekday prints the UTC day beside each date; without it, output is unchanged" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "alpha\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    var plain = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "-F", "oneline" });
    defer freeResult(gpa, &plain);
    try expectExitDiag(plain.term, 0, plain);
    var flagged = try runZit(gpa, io, tmp.dir, &.{ "timeline", "-F", "oneline", "--weekday" });
    defer freeResult(gpa, &flagged);
    try expectExitDiag(flagged.term, 0, flagged);

    const t_ = std.testing;
    const names = [7][]const u8{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

    // The plain listing carries no weekday token in the date column.
    for (names) |n| {
        var pat_buf: [8]u8 = undefined;
        const pat = std.fmt.bufPrint(&pat_buf, "  {s} 2", .{n}) catch unreachable;
        try t_.expect(std.mem.indexOf(u8, plain.stdout, pat) == null);
    }

    // The flagged listing carries exactly the day zit's own weekdayOf computes
    // for the printed date. Recover the date from the flagged line itself, so
    // the test holds on any day it runs.
    var lines = std.mem.splitScalar(u8, flagged.stdout, '\n');
    const row = lines.next().?;
    var fields = std.mem.tokenizeScalar(u8, row, ' ');
    _ = fields.next().?; // hash
    const day_token = fields.next().?;
    const iso = fields.next().?;
    try t_.expectEqual(@as(usize, 3), day_token.len);
    // Parse YYYY-MM-DD from the row and cross-check through daysFromCivil-free
    // arithmetic: reuse the library's own weekday of the commit mtime is not
    // observable here, so anchor on the printed date via zit.repostate.
    const y = std.fmt.parseInt(i64, iso[0..4], 10) catch unreachable;
    const mo = std.fmt.parseInt(u8, iso[5..7], 10) catch unreachable;
    const d = std.fmt.parseInt(u8, iso[8..10], 10) catch unreachable;
    // Sakamoto reference, the same one the unit property test pins against.
    const off = [12]i64{ 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
    const yy = if (mo < 3) y - 1 else y;
    const expect_day: usize = @intCast(@mod(yy + @divFloor(yy, 4) - @divFloor(yy, 100) + @divFloor(yy, 400) + off[mo - 1] + d, 7));
    try t_.expectEqualStrings(names[expect_day], day_token);

    // Removing the weekday token from the flagged row yields the plain row.
    var rebuilt: std.ArrayList(u8) = .empty;
    defer rebuilt.deinit(gpa);
    const day_at = std.mem.indexOf(u8, row, day_token).?;
    try rebuilt.appendSlice(gpa, row[0..day_at]);
    try rebuilt.appendSlice(gpa, row[day_at + 4 ..]);
    var plain_lines = std.mem.splitScalar(u8, plain.stdout, '\n');
    try t_.expectEqualStrings(plain_lines.next().?, rebuilt.items);
}

test "worktrees: per-checkout verbs never see the other checkout's branch as current" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "a", .default_dir);
    try tmp.dir.createDir(io, "b", .default_dir);
    var a_dir = try tmp.dir.openDir(io, "a", .{});
    defer a_dir.close(io);
    var b_dir = try tmp.dir.openDir(io, "b", .{});
    defer b_dir.close(io);

    // Checkout A: root on trunk, then a `forum` branch changing f.txt. A's
    // directory IS the store's home, which is what made this bug bite: a
    // detached worktree's store-dir resolution lands exactly here, beside
    // A's own .zit-checkout.
    try initRepo(gpa, io, a_dir);
    try writeFile(io, a_dir, "f.txt", "base\n");
    try addManaged(gpa, io, a_dir, &.{"f.txt"});
    var c1 = try runCommitEnv(gpa, io, a_dir, .{ .user = "t" }, &.{ "commit", "-m", "root" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    try writeFile(io, a_dir, "f.txt", "forum\n");
    var c2 = try runCommitEnv(gpa, io, a_dir, .{ .user = "t" }, &.{ "commit", "--branch", "forum", "-m", "forum" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 0);

    // Checkout B: opened at trunk on A's store, then its own `mirror` branch
    // adding g.txt.
    var opened = try runZit(gpa, io, b_dir, &.{ "open", "trunk", "--store=../a/.zit" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);
    try writeFile(io, b_dir, "g.txt", "mirror\n");
    var ad = try runZit(gpa, io, b_dir, &.{ "add", "g.txt" });
    defer freeResult(gpa, &ad);
    try expectExit(ad.term, 0);
    var c3 = try runCommitEnv(gpa, io, b_dir, .{ .user = "t" }, &.{ "commit", "--branch", "mirror", "-m", "m1" });
    defer freeResult(gpa, &c3);
    try expectExit(c3.term, 0);

    const t_ = std.testing;

    // leaves: each checkout stars ITS OWN branch's leaf, never the other's.
    var la = try runZit(gpa, io, a_dir, &.{"leaves"});
    defer freeResult(gpa, &la);
    try expectExitDiag(la.term, 0, la);
    var lb = try runZit(gpa, io, b_dir, &.{"leaves"});
    defer freeResult(gpa, &lb);
    try expectExitDiag(lb.term, 0, lb);
    var it = std.mem.splitScalar(u8, la.stdout, '\n');
    while (it.next()) |line| {
        if (std.mem.startsWith(u8, line, "*")) try t_.expect(std.mem.endsWith(u8, line, "forum"));
    }
    it = std.mem.splitScalar(u8, lb.stdout, '\n');
    while (it.next()) |line| {
        if (std.mem.startsWith(u8, line, "*")) try t_.expect(std.mem.endsWith(u8, line, "mirror"));
    }

    // Bare ls: B lists ITS check-in's files (f.txt AND g.txt), not A's.
    var ls_b = try runZit(gpa, io, b_dir, &.{"ls"});
    defer freeResult(gpa, &ls_b);
    try expectExitDiag(ls_b.term, 0, ls_b);
    try t_.expect(std.mem.indexOf(u8, ls_b.stdout, "g.txt") != null);

    // update trunk in B: the plan's base is B's own check-in, so g.txt is
    // REMOVED and f.txt reads trunk's bytes. The broken resolution planned
    // from A's checkout instead and left g.txt behind.
    var up = try runZit(gpa, io, b_dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &up);
    try expectExitDiag(up.term, 0, up);
    {
        const got = try b_dir.readFileAlloc(io, "f.txt", gpa, .limited(64));
        defer gpa.free(got);
        try t_.expectEqualStrings("base\n", got);
    }
    try t_.expectError(error.FileNotFound, b_dir.statFile(io, "g.txt", .{}));

    // merge mirror in B (now on trunk): trunk has not moved since mirror
    // branched, so the merge is conflict-free -- the broken resolution took
    // A's forum branch as "current" and manufactured conflicts against a
    // branch B never held.
    var mg = try runZit(gpa, io, b_dir, &.{ "merge", "mirror" });
    defer freeResult(gpa, &mg);
    try expectExitDiag(mg.term, 0, mg);
    {
        const got = try b_dir.readFileAlloc(io, "g.txt", gpa, .limited(64));
        defer gpa.free(got);
        try t_.expectEqualStrings("mirror\n", got);
    }
    try t_.expect(std.mem.indexOf(u8, mg.stdout, "<<<<<<<") == null);

    // And A never moved: still on forum, tree intact.
    var sa = try runZit(gpa, io, a_dir, &.{"status"});
    defer freeResult(gpa, &sa);
    try expectExitDiag(sa.term, 0, sa);
    try t_.expect(std.mem.indexOf(u8, sa.stdout, "tags:         forum") != null);
    {
        const got = try a_dir.readFileAlloc(io, "f.txt", gpa, .limited(64));
        defer gpa.free(got);
        try t_.expectEqualStrings("forum\n", got);
    }
}

/// Push a file's mtime `seconds` into the past, so the tree-stat memo's
/// race window (which declines to record any freshly-written file) does not
/// suppress the behavior under test.
fn backdateFile(io: std.Io, dir: std.Io.Dir, path: []const u8, seconds: i96) !void {
    var f = try dir.openFile(io, path, .{ .mode = .read_write });
    defer f.close(io);
    const now = std.Io.Timestamp.now(io, .real);
    try f.setTimestamps(io, .{ .modify_timestamp = .{ .new = .{
        .nanoseconds = now.nanoseconds - seconds * std.time.ns_per_s,
    } } });
}

test "cli status: the tree-stat memo is written, never masks an edit, and its loss changes no answer" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "alpha\n");
    try writeFile(io, tmp.dir, "b.txt", "beta\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "t" }, &.{ "commit", "-m", "r" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);
    const t_ = std.testing;

    // Outside the race window, a clean scan hashes both files and persists
    // the memo.
    try backdateFile(io, tmp.dir, "a.txt", 10);
    try backdateFile(io, tmp.dir, "b.txt", 10);
    {
        var st = try runZit(gpa, io, tmp.dir, &.{"status"});
        defer freeResult(gpa, &st);
        try expectExitDiag(st.term, 0, st);
        try t_.expect(std.mem.indexOf(u8, st.stdout, "edited") == null);
    }
    _ = tmp.dir.statFile(io, ".zit-cache/treestat", .{}) catch
        return error.TreeStatMemoNotWritten;

    // An edit lands (new mtime and size): the warm memo must not mask it.
    try writeFile(io, tmp.dir, "b.txt", "beta changed\n");
    try backdateFile(io, tmp.dir, "b.txt", 5);
    var warm = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &warm);
    try expectExitDiag(warm.term, 0, warm);
    try t_.expect(std.mem.indexOf(u8, warm.stdout, "EDITED     b.txt") != null);
    try t_.expect(std.mem.indexOf(u8, warm.stdout, "a.txt") == null);

    // The ladder rule: deleting the advisory tier changes no answer, for
    // status and for diff.
    var cold_status_answer: []u8 = undefined;
    {
        try tmp.dir.deleteTree(io, ".zit-cache");
        var cold = try runZit(gpa, io, tmp.dir, &.{"status"});
        defer freeResult(gpa, &cold);
        try expectExitDiag(cold.term, 0, cold);
        cold_status_answer = try gpa.dupe(u8, cold.stdout);
    }
    defer gpa.free(cold_status_answer);
    try t_.expectEqualStrings(warm.stdout, cold_status_answer);

    var warm_diff = try runZit(gpa, io, tmp.dir, &.{"diff"});
    defer freeResult(gpa, &warm_diff);
    try expectExitDiag(warm_diff.term, 0, warm_diff);
    {
        try tmp.dir.deleteTree(io, ".zit-cache");
        var cold_diff = try runZit(gpa, io, tmp.dir, &.{"diff"});
        defer freeResult(gpa, &cold_diff);
        try expectExitDiag(cold_diff.term, 0, cold_diff);
        try t_.expectEqualStrings(cold_diff.stdout, warm_diff.stdout);
        try t_.expect(std.mem.indexOf(u8, cold_diff.stdout, "beta changed") != null);
    }
}

test "cli settings: .fossil-settings is honoured, and wins over .zit-settings" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "keep.txt", "x\n");
    try addManaged(gpa, io, tmp.dir, &.{"keep.txt"});
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "t" }, &.{ "commit", "-m", "r" });
    defer freeResult(gpa, &c1);
    try expectExit(c1.term, 0);

    // A fossil-mirrored repository's own ignore policy, in fossil's spelling.
    try tmp.dir.createDir(io, ".fossil-settings", .default_dir);
    try writeFile(io, tmp.dir, ".fossil-settings/ignore-glob", "*.exe\n");
    try writeFile(io, tmp.dir, "win.exe", "");
    const t_ = std.testing;
    {
        var ex = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try t_.expect(std.mem.indexOf(u8, ex.stdout, "win.exe") == null);
    }

    // Both spellings present: fossil's wins, so the zit file's *.log glob is
    // inert and a.log stays visible -- documented precedence, not a union.
    try tmp.dir.createDir(io, ".zit-settings", .default_dir);
    try writeFile(io, tmp.dir, ".zit-settings/ignore-glob", "*.log\n");
    try writeFile(io, tmp.dir, "a.log", "");
    {
        var ex = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try t_.expect(std.mem.indexOf(u8, ex.stdout, "a.log") != null);
        try t_.expect(std.mem.indexOf(u8, ex.stdout, "win.exe") == null);
    }

    // The settings tier reads fossil's directory too, as the versioned tier.
    try writeFile(io, tmp.dir, ".fossil-settings/crlf-glob", "*.bat\n");
    {
        var st = try runZit(gpa, io, tmp.dir, &.{ "settings", "crlf-glob" });
        defer freeResult(gpa, &st);
        try expectExitDiag(st.term, 0, st);
        try t_.expect(std.mem.indexOf(u8, st.stdout, "*.bat") != null);
        // The provenance note is a diagnostic and rides stderr; it must
        // name the file that is live rather than the tier.
        try t_.expect(std.mem.indexOf(u8, st.stderr, ".fossil-settings/crlf-glob") != null);
    }
}
test "checkout settings: a versioned glob list splits on whitespace and quotes, and drops # lines" {
    // Expectations from fossil `glob.c glob_create` (whitespace separates,
    // quotes group) and `db.c db_get_versioned` (`#` lines stripped first).
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, ".fossil-settings", .default_dir);
    try writeFile(io, tmp.dir, ".fossil-settings/ignore-glob", "# build leftovers\n*.o *.a\n\"sp ace.txt\"\n");
    try touch(io, tmp.dir, "one.o");
    try touch(io, tmp.dir, "two.a");
    try touch(io, tmp.dir, "sp ace.txt");
    try touch(io, tmp.dir, "keep.txt");

    var ex = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &ex);
    try expectExitDiag(ex.term, 0, ex);
    // The comment line is not a pattern, both whitespace-separated globs
    // apply, and the quoted one keeps its space. (`.fossil-settings/` is an
    // untracked extra too, hidden here only by the dotfiles default.)
    try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "keep.txt\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "one.o") == null);
    try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "two.a") == null);
    try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "sp ace.txt") == null);
}

test "checkout settings: the shadow note names the live file, fires only against a stored value, and .no-warn silences it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, ".fossil-settings", .default_dir);
    try writeFile(io, tmp.dir, ".fossil-settings/ignore-glob", "*.tmp\n");

    // No stored value yet: a versioned file alone is nothing to warn about,
    // and the provenance line names the file that is live.
    {
        var st = try runZit(gpa, io, tmp.dir, &.{ "settings", "ignore-glob" });
        defer freeResult(gpa, &st);
        try expectExitDiag(st.term, 0, st);
        try std.testing.expectEqualStrings("ignore-glob *.tmp\n", st.stdout);
        try std.testing.expectEqualStrings(
            "note: ignore-glob is set in .fossil-settings/ignore-glob\n",
            st.stderr,
        );
    }

    // A repository value under it IS a shadowing, and the write says so.
    {
        var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "ignore-glob", "*.o" });
        defer freeResult(gpa, &w);
        try expectExitDiag(w.term, 0, w);
        try std.testing.expect(std.mem.indexOf(
            u8,
            w.stderr,
            "note: .fossil-settings/ignore-glob overrides the ignore-glob setting in the repository tier",
        ) != null);
    }
    {
        var st = try runZit(gpa, io, tmp.dir, &.{ "settings", "ignore-glob" });
        defer freeResult(gpa, &st);
        try expectExitDiag(st.term, 0, st);
        try std.testing.expect(std.mem.indexOf(u8, st.stderr, "overrides the ignore-glob setting") != null);
    }

    // A `.no-warn` in the OTHER directory silences it: one setting, two
    // spellings, and the silence follows the setting. The listing still says
    // which file is live.
    try tmp.dir.createDir(io, ".zit-settings", .default_dir);
    try writeFile(io, tmp.dir, ".zit-settings/ignore-glob.no-warn", "");
    {
        var st = try runZit(gpa, io, tmp.dir, &.{ "settings", "ignore-glob" });
        defer freeResult(gpa, &st);
        try expectExitDiag(st.term, 0, st);
        try std.testing.expectEqualStrings("ignore-glob *.tmp\n", st.stdout);
        try std.testing.expectEqualStrings(
            "note: ignore-glob is set in .fossil-settings/ignore-glob\n",
            st.stderr,
        );
    }
}

test "checkout settings: every tier of ignore-glob reaches the sweep, and .zitignore adds to it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try initRepo(gpa, io, repo);
    try touch(io, repo, "a.o");
    try touch(io, repo, "b.log");
    try touch(io, repo, "c.bak");
    try touch(io, repo, "keep.txt");

    const cfg = "../zit-global-settings";
    const env: []const [2][]const u8 = &.{.{ "ZIT_CONFIG", cfg }};

    // The GLOBAL tier now reaches the sweep, which is fossil's `db_get`
    // behaviour and was previously unreachable from a listing verb.
    var g = try runZitEnv(gpa, io, repo, env, &.{ "settings", "--global", "ignore-glob", "*.o" });
    defer freeResult(gpa, &g);
    try expectExitDiag(g.term, 0, g);
    {
        var ex = try runZitEnv(gpa, io, repo, env, &.{"extras"});
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "a.o") == null);
        try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "b.log") != null);
        try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "c.bak") != null);
    }

    // A repository value shadows the global one, as everywhere else.
    var r = try runZitEnv(gpa, io, repo, env, &.{ "settings", "ignore-glob", "*.log" });
    defer freeResult(gpa, &r);
    try expectExitDiag(r.term, 0, r);
    // ... and `.zitignore` ADDS to whatever won rather than replacing it.
    try writeFile(io, repo, ".zitignore", "*.bak\n");
    {
        var ex = try runZitEnv(gpa, io, repo, env, &.{"extras"});
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "a.o") != null);
        try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "b.log") == null);
        try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "c.bak") == null);
        try std.testing.expect(std.mem.indexOf(u8, ex.stdout, "keep.txt") != null);
    }
}

test "checkout settings: clean resolves keep-glob from the config it already read" {
    // clean used to open the store twice -- once for ignore-glob, once for
    // keep-glob through the sidecar-refreshing resolver. One open now answers
    // both, and the protection still holds.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "junk.tmp");
    try touch(io, tmp.dir, "precious.key");

    var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "keep-glob", "*.key" });
    defer freeResult(gpa, &w);
    try expectExitDiag(w.term, 0, w);

    var res = try runZit(gpa, io, tmp.dir, &.{ "clean", "-n" });
    defer freeResult(gpa, &res);
    try expectExitDiag(res.term, 0, res);
    try std.testing.expectEqualStrings("junk.tmp\n", res.stdout);
}

// --- landing ---------------------------------------------------------------
//
// The ceremony that gives a finished branch a reviewable record and a CI
// event. These pin landing's acceptance criteria, and one thing beyond them: that
// nothing here mints an artifact kind fossil does not already understand. That
// last pin is the feature's whole compatibility story, so it is a test rather
// than a comment.

/// Seed a repository with a trunk root and one check-in on `feat/x`, leaving
/// the checkout back on trunk. The shape every landing test starts from.
fn seedLandingRepo(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !void {
    try initRepo(gpa, io, dir);
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, dir, "a.txt", "root\n", "root", &root_c);

    try writeFile(io, dir, "a.txt", "root\nbranch work\n");
    var branch_c = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", "work", "--branch", "feat/x" });
    defer freeResult(gpa, &branch_c);
    try expectExit(branch_c.term, 0);

    var back = try runZit(gpa, io, dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExit(back.term, 0);
}

test "checkout settings: crnl-glob is the fallback spelling of crlf-glob, and crlf-glob wins" {
    // fossil reads `db_get("crlf-glob", db_get("crnl-glob",""))` (checkin.c),
    // so the older spelling answers only while the newer one is absent.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, ".zit-settings", .default_dir);
    try writeFile(io, tmp.dir, ".zit-settings/crnl-glob", "*.txt\n");

    // The class is now checked, and only `*.txt` may carry CR/LF.
    try writeFile(io, tmp.dir, "ok.txt", "a\r\nb\r\n");
    try writeFile(io, tmp.dir, "bad.dat", "a\r\nb\r\n");
    try addManaged(gpa, io, tmp.dir, &.{ "ok.txt", "bad.dat" });
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
        defer freeResult(gpa, &c);
        try expectExit(c.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "bad.dat") != null);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "CR/LF") != null);
    }

    // `crlf-glob` present anywhere REPLACES it: now `*.dat` is the declared
    // class and `ok.txt` is the file that is refused.
    try writeFile(io, tmp.dir, ".zit-settings/crlf-glob", "*.dat\n");
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "two" });
        defer freeResult(gpa, &c);
        try expectExit(c.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "ok.txt") != null);
    }
}

test "checkout settings: encoding-glob gates UTF-16 and invalid UTF-8, and binary-glob does not claim them" {
    // fossil warns about "Unicode" and "invalid UTF-8" at commit time and lets
    // `encoding-glob` name the files that are exempt (checkin.c). zit keeps its
    // opt-in shape: the class is checked only once the glob is declared, and
    // then it refuses rather than prompts.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // UTF-16 LE, "hi". The zero bytes are the high halves of ASCII units.
    const utf16 = "\xFF\xFEh\x00i\x00";
    const bad_utf8 = "hello \xC3\x28 there\n";

    // Undeclared: committed, and only mentioned in the receipt.
    try writeFile(io, tmp.dir, "seed.txt", utf16);
    try addManaged(gpa, io, tmp.dir, &.{"seed.txt"});
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
        defer freeResult(gpa, &c);
        try expectExitDiag(c.term, 0, c);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "UTF-16") != null);
    }

    // Declaring binary-glob does NOT make a UTF-16 file
    // binary: it is scanned as 16-bit units, so it carries no NUL.
    {
        var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "binary-glob", "*.dat" });
        defer freeResult(gpa, &w);
        try expectExitDiag(w.term, 0, w);
    }
    try writeFile(io, tmp.dir, "wide.txt", utf16);
    try addManaged(gpa, io, tmp.dir, &.{"wide.txt"});
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "wide" });
        defer freeResult(gpa, &c);
        try expectExitDiag(c.term, 0, c);
    }

    // Declared: only `*.bin` may carry either encoding class.
    {
        var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "encoding-glob", "*.bin" });
        defer freeResult(gpa, &w);
        try expectExitDiag(w.term, 0, w);
    }
    try writeFile(io, tmp.dir, "w.txt", utf16);
    try writeFile(io, tmp.dir, "w.bin", utf16);
    try writeFile(io, tmp.dir, "bad.txt", bad_utf8);
    try addManaged(gpa, io, tmp.dir, &.{ "w.txt", "w.bin", "bad.txt" });
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "all" });
        defer freeResult(gpa, &c);
        try expectExit(c.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "encoding-glob") != null);
    }
    // Each class names itself, so the operator knows which one they have.
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "wide", "w.txt" });
        defer freeResult(gpa, &c);
        try expectExit(c.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "w.txt") != null);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "UTF-16") != null);
    }
    // The exempt path commits on its own.
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "exempt", "w.bin" });
        defer freeResult(gpa, &c);
        try expectExitDiag(c.term, 0, c);
    }
    // The other encoding class is the same glob, with its own words.
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "bad", "bad.txt" });
        defer freeResult(gpa, &c);
        try expectExit(c.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "not valid UTF-8") != null);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "encoding-glob") != null);
    }

    // `*` is how a project turns the check back off, as for every other glob.
    {
        var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "encoding-glob", "*" });
        defer freeResult(gpa, &w);
        try expectExitDiag(w.term, 0, w);
    }
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "off" });
        defer freeResult(gpa, &c);
        try expectExitDiag(c.term, 0, c);
    }
}

test "checkout settings: declaring encoding-glob alone does not turn binary files into refusals" {
    // fossil's `commit_warning` decides the binary class first and returns
    // from that branch, so `encoding-glob` is never consulted for a blob with
    // a NUL. Without that ordering, declaring encoding-glob would silently
    // start refusing every PNG and zip in the tree -- naming the wrong class
    // and pointing at a glob that cannot exempt them.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    {
        var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "encoding-glob", "*.txt" });
        defer freeResult(gpa, &w);
        try expectExitDiag(w.term, 0, w);
    }

    // A PNG header: a NUL, and bytes that are not valid UTF-8 either.
    try writeFile(io, tmp.dir, "pic.png", "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\xFF\xFE\x01");
    try addManaged(gpa, io, tmp.dir, &.{"pic.png"});
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "pic" });
        defer freeResult(gpa, &c);
        try expectExitDiag(c.term, 0, c);
        // The receipt names the class the file is actually in.
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "pic.png is binary") != null);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "encoding-glob") == null);
    }

    // Declaring the binary class is what starts refusing it, and it is the
    // binary glob that exempts it.
    {
        var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "binary-glob", "*.dat" });
        defer freeResult(gpa, &w);
        try expectExitDiag(w.term, 0, w);
    }
    try writeFile(io, tmp.dir, "other.png", "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\xFF\xFE\x01");
    try addManaged(gpa, io, tmp.dir, &.{"other.png"});
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "other" });
        defer freeResult(gpa, &c);
        try expectExit(c.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "binary-glob") != null);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "encoding-glob") == null);
    }
}

test "checkout settings: a repository crnl-glob gates the commit when crlf-glob is unset" {
    // The commit gate reads its settings out of the ONE config frame it opened
    // for the write lock, so the repository tier has to answer for both
    // spellings from that frame -- not from a second store open per name.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    {
        var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "crnl-glob", "*.bin" });
        defer freeResult(gpa, &w);
        try expectExitDiag(w.term, 0, w);
    }

    try writeFile(io, tmp.dir, "ok.bin", "a\r\nb\r\n");
    try writeFile(io, tmp.dir, "bad.txt", "a\r\nb\r\n");
    try addManaged(gpa, io, tmp.dir, &.{ "ok.bin", "bad.txt" });
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
        defer freeResult(gpa, &c);
        try expectExit(c.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "bad.txt") != null);
        try std.testing.expect(std.mem.indexOf(u8, c.stderr, "CR/LF") != null);
    }
    // The declared path goes in on its own.
    {
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one", "ok.bin" });
        defer freeResult(gpa, &c);
        try expectExitDiag(c.term, 0, c);
    }
}

test "checkout settings: settings manifest on writes the build stamps at the next update" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var c1: std.process.RunResult = undefined;
    defer freeResult(gpa, &c1);
    _ = try commitFile(gpa, io, tmp.dir, "a.txt", "x\n", "root", &c1);

    // fossil's boolean spelling, which zit used to store and then match
    // against no selector letter at all -- so it produced nothing.
    var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "manifest", "on" });
    defer freeResult(gpa, &w);
    try expectExitDiag(w.term, 0, w);
    // Stored verbatim: the listing echoes what the operator wrote.
    try std.testing.expectEqualStrings("manifest on\n", w.stdout);

    var up = try runZit(gpa, io, tmp.dir, &.{"update"});
    defer freeResult(gpa, &up);
    try expectExitDiag(up.term, 0, up);
    try std.testing.expect(fileExists(io, tmp.dir, "manifest"));
    try std.testing.expect(fileExists(io, tmp.dir, "manifest.uuid"));
}

test "checkout settings: the new names are full settings at both stored tiers" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(io, "repo", .default_dir);
    var repo = try tmp.dir.openDir(io, "repo", .{});
    defer repo.close(io);
    try initRepo(gpa, io, repo);

    const cfg = "../zit-global-settings";
    const env: []const [2][]const u8 = &.{.{ "ZIT_CONFIG", cfg }};

    const names = [_][2][]const u8{
        .{ "crnl-glob", "*.bat" },
        .{ "clean-glob", "*.tmp" },
        .{ "empty-dirs", "logs tmp" },
        .{ "encoding-glob", "*.utf16" },
        // item 8: a mirrored Fossil repository's file is not an error, even
        // though nothing reads it yet.
        .{ "mimetypes", "md text/markdown" },
        // item 13: a boolean-valued setting takes fossil's spellings at every
        // tier, so `yes` is not a usage error here.
        .{ "dotfiles", "yes" },
    };
    for (names) |n| {
        var w = try runZitEnv(gpa, io, repo, env, &.{ "settings", n[0], n[1] });
        defer freeResult(gpa, &w);
        try expectExitDiag(w.term, 0, w);
        var r = try runZitEnv(gpa, io, repo, env, &.{ "settings", n[0], "--value" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
        try std.testing.expectEqualStrings(n[1], std.mem.trimEnd(u8, r.stdout, "\n"));

        var g = try runZitEnv(gpa, io, repo, env, &.{ "settings", "--global", n[0], n[1] });
        defer freeResult(gpa, &g);
        try expectExitDiag(g.term, 0, g);
    }

    // `dotfiles on` round-trips as `on`, not re-spelled -- the frame stores
    // the operator's own text.
    var on = try runZitEnv(gpa, io, repo, env, &.{ "settings", "dotfiles", "on" });
    defer freeResult(gpa, &on);
    try expectExitDiag(on.term, 0, on);
    try std.testing.expectEqualStrings("dotfiles on\n", on.stdout);

    // And a versioned file supplies any of them.
    try repo.createDir(io, ".fossil-settings", .default_dir);
    try writeFile(io, repo, ".fossil-settings/mimetypes", "# rows\nzig text/plain\n");
    var m = try runZitEnv(gpa, io, repo, env, &.{ "settings", "mimetypes", "--value" });
    defer freeResult(gpa, &m);
    try expectExitDiag(m.term, 0, m);
    try std.testing.expectEqualStrings("zig text/plain\n", m.stdout);
}

test "checkout settings: a versioned autosync is ignored, and says so" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, ".zit-settings", .default_dir);
    try writeFile(io, tmp.dir, ".zit-settings/autosync", "off\n");

    var st = try runZit(gpa, io, tmp.dir, &.{ "settings", "autosync" });
    defer freeResult(gpa, &st);
    try expectExitDiag(st.term, 0, st);
    // The file cannot supply the value, so the default still answers.
    try std.testing.expectEqualStrings("autosync on\n", st.stdout);
    try std.testing.expect(std.mem.indexOf(
        u8,
        st.stderr,
        "note: .zit-settings/autosync is versioned and ignored; autosync is a local setting",
    ) != null);
}

test "cli landing: open records a landing, ls lists it, and the branch stays unmerged" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "land the x work", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);
    const id = opened.stdout[0..64];

    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &listed);
    try expectExit(listed.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, id[0..12]) != null);
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, "open feat/x -> trunk") != null);

    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", id[0..12] });
    defer freeResult(gpa, &shown);
    try expectExit(shown.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "state    open") != null);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "land the x work") != null);

    // Opening a landing proposes; it does not land. Trunk must not have moved.
    var status = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &status);
    try std.testing.expect(std.mem.indexOf(u8, status.stdout, "nothing to report") != null);

    // One open landing per branch, refused by name rather than silently
    // minting a second permanent record.
    var again = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "again", "--user", "u" });
    defer freeResult(gpa, &again);
    try expectExit(again.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, again.stderr, "already has an open landing") != null);
}

/// How many store entries this run's forum index walked, from
/// `ZIT_LANDING_STATS`.
fn landingWalked(stderr: []const u8) !u64 {
    const tag = "landing-stats: forum index walked ";
    const at = std.mem.indexOf(u8, stderr, tag) orelse return error.NoLandingStats;
    const rest = stderr[at + tag.len ..];
    const end = std.mem.indexOfScalar(u8, rest, ' ') orelse return error.NoLandingStats;
    return std.fmt.parseInt(u64, rest[0..end], 10);
}

test "cli landing: a read walks only what was appended since the last one, and a shun or a rewrite is honoured" {
    // Finding the landing threads used to read every entry in the store on
    // every landing verb, so an accept grew with the store. The forum index
    // keeps their names keyed to the log prefix: a read walks the tail, a
    // rewrite rebuilds it, and a shunned post drops out when it is read.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);
    const stats = [_][2][]const u8{.{ "ZIT_LANDING_STATS", "1" }};

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "land the x work", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExitDiag(opened.term, 0, opened);
    const id = opened.stdout[0..64];

    // Built by `open`; this read walks only the thread root `open` appended.
    var first = try runZitEnv(gpa, io, tmp.dir, &stats, &.{ "landing", "ls" });
    defer freeResult(gpa, &first);
    try expectExitDiag(first.term, 0, first);
    try std.testing.expectEqual(@as(u64, 1), try landingWalked(first.stderr));
    // Nothing appended since: nothing walked, same answer.
    var warm = try runZitEnv(gpa, io, tmp.dir, &stats, &.{ "landing", "ls" });
    defer freeResult(gpa, &warm);
    try std.testing.expectEqual(@as(u64, 0), try landingWalked(warm.stderr));
    try std.testing.expectEqualStrings(first.stdout, warm.stdout);

    // One reply appended: one entry walked, and the reply is in the thread.
    var said = try runZit(gpa, io, tmp.dir, &.{ "landing", "comment", id[0..12], "-m", "one more thing", "--user", "u" });
    defer freeResult(gpa, &said);
    try expectExitDiag(said.term, 0, said);
    var shown = try runZitEnv(gpa, io, tmp.dir, &stats, &.{ "landing", "show", id[0..12] });
    defer freeResult(gpa, &shown);
    try expectExitDiag(shown.term, 0, shown);
    try std.testing.expectEqual(@as(u64, 1), try landingWalked(shown.stderr));
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "one more thing") != null);

    // A deferred shun masks the root without rewriting the store: the index
    // still names it, and the read drops it.
    var masked = try runZit(gpa, io, tmp.dir, &.{ "shun", "--defer", "--force", id });
    defer freeResult(gpa, &masked);
    try expectExitDiag(masked.term, 0, masked);
    var after_mask = try runZitEnv(gpa, io, tmp.dir, &stats, &.{ "landing", "ls" });
    defer freeResult(gpa, &after_mask);
    try expectExitDiag(after_mask.term, 0, after_mask);
    try std.testing.expect(std.mem.indexOf(u8, after_mask.stdout, id[0..12]) == null);

    // A rewrite replaces the store: the index's key no longer holds, so it is
    // rebuilt by one full walk, and still nothing names the shunned landing.
    var rewritten = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", id });
    defer freeResult(gpa, &rewritten);
    try expectExitDiag(rewritten.term, 0, rewritten);
    var after_rewrite = try runZitEnv(gpa, io, tmp.dir, &stats, &.{ "landing", "ls" });
    defer freeResult(gpa, &after_rewrite);
    try expectExitDiag(after_rewrite.term, 0, after_rewrite);
    try std.testing.expect(try landingWalked(after_rewrite.stderr) > 1);
    try std.testing.expect(std.mem.indexOf(u8, after_rewrite.stdout, id[0..12]) == null);

    // The rewrite the length check cannot see: the store shrinks by one
    // entry, then grows back by one before any landing read. The new entry
    // sits at a rid the index already covered, so only the prefix key tells
    // the index it was built over a different store.
    try writeFile(io, tmp.dir, "junk.txt", "not a landing\n");
    var junk = try runZitStdin(gpa, io, tmp.dir, "junk.txt", &.{ "forum", "post", "--title", "junk", "--user", "u" });
    defer freeResult(gpa, &junk);
    try expectExitDiag(junk.term, 0, junk);
    var covered = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &covered);
    try expectExitDiag(covered.term, 0, covered);
    var dropped = try runZit(gpa, io, tmp.dir, &.{ "shun", "--force", std.mem.trimEnd(u8, junk.stdout, "\n") });
    defer freeResult(gpa, &dropped);
    try expectExitDiag(dropped.term, 0, dropped);
    try writeFile(io, tmp.dir, "root.txt", "zit-landing: 1\nbranch: feat/x\ntarget: trunk\n\nposted directly\n");
    var direct = try runZitStdin(gpa, io, tmp.dir, "root.txt", &.{ "forum", "post", "--title", "land x again", "--user", "u" });
    defer freeResult(gpa, &direct);
    try expectExitDiag(direct.term, 0, direct);
    var found = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &found);
    try expectExitDiag(found.term, 0, found);
    try std.testing.expect(std.mem.indexOf(u8, found.stdout, direct.stdout[0..12]) != null);
}

test "cli landing: accept merges the branch, settles the record, and tags the check-in" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "land it", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);
    const id = opened.stdout[0..64];

    var accepted = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", id[0..12], "--user", "u" });
    defer freeResult(gpa, &accepted);
    try expectExit(accepted.term, 0);
    // The interface hides the encoding: one landing-shaped line, never the
    // merge/check-in/tag lines of the verbs it composes.
    try std.testing.expect(std.mem.indexOf(u8, accepted.stdout, "accepted ") != null);
    try std.testing.expect(std.mem.indexOf(u8, accepted.stdout, "check-in ") == null);
    try std.testing.expect(std.mem.indexOf(u8, accepted.stdout, "tag ") == null);

    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &listed);
    try std.testing.expectEqualStrings("", listed.stdout); // no longer open

    var all = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls", "--all" });
    defer freeResult(gpa, &all);
    try std.testing.expect(std.mem.indexOf(u8, all.stdout, "accepted feat/x -> trunk") != null);

    // The durable, check-in-oriented fact: a raw tag naming the landing.
    var tags = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", "--raw", "trunk" });
    defer freeResult(gpa, &tags);
    try std.testing.expect(std.mem.indexOf(u8, tags.stdout, "landing ") != null);
    try std.testing.expect(std.mem.indexOf(u8, tags.stdout, id) != null);

    // A settled landing takes no second verdict.
    var twice = try runZit(gpa, io, tmp.dir, &.{ "landing", "reject", id[0..12], "--user", "u" });
    defer freeResult(gpa, &twice);
    try expectExit(twice.term, 1);

    var verified = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &verified);
    try expectExit(verified.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, verified.stdout, "0 faults") != null);
}

test "cli landing: reject settles the record and leaves the target untouched" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);

    var before = try runZit(gpa, io, tmp.dir, &.{ "info", "trunk" });
    defer freeResult(gpa, &before);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "maybe", "--user", "u" });
    defer freeResult(gpa, &opened);
    const id = opened.stdout[0..64];

    var rejected = try runZit(gpa, io, tmp.dir, &.{ "landing", "reject", id[0..12], "-m", "not yet", "--user", "u" });
    defer freeResult(gpa, &rejected);
    try expectExit(rejected.term, 0);

    var all = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls", "--all" });
    defer freeResult(gpa, &all);
    try std.testing.expect(std.mem.indexOf(u8, all.stdout, "rejected feat/x -> trunk") != null);

    var after = try runZit(gpa, io, tmp.dir, &.{ "info", "trunk" });
    defer freeResult(gpa, &after);
    try std.testing.expectEqualStrings(before.stdout, after.stdout);
}

test "cli landing: the record tracks the branch, not a frozen range" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "in progress", "--user", "u" });
    defer freeResult(gpa, &opened);
    const id = opened.stdout[0..64];

    // Work continues after the landing opens — the workflow this serves keeps
    // committing to the branch, so a snapshot would be wrong by now.
    var to_branch = try runZit(gpa, io, tmp.dir, &.{ "update", "feat/x" });
    defer freeResult(gpa, &to_branch);
    try expectExit(to_branch.term, 0);
    try writeFile(io, tmp.dir, "a.txt", "root\nbranch work\nmore work\n");
    var more = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "more" });
    defer freeResult(gpa, &more);
    try expectExit(more.term, 0);

    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExit(back.term, 0);

    // Still one open landing on the same branch, unchanged by the new commit.
    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &listed);
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, "open feat/x -> trunk") != null);

    // And accepting it lands the LATER work, which is the whole point.
    var accepted = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", id[0..12], "--user", "u" });
    defer freeResult(gpa, &accepted);
    try expectExit(accepted.term, 0);
    var cat = try runZit(gpa, io, tmp.dir, &.{ "cat", "a.txt" });
    defer freeResult(gpa, &cat);
    try std.testing.expect(std.mem.indexOf(u8, cat.stdout, "more work") != null);
}

test "cli landing: a landing record is an ordinary forum artifact (compatibility pin)" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "ordinary", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);
    const id = opened.stdout[0..64];

    // Landing's invariant: a stock fossil clone must meet only artifacts it
    // already understands. The record is a forum post — H card, W body, no
    // card fossil does not know. If this ever fails because a new card was
    // minted, the design went wrong, not the test.
    var raw = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", id });
    defer freeResult(gpa, &raw);
    try expectExit(raw.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "\nH ") != null);
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "zit-landing: 1") != null);
    // No K card (that would be a ticket), no T-card run in the record itself.
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "\nK ") == null);
}

test "cli landing: --reply-to aims the I-card at a post while G stays the thread root" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "a proposal", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);
    const root = try gpa.dupe(u8, opened.stdout[0..64]);
    defer gpa.free(root);

    var g_buf: [80]u8 = undefined;
    const g_root = try std.fmt.bufPrint(&g_buf, "G {s}\n", .{root});

    // A plain comment replies to the record opener: G and I both name the root.
    var a = try runZit(gpa, io, tmp.dir, &.{ "landing", "comment", root, "-m", "first remark", "--user", "u" });
    defer freeResult(gpa, &a);
    try expectExit(a.term, 0);
    const a_id = try gpa.dupe(u8, a.stdout[0..64]);
    defer gpa.free(a_id);

    var a_raw = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", a_id });
    defer freeResult(gpa, &a_raw);
    var ir_buf: [80]u8 = undefined;
    const i_root = try std.fmt.bufPrint(&ir_buf, "I {s}\n", .{root});
    try std.testing.expect(std.mem.indexOf(u8, a_raw.stdout, g_root) != null);
    try std.testing.expect(std.mem.indexOf(u8, a_raw.stdout, i_root) != null);

    // --reply-to A aims the I-card at A while G still names the thread root,
    // so a reader can tell an answer from a new point.
    var b = try runZit(gpa, io, tmp.dir, &.{ "landing", "comment", root, "-m", "answering A", "--reply-to", a_id, "--user", "u" });
    defer freeResult(gpa, &b);
    try expectExit(b.term, 0);

    var b_raw = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", b.stdout[0..64] });
    defer freeResult(gpa, &b_raw);
    var ia_buf: [80]u8 = undefined;
    const i_a = try std.fmt.bufPrint(&ia_buf, "I {s}\n", .{a_id});
    try std.testing.expect(std.mem.indexOf(u8, b_raw.stdout, i_a) != null);
    try std.testing.expect(std.mem.indexOf(u8, b_raw.stdout, g_root) != null);
}

test "cli landing: --reply-to refuses a post outside the landing's own thread" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);

    // A second branch carries a second, independent landing thread.
    try writeFile(io, tmp.dir, "a.txt", "root\nother work\n");
    var oc = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "other", "--branch", "feat/y" });
    defer freeResult(gpa, &oc);
    try expectExit(oc.term, 0);
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExit(back.term, 0);

    var l1 = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "one", "--user", "u" });
    defer freeResult(gpa, &l1);
    try expectExit(l1.term, 0);
    const one = try gpa.dupe(u8, l1.stdout[0..64]);
    defer gpa.free(one);

    var l2 = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/y", "-m", "two", "--user", "u" });
    defer freeResult(gpa, &l2);
    try expectExit(l2.term, 0);
    const two = try gpa.dupe(u8, l2.stdout[0..64]);
    defer gpa.free(two);

    // Aiming landing one's comment at landing two's root is refused: the
    // capability check keeps a reply inside its own thread.
    var bad = try runZit(gpa, io, tmp.dir, &.{ "landing", "comment", one, "-m", "cross-thread", "--reply-to", two, "--user", "u" });
    defer freeResult(gpa, &bad);
    try expectExit(bad.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, bad.stderr, "thread") != null);
}

// --- landing title/prose split ---------------------------------------------

/// A stack `depth` segments tall on one shared file, each segment editing a
/// different line of it so the merges have every chance to collide. Returns the
/// landing ids, bottom first. The checkout is left on trunk.
///
/// One file on purpose: a stack whose segments touch disjoint files proves
/// nothing about merging, and the whole claim under test is that a
/// segment still merges cleanly once the one below it has landed.
fn seedStack(
    gpa: std.mem.Allocator,
    io: std.Io,
    dir: std.Io.Dir,
    comptime depth: usize,
    ids: *[depth][]u8,
) !void {
    try initRepo(gpa, io, dir);
    var lines: std.ArrayList(u8) = .empty;
    defer lines.deinit(gpa);
    for (0..depth * 2 + 1) |i| try lines.print(gpa, "line{d}\n", .{i});
    try writeFile(io, dir, "shared.txt", lines.items);
    try addManaged(gpa, io, dir, &.{"shared.txt"});
    var root_c = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", "base" });
    defer freeResult(gpa, &root_c);
    try expectExit(root_c.term, 0);

    // Each segment rewrites one line and commits onto its own branch, which
    // therefore descends from the segment before it.
    for (0..depth) |n| {
        var body: std.ArrayList(u8) = .empty;
        defer body.deinit(gpa);
        for (0..depth * 2 + 1) |i| {
            if (i == n * 2 + 1) {
                try body.print(gpa, "SEG{d}\n", .{n + 1});
            } else {
                try body.print(gpa, "line{d}\n", .{i});
            }
        }
        // Keep the edits from earlier segments: read back and patch one line.
        const cur = try readFile(gpa, io, dir, "shared.txt");
        defer gpa.free(cur);
        var patched: std.ArrayList(u8) = .empty;
        defer patched.deinit(gpa);
        var it = std.mem.splitScalar(u8, cur, '\n');
        var idx: usize = 0;
        while (it.next()) |line| {
            if (line.len == 0 and it.rest().len == 0) break;
            if (idx == n * 2 + 1) {
                try patched.print(gpa, "SEG{d}\n", .{n + 1});
            } else {
                try patched.print(gpa, "{s}\n", .{line});
            }
            idx += 1;
        }
        try writeFile(io, dir, "shared.txt", patched.items);

        var name_buf: [16]u8 = undefined;
        const branch = try std.fmt.bufPrint(&name_buf, "seg{d}", .{n + 1});
        var c = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", branch, "--branch", branch });
        defer freeResult(gpa, &c);
        try expectExit(c.term, 0);
    }

    var back = try runZit(gpa, io, dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExit(back.term, 0);

    // Bottom targets trunk; every other segment targets the one below it.
    for (0..depth) |n| {
        var b_buf: [16]u8 = undefined;
        var t_buf: [16]u8 = undefined;
        const branch = try std.fmt.bufPrint(&b_buf, "seg{d}", .{n + 1});
        const target = if (n == 0)
            "trunk"
        else
            try std.fmt.bufPrint(&t_buf, "seg{d}", .{n});
        var o = try runZit(gpa, io, dir, &.{ "landing", "open", "--branch", branch, "--to", target, "-m", branch, "--user", "u" });
        defer freeResult(gpa, &o);
        try expectExit(o.term, 0);
        ids[n] = try gpa.dupe(u8, o.stdout[0..64]);
    }
}

test "cli landing: a six-deep stack lands bottom-up, retargeting as it goes" {
    // The claim this whole feature rests on: a segment still merges cleanly
    // once the one below it has landed, at any depth and on a shared file, with
    // no rebase anywhere. Six deep because three proved nothing about depth.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 6;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    for (0..depth) |n| {
        var acc = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[n][0..12], "--user", "u" });
        defer freeResult(gpa, &acc);
        try expectExit(acc.term, 0);
        // Every accept but the last unblocks the next segment by moving it onto
        // trunk. That is the whole of the restack.
        if (n + 1 < depth) {
            try std.testing.expect(std.mem.indexOf(u8, acc.stdout, "retargeted 1 stacked landing(s) onto 'trunk'") != null);
        }
    }

    // Every segment's edit is on trunk, and none overwrote another.
    const final = try readFile(gpa, io, tmp.dir, "shared.txt");
    defer gpa.free(final);
    for (0..depth) |n| {
        var want: [8]u8 = undefined;
        const tag = try std.fmt.bufPrint(&want, "SEG{d}", .{n + 1});
        try std.testing.expect(std.mem.indexOf(u8, final, tag) != null);
    }

    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls", "--all" });
    defer freeResult(gpa, &listed);
    try expectExit(listed.term, 0);
    try std.testing.expectEqual(@as(usize, 0), std.mem.count(u8, listed.stdout, " open "));

    // Each landed segment closed its own branch, so the stack leaves exactly
    // one open leaf behind: trunk. Before this, a six-deep stack left seven,
    // and `leaves` is how an operator finds work that still needs doing.
    var leaves = try runZit(gpa, io, tmp.dir, &.{"leaves"});
    defer freeResult(gpa, &leaves);
    try expectExit(leaves.term, 0);
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, leaves.stdout, "\n"));
}

test "cli landing: accept refuses a segment whose base has not landed" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 3;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    // The middle segment: its base is open, so landing it would silently widen
    // its own diff to include work nobody reviewed under it.
    var acc = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[1][0..12], "--user", "u" });
    defer freeResult(gpa, &acc);
    try expectExit(acc.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, acc.stderr, "which has its own open landing") != null);
    try std.testing.expect(std.mem.indexOf(u8, acc.stderr, ids[0][0..12]) != null);

    // Refused means refused: nothing merged, every landing still open.
    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &listed);
    try std.testing.expectEqual(@as(usize, depth), std.mem.count(u8, listed.stdout, " open "));
}

test "cli landing: a stacked landing reviews only its own check-ins" {
    // The reviewer-facing half. A segment's diff is computed against the branch
    // below it, so the work under review is the segment's own -- which is what
    // makes small stacked landings worth proposing at all.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 4;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    for (1..depth) |n| {
        var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", ids[n][0..12] });
        defer freeResult(gpa, &shown);
        try expectExit(shown.term, 0);
        try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, shown.stdout, "\ncheckin "));
        // And it names what it sits on.
        try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "\non       ") != null);
    }
}

test "cli landing: retarget moves a landing, and refuses the cases that mean nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 2;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    var rt = try runZit(gpa, io, tmp.dir, &.{ "landing", "retarget", ids[1][0..12], "--to", "trunk", "--user", "u" });
    defer freeResult(gpa, &rt);
    try expectExit(rt.term, 0);

    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", ids[1][0..12] });
    defer freeResult(gpa, &shown);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "seg2 -> trunk") != null);

    // A no-op retarget is refused rather than written: a permanent record does
    // not need a reply that changes nothing.
    var again = try runZit(gpa, io, tmp.dir, &.{ "landing", "retarget", ids[1][0..12], "--to", "trunk", "--user", "u" });
    defer freeResult(gpa, &again);
    try expectExit(again.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, again.stderr, "already lands on") != null);

    // And a landing cannot land on itself.
    var self = try runZit(gpa, io, tmp.dir, &.{ "landing", "retarget", ids[1][0..12], "--to", "seg2", "--user", "u" });
    defer freeResult(gpa, &self);
    try expectExit(self.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, self.stderr, "cannot land on itself") != null);
}

test "cli landing: a rejected base is flagged on what was stacked over it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 2;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    var rej = try runZit(gpa, io, tmp.dir, &.{ "landing", "reject", ids[0][0..12], "-m", "not this way", "--user", "u" });
    defer freeResult(gpa, &rej);
    try expectExit(rej.term, 0);

    // seg2 still merges cleanly into seg1 -- true, and useless, because nobody
    // is going to land seg1. The record says so rather than leaving the
    // mergeability line to imply everything is fine.
    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", ids[1][0..12] });
    defer freeResult(gpa, &shown);
    try expectExit(shown.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "WARNING") != null);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "was rejected") != null);
}

test "cli landing: a retargeted landing is still an ordinary forum thread" {
    // The compatibility pin, matching the landing one: a stock fossil clone must
    // read every part of a stack, including the retarget replies. If this ever
    // needs a new card, the design was wrong.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 2;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    var rt = try runZit(gpa, io, tmp.dir, &.{ "landing", "retarget", ids[1][0..12], "--to", "trunk", "--user", "u" });
    defer freeResult(gpa, &rt);
    try expectExit(rt.term, 0);
    const reply = rt.stdout[rt.stdout.len - 65 ..][0..64];

    var art = try runZit(gpa, io, tmp.dir, &.{ "artifact", reply });
    defer freeResult(gpa, &art);
    try expectExit(art.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, art.stdout, "forum") != null);
}

test "cli landing: a forge lands a stack against a bare store" {
    // The forge path. `accept -R` merges in a working area it creates and
    // removes, and a stack must land through it exactly as it does from
    // a checkout -- including the retarget that unblocks the next segment.
    // Tested separately because it is a different accept implementation, not a
    // different flag on the same one.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 3;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    for (0..depth) |n| {
        var acc = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[n][0..12], "--user", "u", "-R", ".zit" });
        defer freeResult(gpa, &acc);
        try expectExit(acc.term, 0);
        if (n + 1 < depth) {
            try std.testing.expect(std.mem.indexOf(u8, acc.stdout, "retargeted 1 stacked landing(s) onto 'trunk'") != null);
        }
    }

    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls", "--all", "-R", ".zit" });
    defer freeResult(gpa, &listed);
    try expectExit(listed.term, 0);
    try std.testing.expectEqual(@as(usize, 0), std.mem.count(u8, listed.stdout, " open "));
    try std.testing.expectEqual(@as(usize, depth), std.mem.count(u8, listed.stdout, " accepted "));
}

test "cli landing: the ordering guard holds against a bare store too" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 2;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    // Refused BEFORE the ephemeral checkout is built, so a forge never pays for
    // a merge it was always going to decline.
    var acc = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[1][0..12], "--user", "u", "-R", ".zit" });
    defer freeResult(gpa, &acc);
    try expectExit(acc.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, acc.stderr, "which has its own open landing") != null);
}

test "cli landing: an accepted branch closes; a rejected one stays open" {
    // A landed branch is finished, and saying so is what keeps `leaves` a list
    // of work in progress. A REJECTED branch is not finished — the author may
    // revise and propose it again — so closing that one would be the tooling
    // making their decision for them.
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try seedLandingRepo(gpa, io, tmp.dir);
        var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "land it", "--user", "u" });
        defer freeResult(gpa, &opened);
        try expectExit(opened.term, 0);
        var acc = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", opened.stdout[0..12], "--user", "u" });
        defer freeResult(gpa, &acc);
        try expectExit(acc.term, 0);

        var branches = try runZit(gpa, io, tmp.dir, &.{"branch"});
        defer freeResult(gpa, &branches);
        try expectExit(branches.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, branches.stdout, "closed") != null);
    }
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try seedLandingRepo(gpa, io, tmp.dir);
        var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "maybe", "--user", "u" });
        defer freeResult(gpa, &opened);
        try expectExit(opened.term, 0);
        var rej = try runZit(gpa, io, tmp.dir, &.{ "landing", "reject", opened.stdout[0..12], "-m", "no", "--user", "u" });
        defer freeResult(gpa, &rej);
        try expectExit(rej.term, 0);

        var branches = try runZit(gpa, io, tmp.dir, &.{"branch"});
        defer freeResult(gpa, &branches);
        try expectExit(branches.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, branches.stdout, "closed") == null);
    }
}

test "cli landing: closing a landed segment does not close the ones above it" {
    // The regression this pins was live for about ten minutes: closing the
    // landed branch with a PROPAGATING `closed` tag marked every segment above
    // it closed too, because in a stack those check-ins are descendants of the
    // landed tip. The effect was invisible in the landing records — which stayed
    // open, correctly — and visible only in `branch` and `leaves`, i.e. exactly
    // where someone looks to find work that still needs doing.
    //
    // Fossil's own propagating close is safe because it closes a check-in with
    // no descendants. A landed stack segment is a leaf by the same-branch rule
    // while still having children on the branch above it, so it is not.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 3;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    var acc = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[0][0..12], "--user", "u" });
    defer freeResult(gpa, &acc);
    try expectExit(acc.term, 0);

    var branches = try runZit(gpa, io, tmp.dir, &.{"branch"});
    defer freeResult(gpa, &branches);
    try expectExit(branches.term, 0);
    // Exactly one branch closed: the one that landed.
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, branches.stdout, "closed"));
    try std.testing.expect(std.mem.indexOf(u8, branches.stdout, "seg2") != null);
    try std.testing.expect(std.mem.indexOf(u8, branches.stdout, "seg3") != null);

    // And the segments above are still landable, which is the fact the closed
    // tag would otherwise have quietly contradicted.
    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &listed);
    try std.testing.expectEqual(@as(usize, depth - 1), std.mem.count(u8, listed.stdout, " open "));
}

test "cli landing: concurrent accepts of adjacent segments cannot both land" {
    // The guard reads state and then acts, so two processes accepting adjacent
    // segments at once could in principle both pass. They cannot both WIN: the
    // loser either refuses on the guard or fails to merge, and the outcome is a
    // spurious refusal rather than an out-of-order landing. Failing closed is
    // the property worth pinning; which side loses is not.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 2;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    // Sequential here, in the order that exercises the guard: the store's
    // append lock already serialises the writes, so the interesting question is
    // whether the SECOND accept can slip past a stale read. It cannot, because
    // it re-folds.
    var top = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[1][0..12], "--user", "u" });
    defer freeResult(gpa, &top);
    try expectExit(top.term, 1);

    var bottom = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[0][0..12], "--user", "u" });
    defer freeResult(gpa, &bottom);
    try expectExit(bottom.term, 0);

    // Exactly one landed, and the other is now unblocked rather than lost.
    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls", "--all" });
    defer freeResult(gpa, &listed);
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, listed.stdout, " accepted "));
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, listed.stdout, " open "));
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, "seg2 -> trunk") != null);
}

test "cli landing: a rejected base refuses the segment above it" {
    // Before this guard the accept SUCCEEDED and put the work on the rejected
    // branch -- `accepted seg2 -> seg1`, where seg1 is a branch nobody is going
    // to land. Nothing failed, nothing warned, and the work was simply
    // somewhere trunk would never see it. A merge cannot catch this because the
    // merge is fine; only a rule can.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 2;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    var rej = try runZit(gpa, io, tmp.dir, &.{ "landing", "reject", ids[0][0..12], "-m", "no", "--user", "u" });
    defer freeResult(gpa, &rej);
    try expectExit(rej.term, 0);

    var acc = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[1][0..12], "--user", "u" });
    defer freeResult(gpa, &acc);
    try expectExit(acc.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, acc.stderr, "was rejected") != null);
    try std.testing.expect(std.mem.indexOf(u8, acc.stderr, "nobody will land") != null);
    // The remedy is named, and it is the one that works.
    try std.testing.expect(std.mem.indexOf(u8, acc.stderr, "landing retarget") != null);

    // Following it lands the work on trunk.
    var rt = try runZit(gpa, io, tmp.dir, &.{ "landing", "retarget", ids[1][0..12], "--to", "trunk", "--user", "u" });
    defer freeResult(gpa, &rt);
    try expectExit(rt.term, 0);
    var acc2 = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[1][0..12], "--user", "u" });
    defer freeResult(gpa, &acc2);
    try expectExit(acc2.term, 0);

    // AND THE REJECTED SEGMENT'S CONTENT COMES WITH IT. seg2's branch descends
    // from seg1's, so it literally contains seg1's check-ins; rejecting the
    // landing rejects the PROPOSAL, not the code underneath the segment above
    // it. Anyone who wants seg1's change gone has to revert it, not reject it.
    // Pinned because it is the surprising half of this outcome.
    const final = try readFile(gpa, io, tmp.dir, "shared.txt");
    defer gpa.free(final);
    try std.testing.expect(std.mem.indexOf(u8, final, "SEG1") != null);
    try std.testing.expect(std.mem.indexOf(u8, final, "SEG2") != null);
}

test "cli landing: a landing whose base already landed refuses rather than misfiling" {
    // Reachable when the auto-retarget after an accept did not run -- it is
    // best-effort by design. The child then still points at a branch that has
    // landed, and accepting it would merge into that branch instead of trunk.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 2;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    var acc = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[0][0..12], "--user", "u" });
    defer freeResult(gpa, &acc);
    try expectExit(acc.term, 0);

    // Put the child back where a failed auto-retarget would have left it.
    var back = try runZit(gpa, io, tmp.dir, &.{ "landing", "retarget", ids[1][0..12], "--to", "seg1", "--user", "u" });
    defer freeResult(gpa, &back);
    try expectExit(back.term, 0);

    var acc2 = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[1][0..12], "--user", "u" });
    defer freeResult(gpa, &acc2);
    try expectExit(acc2.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, acc2.stderr, "already landed") != null);
}

test "cli landing: a retarget that would make a cycle is refused" {
    // A cycle is worse than untidy: the ordering guard then refuses every
    // segment in it forever, each waiting on the next, with nothing in the
    // record saying why. It also drove `ls` into an indent computation that
    // panicked outright.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 3;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    // Direct: the bottom onto the one directly above it.
    var direct = try runZit(gpa, io, tmp.dir, &.{ "landing", "retarget", ids[0][0..12], "--to", "seg2", "--user", "u" });
    defer freeResult(gpa, &direct);
    try expectExit(direct.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, direct.stderr, "cycle") != null);

    // Indirect: the bottom onto the top, two hops away.
    var indirect = try runZit(gpa, io, tmp.dir, &.{ "landing", "retarget", ids[0][0..12], "--to", "seg3", "--user", "u" });
    defer freeResult(gpa, &indirect);
    try expectExit(indirect.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, indirect.stderr, "cycle") != null);

    // And the stack still lists, which is the symptom the panic took out.
    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &listed);
    try expectExit(listed.term, 0);
    try std.testing.expectEqual(@as(usize, depth), std.mem.count(u8, listed.stdout, " open "));
}

test "cli landing: retarget checks the branch it names exists" {
    // `open` refuses a target that does not resolve, because minting a record
    // aimed at nothing is a footprint nobody can retract. `retarget` mints the
    // same kind of record and was not checking.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 2;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    var rt = try runZit(gpa, io, tmp.dir, &.{ "landing", "retarget", ids[1][0..12], "--to", "no-such-branch", "--user", "u" });
    defer freeResult(gpa, &rt);
    try expectExit(rt.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, rt.stderr, "not found") != null);

    // Unchanged: the refusal wrote nothing.
    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", ids[1][0..12] });
    defer freeResult(gpa, &shown);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "seg2 -> seg1") != null);
}

test "cli landing: a branch already contained in its target settles instead of failing" {
    // Reachable from a stack: retarget the TOP segment to trunk and land it,
    // and it carries the segment below it in -- so that segment's landing is
    // open over work that is already there. `merge` says "already up to date",
    // which the accept path reported as "did not merge cleanly; resolve and
    // commit". Nothing to resolve, and `reject` was the only way to close the
    // record -- recording the opposite of what happened.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 2;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    var rt = try runZit(gpa, io, tmp.dir, &.{ "landing", "retarget", ids[1][0..12], "--to", "trunk", "--user", "u" });
    defer freeResult(gpa, &rt);
    try expectExit(rt.term, 0);
    var top = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[1][0..12], "--user", "u" });
    defer freeResult(gpa, &top);
    try expectExit(top.term, 0);

    // The bottom's work rode in with it.
    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", ids[0][0..12] });
    defer freeResult(gpa, &shown);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "up-to-date") != null);

    var bottom = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", ids[0][0..12], "--user", "u" });
    defer freeResult(gpa, &bottom);
    try expectExit(bottom.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, bottom.stdout, "already contained") != null);

    // Settled as accepted, which is what actually happened to the work.
    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls", "--all" });
    defer freeResult(gpa, &listed);
    try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, listed.stdout, " accepted "));
    try std.testing.expectEqual(@as(usize, 0), std.mem.count(u8, listed.stdout, " open "));
}

test "cli landing: two segments on one base both retarget when it lands" {
    // A stack is a chain per segment, not per repository: two landings may sit
    // on the same base. Both must move when it lands, or whichever was missed
    // is left pointing at a branch that already landed.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, tmp.dir, "f.txt", "base\n", "base", &root_c);

    try writeFile(io, tmp.dir, "a.txt", "a\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var ca = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "A", "--branch", "segA" });
    defer freeResult(gpa, &ca);
    try expectExit(ca.term, 0);

    try writeFile(io, tmp.dir, "b.txt", "b\n");
    try addManaged(gpa, io, tmp.dir, &.{"b.txt"});
    var cb = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "B", "--branch", "segB" });
    defer freeResult(gpa, &cb);
    try expectExit(cb.term, 0);

    var back_a = try runZit(gpa, io, tmp.dir, &.{ "update", "segA" });
    defer freeResult(gpa, &back_a);
    try expectExit(back_a.term, 0);
    try writeFile(io, tmp.dir, "c.txt", "c\n");
    try addManaged(gpa, io, tmp.dir, &.{"c.txt"});
    var cc = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "C", "--branch", "segC" });
    defer freeResult(gpa, &cc);
    try expectExit(cc.term, 0);

    var to_trunk = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &to_trunk);
    try expectExit(to_trunk.term, 0);

    var oa = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "segA", "--to", "trunk", "-m", "A", "--user", "u" });
    defer freeResult(gpa, &oa);
    try expectExit(oa.term, 0);
    for ([_][]const u8{ "segB", "segC" }) |b| {
        var o = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", b, "--to", "segA", "-m", b, "--user", "u" });
        defer freeResult(gpa, &o);
        try expectExit(o.term, 0);
    }

    var acc = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", oa.stdout[0..12], "--user", "u" });
    defer freeResult(gpa, &acc);
    try expectExit(acc.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, acc.stdout, "retargeted 2 stacked landing(s)") != null);

    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &listed);
    try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, listed.stdout, "-> trunk"));
}

test "cli landing: a multi-line -m puts only its first line in the H card" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);

    const message =
        \\Adds a demo document.
        \\
        \\This exists to exercise the ceremony end to end.
    ;
    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", message, "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);
    const id = opened.stdout[0..64];

    // An H card is one token (§4.7.7): the title is the first line, and the
    // prose below it appears ONCE, in the body, not a second time as a heading.
    var raw = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", id });
    defer freeResult(gpa, &raw);
    try expectExit(raw.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "\nH Adds\\sa\\sdemo\\sdocument.\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "This exists to exercise the ceremony end to end.") != null);
    // The prose is not also in the title, and the title is not also in the body.
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "\\nThis\\sexists") == null);
    const body_at = std.mem.indexOf(u8, raw.stdout, "zit-landing: 1").?;
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout[body_at..], "Adds a demo document.") == null);

    // One record is one row.
    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &listed);
    try expectExit(listed.term, 0);
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, listed.stdout, "\n"));
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, "Adds a demo document.") != null);
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, "This exists") == null);

    // show prints the heading, then the prose beneath it.
    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", id[0..12] });
    defer freeResult(gpa, &shown);
    try expectExit(shown.term, 0);
    const head_at = std.mem.indexOf(u8, shown.stdout, "Adds a demo document.").?;
    const prose_at = std.mem.indexOf(u8, shown.stdout, "This exists to exercise").?;
    try std.testing.expect(head_at < prose_at);
}

test "cli landing: a one-line -m is a title with no prose" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "land the x work", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);
    const id = opened.stdout[0..64];

    // The behaviour a one-line proposal had before the title/prose split, unchanged: the title
    // is the message, and the body is the header block and nothing else.
    var raw = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", id });
    defer freeResult(gpa, &raw);
    try expectExit(raw.term, 0);
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, raw.stdout, "\nH land\\sthe\\sx\\swork\n"));
    // The W body is the header block and nothing else — the title is not
    // repeated there, which is what carrying it once means.
    try std.testing.expectEqual(@as(usize, 0), std.mem.count(u8, raw.stdout, "land the x work"));

    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", id[0..12] });
    defer freeResult(gpa, &shown);
    try expectExit(shown.term, 0);
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, shown.stdout, "land the x work"));
}

test "cli landing: the split is the first newline, not the first blank line" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);

    // No blank line anywhere. Splitting on one would mint a two-line heading,
    // which is the whole defect — so the split is at the first newline.
    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "line one\nline two", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);
    const id = opened.stdout[0..64];

    var raw = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", id });
    defer freeResult(gpa, &raw);
    try expectExit(raw.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "\nH line\\sone\n") != null);
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "\\nline\\stwo") == null);
    try std.testing.expect(std.mem.indexOf(u8, raw.stdout, "line two") != null);
}

test "cli landing: a message with no title line is refused, not minted titleless" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);

    // An H card's token is 1*tchar: a landing with no first line has no title
    // to mint. Refusing beats writing a permanent artifact nobody can retract.
    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "\n\nbody only", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, opened.stderr, "must open with a title line") != null);

    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &listed);
    try expectExit(listed.term, 0);
    try std.testing.expectEqualStrings("", listed.stdout);
}

// --- landing mergeability probe --------------------------------------------
//
// A forge must be able to ask "would this land cleanly?" WITHOUT attempting
// the merge, because a server that attempts one can end up holding a conflict.
// These pin the two properties that make the answer worth having: it agrees
// with the real merge engine, and it writes nothing.

/// Seed trunk + a branch whose edit is in a different region of the same file,
/// which a three-way merge resolves cleanly. This is the case a HASH-level
/// resolver would wrongly call a conflict, so it is the sharpest probe test.
fn seedContentMergeable(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !void {
    try initRepo(gpa, io, dir);
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, dir, "f.txt", "top\nMID\nbottom\n", "root", &root_c);

    try writeFile(io, dir, "f.txt", "top\nMID\nBOTTOM-CHANGED\n");
    var br = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", "branch", "--branch", "feat/x" });
    defer freeResult(gpa, &br);
    try expectExit(br.term, 0);

    var back = try runZit(gpa, io, dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExit(back.term, 0);
    try writeFile(io, dir, "f.txt", "TOP-CHANGED\nMID\nbottom\n");
    var tr = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", "trunk" });
    defer freeResult(gpa, &tr);
    try expectExit(tr.term, 0);
}

test "cli landing: the probe reports a content-mergeable branch clean, not conflicted" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedContentMergeable(gpa, io, tmp.dir);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "clean", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);

    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", opened.stdout[0..12] });
    defer freeResult(gpa, &shown);
    try expectExit(shown.term, 0);
    // Both sides edited f.txt. Hash-level resolution refuses this; a
    // content-level probe must not, or every long-lived branch reads as
    // conflicted and the flag stops meaning anything.
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "merges   clean") != null);

    // And the real merge agrees, which is the property that matters.
    var merged = try runZit(gpa, io, tmp.dir, &.{ "merge", "feat/x" });
    defer freeResult(gpa, &merged);
    try expectExit(merged.term, 0);
}

test "cli landing: the probe names conflicting paths and agrees with the merge engine" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, tmp.dir, "keep.txt", "keep\n", "root", &root_c);

    // add-vs-add with differing content: an unconditional whole-file conflict.
    try writeFile(io, tmp.dir, "h.txt", "BRANCH\n");
    try addManaged(gpa, io, tmp.dir, &.{"h.txt"});
    var br = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "branch", "--branch", "feat/bad" });
    defer freeResult(gpa, &br);
    try expectExit(br.term, 0);

    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try writeFile(io, tmp.dir, "h.txt", "TRUNK\n");
    try addManaged(gpa, io, tmp.dir, &.{"h.txt"});
    var tr = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "trunk" });
    defer freeResult(gpa, &tr);
    try expectExit(tr.term, 0);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/bad", "-m", "bad", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);

    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", opened.stdout[0..12] });
    defer freeResult(gpa, &shown);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "merges   conflict (1)") != null);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "conflict h.txt") != null);

    // The engine must name the same file. Two conflict detectors that can
    // disagree would be worse than one: the forge would either block a branch
    // that merges or clear one that does not.
    var merged = try runZit(gpa, io, tmp.dir, &.{ "merge", "feat/bad" });
    defer freeResult(gpa, &merged);
    try std.testing.expect(std.mem.indexOf(u8, merged.stderr, "h.txt") != null);
}

test "cli landing: the probe answers against a bare store and writes nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedContentMergeable(gpa, io, tmp.dir);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "clean", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);

    var before = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &before);
    try expectExit(before.term, 0);

    // A genuinely BARE store -- the store bytes alone, no checkout beside them,
    // which is the shape quarry serves. The bare-store idiom.
    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    const store_bytes = try tmp.dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(store_bytes);
    try away.dir.createDir(io, "store", .default_dir);
    var store_dir = try away.dir.openDir(io, "store", .{});
    defer store_dir.close(io);
    try writeFile(io, store_dir, ".zit", store_bytes);

    var shown = try runZit(gpa, io, away.dir, &.{ "landing", "show", "-R", "store", opened.stdout[0..12] });
    defer freeResult(gpa, &shown);
    try expectExitDiag(shown.term, 0, shown);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "merges   clean") != null);

    // Asking must not change the store: a probe that writes is not a probe.
    var after = try runZit(gpa, io, tmp.dir, &.{"verify"});
    defer freeResult(gpa, &after);
    try expectExit(after.term, 0);
    try std.testing.expectEqualStrings(before.stdout, after.stdout);
}

test "cli landing: an already-merged branch reads up-to-date, not clean" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedContentMergeable(gpa, io, tmp.dir);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "clean", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);

    var accepted = try runZit(gpa, io, tmp.dir, &.{ "landing", "accept", opened.stdout[0..12], "--user", "u" });
    defer freeResult(gpa, &accepted);
    try expectExit(accepted.term, 0);

    // After landing, the branch is an ancestor of the target. "up-to-date" and
    // "clean" are different answers and a forge renders them differently.
    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", opened.stdout[0..12] });
    defer freeResult(gpa, &shown);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "merges   up-to-date") != null);
}

// --- landing accept against a bare store ------------------------------------
//
// The forge path. These pin the RFC rule the design rests on and the two
// properties a server must have: it never holds a half-finished merge, and it
// never mints an artifact a dense checkout would have minted differently.

/// Copy a store into a directory holding nothing else -- no checkout, no
/// worktree. What quarry serves.
fn bareStoreOf(gpa: std.mem.Allocator, io: std.Io, src: std.Io.Dir, dst: std.Io.Dir, name: []const u8) !void {
    const bytes = try src.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(bytes);
    try dst.createDir(io, name, .default_dir);
    var d = try dst.openDir(io, name, .{});
    defer d.close(io);
    try writeFile(io, d, ".zit", bytes);
}

test "cli landing: accept -R lands a branch with no checkout, and mints what a dense merge would" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    // f.txt is edited on both sides in different regions; k.txt is untouched
    // and must carry forward. Both halves matter to the card comparison.
    try initRepo(gpa, io, tmp.dir);
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, tmp.dir, "f.txt", "top\nMID\nbottom\n", "root", &root_c);
    try writeFile(io, tmp.dir, "k.txt", "keep\n");
    try addManaged(gpa, io, tmp.dir, &.{"k.txt"});
    var k_c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "keep" });
    defer freeResult(gpa, &k_c);

    try writeFile(io, tmp.dir, "f.txt", "top\nMID\nBOTTOM-CHANGED\n");
    var br = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "branch", "--branch", "feat/x" });
    defer freeResult(gpa, &br);
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try writeFile(io, tmp.dir, "f.txt", "TOP-CHANGED\nMID\nbottom\n");
    var tr = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "trunk" });
    defer freeResult(gpa, &tr);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "land it", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);
    const id = opened.stdout[0..12];

    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    try bareStoreOf(gpa, io, tmp.dir, away.dir, "store");

    var accepted = try runZit(gpa, io, away.dir, &.{ "landing", "accept", "-R", "store", id, "--user", "u" });
    defer freeResult(gpa, &accepted);
    try expectExitDiag(accepted.term, 0, accepted);
    try std.testing.expect(std.mem.indexOf(u8, accepted.stdout, "accepted ") != null);

    // sparse-cone Section 4.4: out-of-cone changes are applied "to the record
    // (manifest synthesis) without materializing the files". k.txt was never
    // materialized anywhere, yet the record carries it.
    var listed = try runZit(gpa, io, away.dir, &.{ "ls", "-R", "store", "trunk" });
    defer freeResult(gpa, &listed);
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, "k.txt") != null);
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, "f.txt") != null);

    // The merged bytes combine BOTH sides -- content-level, not hash-level.
    var content = try runZit(gpa, io, away.dir, &.{ "cat", "f.txt", "trunk", "-R", "store" });
    defer freeResult(gpa, &content);
    try std.testing.expect(std.mem.indexOf(u8, content.stdout, "TOP-CHANGED") != null);
    try std.testing.expect(std.mem.indexOf(u8, content.stdout, "BOTTOM-CHANGED") != null);

    // The ephemeral worktree is gone: a forge that leaks working areas beside
    // its stores accumulates them forever.
    var store_dir = try away.dir.openDir(io, "store", .{});
    defer store_dir.close(io);
    try std.testing.expectError(error.FileNotFound, store_dir.statFile(io, ".zit-landing-" ++ "000000000000", .{}));

    var verified = try runZit(gpa, io, away.dir, &.{ "verify", "-R", "store" });
    defer freeResult(gpa, &verified);
    try expectExit(verified.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, verified.stdout, "0 faults") != null);

    // artifact-format equivalence: the same merge from a dense checkout mints
    // the same F, P and R cards. D and U differ per invocation by definition,
    // so they are excluded -- this is the whole acceptance criterion.
    var dense_merge = try runZit(gpa, io, tmp.dir, &.{ "merge", "feat/x" });
    defer freeResult(gpa, &dense_merge);
    try expectExit(dense_merge.term, 0);
    var dense_commit = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "land it" });
    defer freeResult(gpa, &dense_commit);
    try expectExit(dense_commit.term, 0);

    var bare_art = try runZit(gpa, io, away.dir, &.{ "artifact", "--raw", "trunk", "-R", "store" });
    defer freeResult(gpa, &bare_art);
    var dense_art = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", "trunk" });
    defer freeResult(gpa, &dense_art);
    try expectCardsEqual(gpa, bare_art.stdout, dense_art.stdout);
}

/// Compare two manifests on the cards accept -R pins: F, P and R. D (timestamp) and
/// Z (which covers D) differ per invocation by construction, so a byte
/// comparison of whole manifests could never pass and would be the wrong test.
fn expectCardsEqual(gpa: std.mem.Allocator, a: []const u8, b: []const u8) !void {
    for ([_][]const u8{ "F ", "P ", "R " }) |prefix| {
        const ca = try cardsWithPrefix(gpa, a, prefix);
        defer gpa.free(ca);
        const cb = try cardsWithPrefix(gpa, b, prefix);
        defer gpa.free(cb);
        std.testing.expectEqualStrings(ca, cb) catch |e| {
            std.debug.print("manifest '{s}' cards differ between a bare accept and a dense merge\n", .{prefix});
            return e;
        };
    }
}

fn cardsWithPrefix(gpa: std.mem.Allocator, manifest: []const u8, prefix: []const u8) ![]u8 {
    var out: std.ArrayList(u8) = .empty;
    errdefer out.deinit(gpa);
    var it = std.mem.splitScalar(u8, manifest, '\n');
    while (it.next()) |line| {
        if (!std.mem.startsWith(u8, line, prefix)) continue;
        try out.appendSlice(gpa, line);
        try out.append(gpa, '\n');
    }
    return out.toOwnedSlice(gpa);
}

test "cli landing: a conflicting accept -R refuses, writes nothing, and says which paths" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, tmp.dir, "h.txt", "base\n", "root", &root_c);

    try writeFile(io, tmp.dir, "h.txt", "THEIRS\n");
    var br = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "branch", "--branch", "feat/bad" });
    defer freeResult(gpa, &br);
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try writeFile(io, tmp.dir, "h.txt", "OURS\n");
    var tr = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "trunk" });
    defer freeResult(gpa, &tr);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/bad", "-m", "bad", "--user", "u" });
    defer freeResult(gpa, &opened);
    const id = opened.stdout[0..12];

    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    try bareStoreOf(gpa, io, tmp.dir, away.dir, "store");
    var store_dir = try away.dir.openDir(io, "store", .{});
    defer store_dir.close(io);
    const before = try store_dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(before);

    var refused = try runZit(gpa, io, away.dir, &.{ "landing", "accept", "-R", "store", id, "--user", "u" });
    defer freeResult(gpa, &refused);
    // Exit 5, not 1: "needs a human" is a state a forge renders, not a bug it
    // reports. A caller must be able to tell them apart without reading prose.
    try expectExit(refused.term, 5);
    try std.testing.expect(std.mem.indexOf(u8, refused.stderr, "conflict h.txt") != null);
    try std.testing.expect(std.mem.indexOf(u8, refused.stderr, "nothing was written") != null);

    // The whole point: a server must never hold a half-finished merge, and
    // must never mint a check-in carrying conflict markers. Byte equality is
    // the only assertion that actually proves it.
    const after = try store_dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);

    // And the record is still open, so the forge shows it as awaiting a human.
    var listed = try runZit(gpa, io, away.dir, &.{ "landing", "ls", "-R", "store" });
    defer freeResult(gpa, &listed);
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, "open feat/bad -> trunk") != null);
}

test "cli landing: accept --expect-tip refuses a branch that moved after it was checked, and lands the tip it names" {
    // A forge checks a landing (CI, a preflight) against one tip, then accepts.
    // A push in between moves the branch. Without a way to name the checked
    // tip, the accept merged whatever the branch was by then, which nothing
    // had checked.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, tmp.dir, "f.txt", "a\n", "root", &root_c);

    try writeFile(io, tmp.dir, "f.txt", "checked\n");
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "checked", "--branch", "feat/x" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);
    const checked = try checkinHash(c1.stdout);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "x", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExitDiag(opened.term, 0, opened);
    const id = opened.stdout[0..12];

    // The branch moves after the check.
    try writeFile(io, tmp.dir, "f.txt", "unchecked\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "unchecked" });
    defer freeResult(gpa, &c2);
    try expectExitDiag(c2.term, 0, c2);
    const moved = try checkinHash(c2.stdout);

    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    try bareStoreOf(gpa, io, tmp.dir, away.dir, "store");
    var store_dir = try away.dir.openDir(io, "store", .{});
    defer store_dir.close(io);
    const before = try store_dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(before);

    // Exit 6: its own status, so a caller re-checks rather than reporting an
    // error or rendering a conflict. The message names where the branch is.
    var stale = try runZit(gpa, io, away.dir, &.{ "landing", "accept", "-R", "store", id, "--user", "u", "--expect-tip", checked });
    defer freeResult(gpa, &stale);
    try expectExit(stale.term, 6);
    const want = try std.fmt.allocPrint(gpa, "branch feat/x is at {s}, not {s}; nothing was written", .{ moved, checked });
    defer gpa.free(want);
    try std.testing.expect(std.mem.indexOf(u8, stale.stderr, want) != null);
    const after = try store_dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);

    // Naming the tip the branch is actually at lands it.
    var ok = try runZit(gpa, io, away.dir, &.{ "landing", "accept", "-R", "store", id, "--user", "u", "--expect-tip", moved });
    defer freeResult(gpa, &ok);
    try expectExitDiag(ok.term, 0, ok);
    var probe = try runZit(gpa, io, away.dir, &.{ "merge", "--probe", moved, "--into", "trunk", "-R", "store" });
    defer freeResult(gpa, &probe);
    try std.testing.expectEqualStrings("merges   up-to-date\n", probe.stdout);

    // The flag belongs to accept alone.
    var misuse = try runZit(gpa, io, away.dir, &.{ "landing", "reject", "-R", "store", id, "--expect-tip", moved });
    defer freeResult(gpa, &misuse);
    try expectExit(misuse.term, 2);
}

test "cli landing: accept --expect-target refuses a target that moved after the pair was judged, and lands on the target it names" {
    // A premerge forge judges the pair (branch tip, target tip), then accepts.
    // Another landing lands meanwhile and moves the target. Without a way to
    // name the judged target, the accept merged onto the new one, a pair
    // nothing tested.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    const judged = try commitFile(gpa, io, tmp.dir, "f.txt", "a\n", "root", &root_c);

    try writeFile(io, tmp.dir, "f.txt", "feature\n");
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "feature", "--branch", "feat/x" });
    defer freeResult(gpa, &c1);
    try expectExitDiag(c1.term, 0, c1);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "x", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExitDiag(opened.term, 0, opened);
    const id = opened.stdout[0..12];

    // The target moves after the pair was judged.
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);
    var c2: std.process.RunResult = undefined;
    defer freeResult(gpa, &c2);
    const moved = try commitFile(gpa, io, tmp.dir, "g.txt", "g\n", "another landing", &c2);

    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    try bareStoreOf(gpa, io, tmp.dir, away.dir, "store");
    var store_dir = try away.dir.openDir(io, "store", .{});
    defer store_dir.close(io);
    const before = try store_dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(before);

    // Exit 7, apart from 6, so a caller re-judges the pair. The message names
    // where the target is now, and nothing is written.
    var stale = try runZit(gpa, io, away.dir, &.{ "landing", "accept", "-R", "store", id, "--user", "u", "--expect-target", judged });
    defer freeResult(gpa, &stale);
    try expectExit(stale.term, 7);
    const want = try std.fmt.allocPrint(gpa, "target trunk is at {s}, not {s}; nothing was written", .{ moved, judged });
    defer gpa.free(want);
    try std.testing.expect(std.mem.indexOf(u8, stale.stderr, want) != null);
    const after = try store_dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);

    // Naming the target as it is lands the landing on it.
    var ok = try runZit(gpa, io, away.dir, &.{ "landing", "accept", "-R", "store", id, "--user", "u", "--expect-target", moved });
    defer freeResult(gpa, &ok);
    try expectExitDiag(ok.term, 0, ok);
    var probe = try runZit(gpa, io, away.dir, &.{ "merge", "--probe", moved, "--into", "trunk", "-R", "store" });
    defer freeResult(gpa, &probe);
    try std.testing.expectEqualStrings("merges   up-to-date\n", probe.stdout);

    // The flag belongs to accept alone.
    var misuse = try runZit(gpa, io, away.dir, &.{ "landing", "reject", "-R", "store", id, "--expect-target", moved });
    defer freeResult(gpa, &misuse);
    try expectExit(misuse.term, 2);
}

// --- landing show: check-ins and thread -------------------------------------

test "cli landing: show lists the branch's check-ins, newest first, and tracks later ones" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, tmp.dir, "f.txt", "a\n", "root", &root_c);

    try writeFile(io, tmp.dir, "f.txt", "b\n");
    var c1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "first branch commit", "--branch", "feat/x" });
    defer freeResult(gpa, &c1);
    try writeFile(io, tmp.dir, "f.txt", "c\n");
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "second branch commit" });
    defer freeResult(gpa, &c2);
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "land it", "--user", "dan" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);
    const id = opened.stdout[0..12];

    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", id });
    defer freeResult(gpa, &shown);
    try expectExitDiag(shown.term, 0, shown);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "first branch commit") != null);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "second branch commit") != null);
    // The root is on BOTH sides, so it is not part of what is proposed.
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "  root") == null);
    // Newest first.
    const first = std.mem.indexOf(u8, shown.stdout, "first branch commit").?;
    const second = std.mem.indexOf(u8, shown.stdout, "second branch commit").?;
    try std.testing.expect(second < first);

    // Landing's rule: the record names a branch, so work added after it opened
    // belongs to it. This is the display half of that.
    var to_branch = try runZit(gpa, io, tmp.dir, &.{ "update", "feat/x" });
    defer freeResult(gpa, &to_branch);
    try writeFile(io, tmp.dir, "f.txt", "d\n");
    var c3 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "added after opening" });
    defer freeResult(gpa, &c3);
    var back2 = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back2);

    var again = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", id });
    defer freeResult(gpa, &again);
    try std.testing.expect(std.mem.indexOf(u8, again.stdout, "added after opening") != null);
}

test "cli landing: a transition renders as an event, a remark as a comment, and an empty thread as nothing" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, tmp.dir, "f.txt", "a\n", "root", &root_c);
    try writeFile(io, tmp.dir, "f.txt", "b\n");
    var br = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "work", "--branch", "feat/x" });
    defer freeResult(gpa, &br);
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "land it", "--user", "dan" });
    defer freeResult(gpa, &opened);
    const id = opened.stdout[0..12];

    // A record nobody has spoken on prints no thread section at all -- not an
    // empty heading.
    var bare = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", id });
    defer freeResult(gpa, &bare);
    try std.testing.expect(std.mem.indexOf(u8, bare.stdout, "this\n") == null);

    var commented = try runZit(gpa, io, tmp.dir, &.{ "landing", "comment", id, "-m", "one nit", "--user", "reviewer" });
    defer freeResult(gpa, &commented);
    try expectExit(commented.term, 0);
    var rejected = try runZit(gpa, io, tmp.dir, &.{ "landing", "reject", id, "-m", "needs a rethink", "--user", "dan" });
    defer freeResult(gpa, &rejected);
    try expectExit(rejected.term, 0);

    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", id });
    defer freeResult(gpa, &shown);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "reviewer") != null);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "one nit") != null);
    // The transition is an EVENT, not a remark whose body happens to start
    // with a marker: the encoding must not leak into the display.
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "dan  rejected this") != null);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "needs a rethink") != null);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, state_marker_probe) == null);
}

/// The on-the-wire marker, spelled here so the test fails if it ever leaks into
/// rendered output. Deliberately a literal rather than an import: this asserts
/// what a READER sees, and should not move because the writer moved.
const state_marker_probe = "zit-landing-state:";

test "cli landing: the fold's pre-screen only rejects — a card-shaped blob is not mistaken for a record" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // Content that PASSES the two-byte screen: a file whose first bytes are
    // `<LETTER><SP>`, and one that opens like a signature wrapper. Both reach
    // the full parse, which is what must reject them. The screen may only ever
    // reject; if it ever admitted, this is where that shows up.
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, tmp.dir, "cardish.txt", "F not/a/card 0123\nW nope\n", "root", &root_c);
    try writeFile(io, tmp.dir, "wrapperish.txt", "-----BEGIN SOMETHING-----\nnot an artifact\n");
    try writeFile(io, tmp.dir, "plain.bin", "\x00\x01\x02 not card shaped\n");
    try addManaged(gpa, io, tmp.dir, &.{ "wrapperish.txt", "plain.bin" });
    var c2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "decoys" });
    defer freeResult(gpa, &c2);

    try writeFile(io, tmp.dir, "cardish.txt", "F not/a/card 9999\nW nope\n");
    var br = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "work", "--branch", "feat/x" });
    defer freeResult(gpa, &br);
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);

    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "the only landing", "--user", "u" });
    defer freeResult(gpa, &opened);
    try expectExit(opened.term, 0);

    // Exactly one landing: the real one. A decoy admitted by the screen and
    // then mis-parsed would show up as a second row or a crash.
    var listed = try runZit(gpa, io, tmp.dir, &.{ "landing", "ls" });
    defer freeResult(gpa, &listed);
    try expectExitDiag(listed.term, 0, listed);
    var rows: usize = 0;
    var it = std.mem.splitScalar(u8, listed.stdout, '\n');
    while (it.next()) |line| {
        if (line.len != 0) rows += 1;
    }
    try std.testing.expectEqual(@as(usize, 1), rows);
    try std.testing.expect(std.mem.indexOf(u8, listed.stdout, "the only landing") != null);

    // And the record still reads in full: the screen must not have skipped
    // the landing's own artifact on its way past the decoys.
    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", opened.stdout[0..12] });
    defer freeResult(gpa, &shown);
    try expectExit(shown.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "branch   feat/x -> trunk") != null);
}

// --- the N card, per type ---------------------------------------------------
//
// artifact-format §4.7.12 gives the READ rule: on wiki, technote and forum
// artifacts the N card names the W body's mimetype, "defaulting to
// text/x-fossil-wiki when omitted". That is not the same rule as when the card
// is WRITTEN, and the draft does not state the write rule at all -- so these
// pin it against fossil's own source, which diverges BY TYPE:
//
//   wiki     (wiki.c:2084)  write N only if present, non-empty, and != default
//   forum    (forum.c:1784) same
//   technote (event.c:284)  write N whenever present and non-empty -- NO
//                           default-suppression
//
// The divergence is fossil's. Tidying it into one rule would mint different
// bytes, and so different artifact ids, than fossil produces from identical
// input -- which is the entire compatibility claim.

fn nCardOf(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, hash: []const u8) !?[]u8 {
    var art = try runZit(gpa, io, dir, &.{ "artifact", "--raw", hash });
    defer freeResult(gpa, &art);
    var it = std.mem.splitScalar(u8, art.stdout, '\n');
    while (it.next()) |line| {
        if (std.mem.startsWith(u8, line, "N ")) return try gpa.dupe(u8, line);
    }
    return null;
}

test "cli: wiki and forum suppress the default mimetype; technote does not" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "body.txt", "hello\n");

    // FORUM (forum.c:1784) -- the default is never spelled out.
    var f_default = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{
        "forum", "post", "--user", "u", "--title", "T", "--mimetype", "text/x-fossil-wiki",
    });
    defer freeResult(gpa, &f_default);
    try expectExit(f_default.term, 0);
    if (try nCardOf(gpa, io, tmp.dir, f_default.stdout[0..64])) |n| {
        defer gpa.free(n);
        std.debug.print("forum minted '{s}'; fossil omits the default\n", .{n});
        return error.TestUnexpectedResult;
    }

    // ...but a non-default one is carried.
    var f_md = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{
        "forum", "post", "--user", "u", "--title", "T2", "--mimetype", "text/x-markdown",
    });
    defer freeResult(gpa, &f_md);
    const f_n = (try nCardOf(gpa, io, tmp.dir, f_md.stdout[0..64])).?;
    defer gpa.free(f_n);
    try std.testing.expectEqualStrings("N text/x-markdown", f_n);

    // WIKI (wiki.c:2084) -- same rule.
    var w_default = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{
        "wiki", "commit", "Page", "--user", "u", "--mimetype", "text/x-fossil-wiki",
    });
    defer freeResult(gpa, &w_default);
    try expectExit(w_default.term, 0);
    if (try nCardOf(gpa, io, tmp.dir, w_default.stdout[0..64])) |n| {
        defer gpa.free(n);
        std.debug.print("wiki minted '{s}'; fossil omits the default\n", .{n});
        return error.TestUnexpectedResult;
    }

    // TECHNOTE (event.c:284) -- fossil writes it, so zit must too. This is the
    // arm that makes "just be consistent across the three" the WRONG fix.
    var t_default = try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{
        "technote", "add", "--user", "u", "--mimetype", "text/x-fossil-wiki",
    });
    defer freeResult(gpa, &t_default);
    try expectExit(t_default.term, 0);
    // `technote add` prints "<event-id> <artifact-hash>".
    const sp = std.mem.indexOfScalar(u8, t_default.stdout, ' ').?;
    const t_hash = std.mem.trim(u8, t_default.stdout[sp + 1 ..], " \n");
    const t_n = (try nCardOf(gpa, io, tmp.dir, t_hash)).?;
    defer gpa.free(t_n);
    try std.testing.expectEqualStrings("N text/x-fossil-wiki", t_n);
}

test "cli landing: a landing record carries the same N card a forum post would" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedLandingRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "body.txt", "hello\n");

    // The pin that would have caught the hardcoded mimetype: a landing MINTS a forum artifact,
    // so given the same mimetype the two must agree card for card. They used
    // to disagree -- landing hardcoded `N text/plain` and could not be told
    // otherwise, which overrode §4.7.12's default on every record it wrote.
    for ([_]?[]const u8{ null, "text/x-markdown", "text/x-fossil-wiki" }) |mt| {
        var opened = if (mt) |m|
            try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "msg", "--user", "u", "--mimetype", m })
        else
            try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "msg", "--user", "u" });
        defer freeResult(gpa, &opened);
        try expectExitDiag(opened.term, 0, opened);

        var posted = if (mt) |m|
            try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{ "forum", "post", "--user", "u", "--title", "T", "--mimetype", m })
        else
            try runZitStdin(gpa, io, tmp.dir, "body.txt", &.{ "forum", "post", "--user", "u", "--title", "T" });
        defer freeResult(gpa, &posted);
        try expectExit(posted.term, 0);

        const landing_n = try nCardOf(gpa, io, tmp.dir, opened.stdout[0..64]);
        defer if (landing_n) |n| gpa.free(n);
        const forum_n = try nCardOf(gpa, io, tmp.dir, posted.stdout[0..64]);
        defer if (forum_n) |n| gpa.free(n);

        if (landing_n == null and forum_n == null) {
            // Both omitted: §4.7.12's default applies to both. Settle the
            // landing so the next iteration's branch is free.
            var rejected = try runZit(gpa, io, tmp.dir, &.{ "landing", "reject", opened.stdout[0..12], "--user", "u" });
            defer freeResult(gpa, &rejected);
            continue;
        }
        try std.testing.expect(landing_n != null and forum_n != null);
        try std.testing.expectEqualStrings(forum_n.?, landing_n.?);

        var rejected = try runZit(gpa, io, tmp.dir, &.{ "landing", "reject", opened.stdout[0..12], "--user", "u" });
        defer freeResult(gpa, &rejected);
    }
}

// --- merge --probe ----------------------------------------------------------
//
// A consumer that keeps its own index of records should not have to scan a
// store to ask about a pair of check-ins it already knows. `landing show`
// answers the same question but must FIND the record first, which is linear in
// store bytes; here the caller names both sides and pays only for the merge.

test "cli merge: --probe over a stack lands each rung on the ones before it, and finds the stop a pairwise probe misses" {
    // r1 -> r2 -> r3 -> r4 are stacked, each built on the last, while trunk
    // moved on its own. Probed against the rung below, r3 is clean: relative
    // to r2 only r3 touched line 5. Landed in order, it collides with trunk's
    // own line-5 edit. The chain answers the landing's question.
    //
    // r2 is the widened-base check. Once r1 has landed its base is r1, and r2
    // only rewrote what r1 wrote: clean. Merging the landed tree (which holds
    // r1's line) against the OLD fork point would see both sides change line
    // 2 and call r2 a conflict with its own parent.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    const t_ = std.testing;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, tmp.dir, "f.txt", "L1\nL2\nL3\nL4\nL5\n", "root", &root_c);

    const Rung = struct { branch: []const u8, content: ?[]const u8, extra: ?[]const u8 };
    const rungs = [_]Rung{
        .{ .branch = "r1", .content = "L1\nR1\nL3\nL4\nL5\n", .extra = null },
        .{ .branch = "r2", .content = "L1\nR2\nL3\nL4\nL5\n", .extra = null },
        .{ .branch = "r3", .content = "L1\nR2\nL3\nL4\nX5\n", .extra = null },
        .{ .branch = "r4", .content = null, .extra = "h.txt" },
    };
    for (rungs) |r| {
        if (r.content) |c| try writeFile(io, tmp.dir, "f.txt", c);
        if (r.extra) |x| {
            try writeFile(io, tmp.dir, x, "h\n");
            try addManaged(gpa, io, tmp.dir, &.{x});
        }
        var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", r.branch, "--branch", r.branch });
        defer freeResult(gpa, &c);
        try expectExitDiag(c.term, 0, c);
    }
    // Trunk moves on a line no rung touches until r3.
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);
    try writeFile(io, tmp.dir, "f.txt", "L1\nL2\nL3\nL4\nT5\n");
    var tm = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "trunk moves" });
    defer freeResult(gpa, &tm);
    try expectExitDiag(tm.term, 0, tm);

    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    try bareStoreOf(gpa, io, tmp.dir, away.dir, "store");
    var store_dir = try away.dir.openDir(io, "store", .{});
    defer store_dir.close(io);
    const before = try store_dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(before);

    var chain = try runZit(gpa, io, away.dir, &.{ "merge", "--probe", "--into", "trunk", "r1", "r2", "r3", "r4", "-R", "store" });
    defer freeResult(gpa, &chain);
    try expectExitDiag(chain.term, 0, chain);
    try t_.expectEqualStrings(
        \\merges   r1 clean
        \\merges   r2 clean
        \\merges   r3 conflict (1)
        \\conflict f.txt
        \\merges   r4 not-reached
        \\
    , chain.stdout);

    // The pairwise question gets the pairwise answer — clean — which is why
    // the chain exists. And the first rung alone reads exactly as it always has.
    var alone = try runZit(gpa, io, away.dir, &.{ "merge", "--probe", "r3", "--into", "r2", "-R", "store" });
    defer freeResult(gpa, &alone);
    try expectExitDiag(alone.term, 0, alone);
    try t_.expectEqualStrings("merges   clean\n", alone.stdout);
    var first = try runZit(gpa, io, away.dir, &.{ "merge", "--probe", "r1", "--into", "trunk", "-R", "store" });
    defer freeResult(gpa, &first);
    try t_.expectEqualStrings("merges   clean\n", first.stdout);

    // A rung already inside the landed tree is up-to-date and the chain goes on.
    var again = try runZit(gpa, io, away.dir, &.{ "merge", "--probe", "--into", "trunk", "r2", "r1", "-R", "store" });
    defer freeResult(gpa, &again);
    try expectExitDiag(again.term, 0, again);
    try t_.expectEqualStrings("merges   r2 clean\nmerges   r1 up-to-date\n", again.stdout);

    // A real merge still takes one NAME.
    var two = try runZit(gpa, io, tmp.dir, &.{ "merge", "r1", "r2" });
    defer freeResult(gpa, &two);
    try expectExit(two.term, 2);

    const after = try store_dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(after);
    try t_.expectEqualSlices(u8, before, after);
}

test "cli merge: --probe answers from a bare store, writes nothing, and exits 0 either way" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var root_c: std.process.RunResult = undefined;
    defer freeResult(gpa, &root_c);
    _ = try commitFile(gpa, io, tmp.dir, "f.txt", "top\nMID\nbottom\n", "root", &root_c);

    // A branch that merges cleanly at CONTENT level (different regions).
    try writeFile(io, tmp.dir, "f.txt", "top\nMID\nBOTTOM\n");
    var ok_b = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "b", "--branch", "feat/ok" });
    defer freeResult(gpa, &ok_b);
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back);
    try writeFile(io, tmp.dir, "f.txt", "TOP\nMID\nbottom\n");
    var t1 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "t" });
    defer freeResult(gpa, &t1);

    // ...and one that genuinely conflicts (add/add, differing).
    try writeFile(io, tmp.dir, "g.txt", "BRANCH\n");
    try addManaged(gpa, io, tmp.dir, &.{"g.txt"});
    var bad_b = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "g", "--branch", "feat/bad" });
    defer freeResult(gpa, &bad_b);
    var back2 = try runZit(gpa, io, tmp.dir, &.{ "update", "trunk" });
    defer freeResult(gpa, &back2);
    try writeFile(io, tmp.dir, "g.txt", "TRUNK\n");
    try addManaged(gpa, io, tmp.dir, &.{"g.txt"});
    var t2 = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "g2" });
    defer freeResult(gpa, &t2);

    var away = std.testing.tmpDir(.{});
    defer away.cleanup();
    try bareStoreOf(gpa, io, tmp.dir, away.dir, "store");
    var store_dir = try away.dir.openDir(io, "store", .{});
    defer store_dir.close(io);
    const before = try store_dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(before);

    var clean = try runZit(gpa, io, away.dir, &.{ "merge", "--probe", "feat/ok", "--into", "trunk", "-R", "store" });
    defer freeResult(gpa, &clean);
    try expectExitDiag(clean.term, 0, clean);
    try std.testing.expect(std.mem.indexOf(u8, clean.stdout, "merges   clean") != null);

    // "Has conflicts" is an ANSWER, not a failure: exit 0, and the paths are
    // on stdout as data. A caller reads the line, not the status.
    var bad = try runZit(gpa, io, away.dir, &.{ "merge", "--probe", "feat/bad", "--into", "trunk", "-R", "store" });
    defer freeResult(gpa, &bad);
    try expectExitDiag(bad.term, 0, bad);
    try std.testing.expect(std.mem.indexOf(u8, bad.stdout, "merges   conflict (1)") != null);
    try std.testing.expect(std.mem.indexOf(u8, bad.stdout, "conflict g.txt") != null);

    // Asking changed nothing. A probe that writes is not a probe.
    const after = try store_dir.readFileAlloc(io, ".zit", gpa, .limited(64 * 1024 * 1024));
    defer gpa.free(after);
    try std.testing.expectEqualSlices(u8, before, after);

    // -R needs --into: there is no checkout to supply the other side, and
    // guessing one would answer a question nobody asked.
    var no_into = try runZit(gpa, io, away.dir, &.{ "merge", "--probe", "feat/ok", "-R", "store" });
    defer freeResult(gpa, &no_into);
    try expectExit(no_into.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, no_into.stderr, "--into") != null);

    // A REAL merge still refuses -R, and says so by name rather than failing as
    // a discovery miss that tells you to pass the flag you just passed.
    var real = try runZit(gpa, io, away.dir, &.{ "merge", "feat/ok", "-R", "store" });
    defer freeResult(gpa, &real);
    try expectExit(real.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, real.stderr, "no working tree to merge into") != null);
}

test "cli merge: --probe agrees with the merge engine, and with landing show" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try seedContentMergeable(gpa, io, tmp.dir);

    // In a checkout, --into defaults to the current check-in, exactly as a
    // real merge takes its other side.
    var probe = try runZit(gpa, io, tmp.dir, &.{ "merge", "--probe", "feat/x" });
    defer freeResult(gpa, &probe);
    try expectExitDiag(probe.term, 0, probe);
    try std.testing.expect(std.mem.indexOf(u8, probe.stdout, "merges   clean") != null);

    // `landing show` must give the same verdict: one probe, two callers.
    var opened = try runZit(gpa, io, tmp.dir, &.{ "landing", "open", "--branch", "feat/x", "-m", "m", "--user", "u" });
    defer freeResult(gpa, &opened);
    var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", opened.stdout[0..12] });
    defer freeResult(gpa, &shown);
    try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "merges   clean") != null);

    // And the engine agrees with both.
    var merged = try runZit(gpa, io, tmp.dir, &.{ "merge", "feat/x" });
    defer freeResult(gpa, &merged);
    try expectExit(merged.term, 0);
}

/// Every artifact `dir` holds that `except` does not, as push
/// `file` cards, read through `zit frames` (whose framing is the push card's
/// own: a header line, then exactly SIZE raw bytes).
fn pushCardsFrom(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, except: []const []const u8) ![]u8 {
    var frames = try runZit(gpa, io, dir, &.{"frames"});
    defer freeResult(gpa, &frames);
    try expectExitDiag(frames.term, 0, frames);
    const out = frames.stdout;

    var cards: std.ArrayListUnmanaged(u8) = .empty;
    errdefer cards.deinit(gpa);
    var pos: usize = 0;
    while (pos < out.len) {
        const nl = std.mem.indexOfScalarPos(u8, out, pos, '\n') orelse break;
        const line = out[pos..nl];
        pos = nl + 1;
        if (std.mem.startsWith(u8, line, "cursor ")) break;
        if (!std.mem.startsWith(u8, line, "artifact ")) continue;
        var it = std.mem.tokenizeScalar(u8, line["artifact ".len..], ' ');
        const hash = it.next() orelse return error.TestUnexpectedResult;
        const size = try std.fmt.parseInt(usize, it.next() orelse return error.TestUnexpectedResult, 10);
        if (pos + size > out.len) return error.TestUnexpectedResult;
        const bytes = out[pos .. pos + size];
        pos += size;
        if (pos < out.len and out[pos] == '\n') pos += 1;
        // `except` holds raw digests, the frames listing prints hex: compare
        // in one alphabet or nothing is ever excluded and every held artifact
        // rides the push again.
        var skip = false;
        for (except) |e| {
            if (e.len == 32 and std.mem.eql(u8, &std.fmt.bytesToHex(e[0..32].*, .lower), hash)) skip = true;
            if (std.mem.eql(u8, e, hash)) skip = true;
        }
        if (skip) continue;
        var header: [128]u8 = undefined;
        try cards.appendSlice(gpa, try std.fmt.bufPrint(&header, "file {s} {d}\n", .{ hash, size }));
        try cards.appendSlice(gpa, bytes);
    }
    return cards.toOwnedSlice(gpa);
}

fn projectCodeOf(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]u8 {
    var stat = try runZit(gpa, io, dir, &.{"stat"});
    defer freeResult(gpa, &stat);
    try expectExitDiag(stat.term, 0, stat);
    var lines = std.mem.tokenizeScalar(u8, stat.stdout, '\n');
    while (lines.next()) |l| {
        if (std.mem.startsWith(u8, l, "project-code ")) return gpa.dupe(u8, std.mem.trim(u8, l["project-code ".len..], " "));
    }
    return error.TestUnexpectedResult;
}

fn hasDigest(set: []const []const u8, want: []const u8) bool {
    for (set) |d| if (std.mem.eql(u8, d, want)) return true;
    return false;
}

test "cli serve: a fronted server refuses a push that would join a protected branch and stores nothing; without --fronted the header is ignored" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // A: the served repository, one seed check-in on trunk.
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "one.txt", "seed\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"one.txt"});
    var seed = try runCommitEnv(gpa, io, tmp_a.dir, .{ .user = "u" }, &.{ "commit", "-m", "seed" });
    defer freeResult(gpa, &seed);
    try expectExit(seed.term, 0);

    // B: a clone that grows by one trunk check-in — the thing to be refused.
    const clone_port = try freshLoopbackPort(io);
    var clone_server = try spawnServe(gpa, io, tmp_a.dir, clone_port);
    try waitForListen(io, clone_port);
    const clone_url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{clone_port});
    defer gpa.free(clone_url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", clone_url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    clone_server.kill(io);
    var b_repo = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_repo.close(io);
    try writeFile(io, b_repo, "two.txt", "grows trunk\n");
    try addManaged(gpa, io, b_repo, &.{"two.txt"});
    var grow = try runCommitEnv(gpa, io, b_repo, .{ .user = "u" }, &.{ "commit", "-m", "grow" });
    defer freeResult(gpa, &grow);
    try expectExit(grow.term, 0);

    const a_before = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_before);
    const b_all = try collectArtifactDigests(gpa, io, b_repo);
    defer freeDigests(gpa, b_all);
    try std.testing.expect(b_all.len > a_before.len);

    // The push B's client would send, as raw cards, so a header can ride it.
    const files = try pushCardsFrom(gpa, io, b_repo, a_before);
    defer gpa.free(files);
    const project = try projectCodeOf(gpa, io, b_repo);
    defer gpa.free(project);
    const request = try std.fmt.allocPrint(gpa, "pragma client-version 20000\npush {s} {s}\n{s}", .{ "c" ** 40, project, files });
    defer gpa.free(request);

    // Fronted, with the front's rule: refused, naming trunk, and A is byte-for-
    // byte what it was.
    {
        const port = try freshLoopbackPort(io);
        var server = try spawnServeFronted(gpa, io, tmp_a.dir, port, true);
        defer server.kill(io);
        try waitForListen(io, port);
        const reply = try postXferCardsHeaders(gpa, io, port, request, "X-Zit-Protect: trunk\r\n");
        defer gpa.free(reply);
        try std.testing.expect(std.mem.startsWith(u8, reply, "error "));
        try std.testing.expect(std.mem.indexOf(u8, reply, "'trunk'") != null);
        server.kill(io);
        const a_after = try collectArtifactDigests(gpa, io, tmp_a.dir);
        defer freeDigests(gpa, a_after);
        try std.testing.expectEqual(a_before.len, a_after.len);
    }
    // Not fronted, same header: ignored. The push lands and A holds B's set.
    {
        const port = try freshLoopbackPort(io);
        var server = try spawnServe(gpa, io, tmp_a.dir, port);
        defer server.kill(io);
        try waitForListen(io, port);
        const reply = try postXferCardsHeaders(gpa, io, port, request, "X-Zit-Protect: trunk\r\n");
        defer gpa.free(reply);
        try std.testing.expect(!std.mem.startsWith(u8, reply, "error "));
        server.kill(io);
        const a_after = try collectArtifactDigests(gpa, io, tmp_a.dir);
        defer freeDigests(gpa, a_after);
        for (b_all) |d| try std.testing.expect(hasDigest(a_after, d));
    }
}

test "cli serve: a refused check-in amended onto another branch lands there, while a held trunk check-in still cannot be moved" {
    // After a refused push the obvious recovery is `zit amend X --branch other`,
    // which cancels `sym-trunk` on X from a control artifact. A cancel aimed at
    // a check-in the server does not hold removes it from nothing held, so it is
    // admitted and X is judged by where it joins — with the move applied, since
    // the two travel in one round. Before this, the cancel was refused by name
    // alone and every later push from that checkout carried the same refused
    // round: a dead end only a fresh clone escaped.
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "one.txt", "seed\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"one.txt"});
    const seed = try commitHashDup(gpa, io, tmp_a.dir, "seed");
    defer gpa.free(seed);

    const clone_port = try freshLoopbackPort(io);
    var clone_server = try spawnServe(gpa, io, tmp_a.dir, clone_port);
    try waitForListen(io, clone_port);
    const clone_url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{clone_port});
    defer gpa.free(clone_url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", clone_url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    clone_server.kill(io);
    var b_repo = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_repo.close(io);
    try writeFile(io, b_repo, "two.txt", "grows trunk\n");
    try addManaged(gpa, io, b_repo, &.{"two.txt"});
    const grow = try commitHashDup(gpa, io, b_repo, "grow");
    defer gpa.free(grow);

    const a_before = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_before);
    const project = try projectCodeOf(gpa, io, b_repo);
    defer gpa.free(project);

    // The recovery: move the refused check-in, then push the same set again.
    // Strictly later than the pinned check-in time: a retag that ties its
    // target's timestamp does not win, in fossil or here.
    var am = try runCommitEnv(gpa, io, b_repo, .{ .user = "u", .epoch = "1234567891" }, &.{ "amend", grow, "--branch", "other" });
    defer freeResult(gpa, &am);
    try expectExitDiag(am.term, 0, am);
    {
        const files = try pushCardsFrom(gpa, io, b_repo, a_before);
        defer gpa.free(files);
        const request = try std.fmt.allocPrint(gpa, "pragma client-version 20000\npush {s} {s}\n{s}", .{ "c" ** 40, project, files });
        defer gpa.free(request);
        const port = try freshLoopbackPort(io);
        var server = try spawnServeFronted(gpa, io, tmp_a.dir, port, true);
        defer server.kill(io);
        try waitForListen(io, port);
        const reply = try postXferCardsHeaders(gpa, io, port, request, "X-Zit-Protect: trunk\r\n");
        defer gpa.free(reply);
        try std.testing.expect(!std.mem.startsWith(u8, reply, "error "));
        server.kill(io);
    }
    // A holds the check-in on `other`, and trunk's tip is still the seed.
    {
        var info = try runZit(gpa, io, tmp_a.dir, &.{ "info", grow });
        defer freeResult(gpa, &info);
        try expectExitDiag(info.term, 0, info);
        try std.testing.expect(std.mem.indexOf(u8, info.stdout, "branch:  other\n") != null);
        const trunk_tip = try tipHashOf(gpa, io, tmp_a.dir, "trunk");
        defer gpa.free(trunk_tip);
        try std.testing.expectEqualStrings(seed, trunk_tip);
    }

    // Not a loosening: the same move on a check-in A HOLDS on trunk is still
    // refused, because cancelling its sym is exactly how it would leave.
    const a_mid = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_mid);
    var am2 = try runCommitEnv(gpa, io, b_repo, .{ .user = "u", .epoch = "1234567892" }, &.{ "amend", seed, "--branch", "other" });
    defer freeResult(gpa, &am2);
    try expectExitDiag(am2.term, 0, am2);
    {
        const files = try pushCardsFrom(gpa, io, b_repo, a_mid);
        defer gpa.free(files);
        const request = try std.fmt.allocPrint(gpa, "pragma client-version 20000\npush {s} {s}\n{s}", .{ "c" ** 40, project, files });
        defer gpa.free(request);
        const port = try freshLoopbackPort(io);
        var server = try spawnServeFronted(gpa, io, tmp_a.dir, port, true);
        defer server.kill(io);
        try waitForListen(io, port);
        const reply = try postXferCardsHeaders(gpa, io, port, request, "X-Zit-Protect: trunk\r\n");
        defer gpa.free(reply);
        try std.testing.expect(std.mem.startsWith(u8, reply, "error "));
        try std.testing.expect(std.mem.indexOf(u8, reply, "'trunk'") != null);
        server.kill(io);
        const a_after = try collectArtifactDigests(gpa, io, tmp_a.dir);
        defer freeDigests(gpa, a_after);
        try std.testing.expectEqual(a_mid.len, a_after.len);
    }
}

test "cli serve: a branch check-in amended onto a protected branch is refused, and amended off it again lands" {
    // The pusher's own move onto trunk travels with every later push, so the
    // way out has to be judged by where the check-in ends up after all of its
    // moves, newest winning, the way the derive itself will place it. Judged
    // by the moves' names alone, the move onto trunk refused every push from
    // that checkout.
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "one.txt", "seed\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"one.txt"});
    const seed = try commitHashDup(gpa, io, tmp_a.dir, "seed");
    defer gpa.free(seed);

    const clone_port = try freshLoopbackPort(io);
    var clone_server = try spawnServe(gpa, io, tmp_a.dir, clone_port);
    try waitForListen(io, clone_port);
    const clone_url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{clone_port});
    defer gpa.free(clone_url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", clone_url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    clone_server.kill(io);
    var b_repo = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_repo.close(io);
    try writeFile(io, b_repo, "two.txt", "on a branch\n");
    try addManaged(gpa, io, b_repo, &.{"two.txt"});
    var ci = try runCommitEnv(gpa, io, b_repo, .{ .user = "u", .epoch = "1234567891" }, &.{ "commit", "-m", "feature work", "--branch", "feat" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    const work = try gpa.dupe(u8, try checkinHash(ci.stdout));
    defer gpa.free(work);

    const a_before = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_before);
    const project = try projectCodeOf(gpa, io, b_repo);
    defer gpa.free(project);

    var onto = try runCommitEnv(gpa, io, b_repo, .{ .user = "u", .epoch = "1234567892" }, &.{ "amend", work, "--branch", "trunk" });
    defer freeResult(gpa, &onto);
    try expectExitDiag(onto.term, 0, onto);
    const refused = try pushProtected(gpa, io, tmp_a.dir, b_repo, a_before, project);
    defer gpa.free(refused);
    try std.testing.expect(std.mem.startsWith(u8, refused, "error "));
    try std.testing.expect(std.mem.indexOf(u8, refused, "'trunk'") != null);
    {
        const a_after = try collectArtifactDigests(gpa, io, tmp_a.dir);
        defer freeDigests(gpa, a_after);
        try std.testing.expectEqual(a_before.len, a_after.len);
    }

    var off = try runCommitEnv(gpa, io, b_repo, .{ .user = "u", .epoch = "1234567893" }, &.{ "amend", work, "--branch", "other" });
    defer freeResult(gpa, &off);
    try expectExitDiag(off.term, 0, off);
    const landed = try pushProtected(gpa, io, tmp_a.dir, b_repo, a_before, project);
    defer gpa.free(landed);
    try std.testing.expect(!std.mem.startsWith(u8, landed, "error "));
    {
        var info = try runZit(gpa, io, tmp_a.dir, &.{ "info", work });
        defer freeResult(gpa, &info);
        try expectExitDiag(info.term, 0, info);
        try std.testing.expect(std.mem.indexOf(u8, info.stdout, "branch:  other\n") != null);
        const trunk_tip = try tipHashOf(gpa, io, tmp_a.dir, "trunk");
        defer gpa.free(trunk_tip);
        try std.testing.expectEqualStrings(seed, trunk_tip);
    }
}

test "cli serve: a fronted divert refuses a push onto trunk once, then keeps it on an attempt branch and tells the front" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "one.txt", "seed\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"one.txt"});
    const seed = try commitHashDup(gpa, io, tmp_a.dir, "seed");
    defer gpa.free(seed);

    const clone_port = try freshLoopbackPort(io);
    var clone_server = try spawnServe(gpa, io, tmp_a.dir, clone_port);
    try waitForListen(io, clone_port);
    const clone_url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{clone_port});
    defer gpa.free(clone_url);
    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", clone_url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    clone_server.kill(io);
    var b_repo = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_repo.close(io);
    try writeFile(io, b_repo, "two.txt", "grows trunk\n");
    try addManaged(gpa, io, b_repo, &.{"two.txt"});
    const grow = try commitHashDup(gpa, io, b_repo, "grow");
    defer gpa.free(grow);
    const a_before = try collectArtifactDigests(gpa, io, tmp_a.dir);
    defer freeDigests(gpa, a_before);
    const project = try projectCodeOf(gpa, io, b_repo);
    defer gpa.free(project);
    const headers = "X-Zit-Protect: trunk\r\nX-Zit-Divert-Login: jan\r\n";

    {
        const got = try pushProtectedHead(gpa, io, tmp_a.dir, b_repo, a_before, project, headers);
        defer gpa.free(got.head);
        defer gpa.free(got.body);
        try std.testing.expect(std.mem.startsWith(u8, got.body, "error "));
        try std.testing.expect(std.mem.indexOf(u8, got.body, "attempt/jan/") != null);
        try std.testing.expect(std.ascii.indexOfIgnoreCase(got.head, "x-zit-diverted") == null);
        const a_after = try collectArtifactDigests(gpa, io, tmp_a.dir);
        defer freeDigests(gpa, a_after);
        try std.testing.expectEqual(a_before.len, a_after.len);
    }
    {
        const got = try pushProtectedHead(gpa, io, tmp_a.dir, b_repo, a_before, project, headers);
        defer gpa.free(got.head);
        defer gpa.free(got.body);
        try std.testing.expect(!std.mem.startsWith(u8, got.body, "error "));
        try std.testing.expect(std.mem.indexOf(u8, got.body, "pragma zit-diverted ") != null);
        const want = try std.fmt.allocPrint(gpa, "x-zit-diverted: {s} attempt/jan/{s} trunk", .{ grow, grow[0..12] });
        defer gpa.free(want);
        try std.testing.expect(std.ascii.indexOfIgnoreCase(got.head, want) != null);
    }
    {
        var info = try runZit(gpa, io, tmp_a.dir, &.{ "info", grow });
        defer freeResult(gpa, &info);
        try expectExitDiag(info.term, 0, info);
        const want = try std.fmt.allocPrint(gpa, "branch:  attempt/jan/{s}\n", .{grow[0..12]});
        defer gpa.free(want);
        try std.testing.expect(std.mem.indexOf(u8, info.stdout, want) != null);
        const trunk_tip = try tipHashOf(gpa, io, tmp_a.dir, "trunk");
        defer gpa.free(trunk_tip);
        try std.testing.expectEqualStrings(seed, trunk_tip);
    }
}

/// A stand-in for a front such as quarry: listens on loopback, adds
/// `headers` to every request it receives, forwards it to a `zit serve
/// --fronted` on `upstream`, and copies the reply back. One request per
/// connection, as both ends close after a reply. `stop` ends it.
const FrontProxy = struct {
    io: std.Io,
    listener: net.Server,
    port: u16,
    upstream: u16,
    headers: []const u8,
    stopping: std.atomic.Value(bool) = .init(false),
    thread: std.Thread = undefined,

    fn start(self: *FrontProxy) !void {
        self.thread = try std.Thread.spawn(.{}, run, .{self});
    }

    fn stop(self: *FrontProxy) void {
        self.stopping.store(true, .release);
        // Unblock the accept so the loop sees the flag.
        if (dialServe(self.io, self.port)) |s| s.close(self.io) else |_| {}
        self.thread.join();
        self.listener.deinit(self.io);
    }

    fn run(self: *FrontProxy) void {
        while (!self.stopping.load(.acquire)) {
            const conn = self.listener.accept(self.io) catch return;
            defer conn.close(self.io);
            if (self.stopping.load(.acquire)) return;
            self.relay(conn) catch {};
        }
    }

    fn relay(self: *FrontProxy, conn: net.Stream) !void {
        const a = std.heap.page_allocator;
        var rbuf: [16 * 1024]u8 = undefined;
        var r = conn.reader(self.io, &rbuf);
        var head: std.ArrayListUnmanaged(u8) = .empty;
        defer head.deinit(a);
        var body_len: usize = 0;
        while (true) {
            const line = try r.interface.takeDelimiterInclusive('\n');
            if (std.mem.eql(u8, line, "\r\n")) break;
            const colon = std.mem.indexOfScalar(u8, line, ':') orelse 0;
            if (colon != 0 and std.ascii.eqlIgnoreCase(line[0..colon], "content-length")) {
                body_len = try std.fmt.parseInt(usize, std.mem.trim(u8, line[colon + 1 ..], " \r\n"), 10);
            }
            try head.appendSlice(a, line);
        }
        try head.appendSlice(a, self.headers);
        try head.appendSlice(a, "\r\n");
        const body = try r.interface.readAlloc(a, body_len);
        defer a.free(body);

        const up = try dialServe(self.io, self.upstream);
        defer up.close(self.io);
        var ubuf: [4096]u8 = undefined;
        var uw = up.writer(self.io, &ubuf);
        try uw.interface.writeAll(head.items);
        try uw.interface.writeAll(body);
        try uw.interface.flush();
        var urbuf: [16 * 1024]u8 = undefined;
        var ur = up.reader(self.io, &urbuf);
        const reply = try ur.interface.allocRemaining(a, .limited(64 << 20));
        defer a.free(reply);
        var wbuf: [4096]u8 = undefined;
        var w = conn.writer(self.io, &wbuf);
        try w.interface.writeAll(reply);
        try w.interface.flush();
    }
};

test "cli push: behind a front that diverts, a commit is refused with the way out, and pushing again prints where it went and pulls it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "one.txt", "seed\n");
    try addManaged(gpa, io, tmp_a.dir, &.{"one.txt"});
    const seed = try commitHashDup(gpa, io, tmp_a.dir, "seed");
    defer gpa.free(seed);

    const serve_port = try freshLoopbackPort(io);
    var server = try spawnServeFronted(gpa, io, tmp_a.dir, serve_port, true);
    defer server.kill(io);
    try waitForListen(io, serve_port);
    const addr = try net.IpAddress.parseIp4("127.0.0.1", 0);
    var front = FrontProxy{
        .io = io,
        .listener = try addr.listen(io, .{}),
        .port = 0,
        .upstream = serve_port,
        .headers = "X-Zit-Protect: trunk\r\nX-Zit-Divert-Login: jan\r\n",
    };
    front.port = front.listener.socket.address.getPort();
    try front.start();
    defer front.stop();
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{front.port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", url, "repo" });
    defer freeResult(gpa, &clone);
    try expectExitDiag(clone.term, 0, clone);
    var b_repo = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_repo.close(io);

    // The commit is kept; its autosync push is refused with the way out.
    try writeFile(io, b_repo, "two.txt", "grows trunk\n");
    try addManaged(gpa, io, b_repo, &.{"two.txt"});
    var ci = try runCommitEnv(gpa, io, b_repo, .{ .user = "u" }, &.{ "commit", "-m", "grow" });
    defer freeResult(gpa, &ci);
    try expectExitDiag(ci.term, 0, ci);
    const grow = try gpa.dupe(u8, try checkinHash(ci.stdout));
    defer gpa.free(grow);
    const attempt = try std.fmt.allocPrint(gpa, "attempt/jan/{s}", .{grow[0..12]});
    defer gpa.free(attempt);
    try std.testing.expect(std.mem.indexOf(u8, ci.stderr, "the peer refused") != null);
    try std.testing.expect(std.mem.indexOf(u8, ci.stderr, attempt) != null);
    try std.testing.expect(std.mem.indexOf(u8, ci.stderr, "autosync push failed; continuing locally") != null);

    // Pushed again: the server's message is printed, and the move is pulled
    // so this repository agrees with the server at once.
    var push = try runZit(gpa, io, b_repo, &.{"push"});
    defer freeResult(gpa, &push);
    try expectExitDiag(push.term, 0, push);
    try std.testing.expect(std.mem.indexOf(u8, push.stderr, "server: check-in ") != null);
    try std.testing.expect(std.mem.indexOf(u8, push.stderr, attempt) != null);
    try std.testing.expect(std.mem.indexOf(u8, push.stdout, "pull from ") != null);
    var cur = try runZit(gpa, io, b_repo, &.{ "branch", "current" });
    defer freeResult(gpa, &cur);
    try expectExitDiag(cur.term, 0, cur);
    try std.testing.expectEqualStrings(attempt, std.mem.trimEnd(u8, cur.stdout, "\n"));

    const trunk_tip = try tipHashOf(gpa, io, tmp_a.dir, "trunk");
    defer gpa.free(trunk_tip);
    try std.testing.expectEqualStrings(seed, trunk_tip);
}

/// `pushProtected` with the front's headers spelled out, keeping the
/// response head.
fn pushProtectedHead(gpa: std.mem.Allocator, io: std.Io, served: std.Io.Dir, from: std.Io.Dir, except: []const []const u8, project: []const u8, headers: []const u8) !XferHead {
    const files = try pushCardsFrom(gpa, io, from, except);
    defer gpa.free(files);
    const request = try std.fmt.allocPrint(gpa, "pragma client-version 20000\npush {s} {s}\n{s}", .{ "c" ** 40, project, files });
    defer gpa.free(request);
    const port = try freshLoopbackPort(io);
    var server = try spawnServeFronted(gpa, io, served, port, true);
    defer server.kill(io);
    try waitForListen(io, port);
    return postXferCardsHead(gpa, io, port, request, headers);
}

/// Push everything `from` holds beyond `except` to a fronted server over
/// `served`, with trunk protected, and return the reply's cards.
fn pushProtected(gpa: std.mem.Allocator, io: std.Io, served: std.Io.Dir, from: std.Io.Dir, except: []const []const u8, project: []const u8) ![]u8 {
    const files = try pushCardsFrom(gpa, io, from, except);
    defer gpa.free(files);
    const request = try std.fmt.allocPrint(gpa, "pragma client-version 20000\npush {s} {s}\n{s}", .{ "c" ** 40, project, files });
    defer gpa.free(request);
    const port = try freshLoopbackPort(io);
    var server = try spawnServeFronted(gpa, io, served, port, true);
    defer server.kill(io);
    try waitForListen(io, port);
    return postXferCardsHeaders(gpa, io, port, request, "X-Zit-Protect: trunk\r\n");
}

/// The first line of `zit info NAME`: the check-in's hash.
fn tipHashOf(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, name: []const u8) ![]u8 {
    var info = try runZit(gpa, io, dir, &.{ "info", name });
    defer freeResult(gpa, &info);
    try expectExitDiag(info.term, 0, info);
    const nl = std.mem.indexOfScalar(u8, info.stdout, '\n') orelse info.stdout.len;
    return gpa.dupe(u8, info.stdout[0..nl]);
}

/// Rewrite one line of `shared.txt` (the seedStack file) and commit
/// on whatever branch the checkout is on.
fn editSharedLine(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, index: usize, text: []const u8, msg: []const u8) !void {
    const cur = try readFile(gpa, io, dir, "shared.txt");
    defer gpa.free(cur);
    var out: std.ArrayList(u8) = .empty;
    defer out.deinit(gpa);
    var it = std.mem.splitScalar(u8, cur, '\n');
    var i: usize = 0;
    while (it.next()) |line| : (i += 1) {
        if (line.len == 0 and it.rest().len == 0) break;
        try out.print(gpa, "{s}\n", .{if (i == index) text else line});
    }
    try writeFile(io, dir, "shared.txt", out.items);
    var c = try runCommitEnv(gpa, io, dir, .{ .user = "u" }, &.{ "commit", "-m", msg });
    defer freeResult(gpa, &c);
    try expectExit(c.term, 0);
}

test "cli landing: update brings the target into the branch on the store, records it, and leaves the checkout alone" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    // seg1 -> trunk, seg2 -> seg1; the checkout is left on trunk.
    const depth = 2;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    // trunk moves, in a file no segment touches -- a hunk adjacent to a
    // segment's line would be a legitimate conflict, not the clean case.
    try writeFile(io, tmp.dir, "trunk.txt", "trunk moved on\n");
    try addManaged(gpa, io, tmp.dir, &.{"trunk.txt"});
    var mv = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "trunk moves" });
    defer freeResult(gpa, &mv);
    try expectExit(mv.term, 0);
    const trunk_tip = try tipHashOf(gpa, io, tmp.dir, "trunk");
    defer gpa.free(trunk_tip);
    const seg1_before = try tipHashOf(gpa, io, tmp.dir, "seg1");
    defer gpa.free(seg1_before);

    // From the checkout, with no -R: the merge still happens in an ephemeral
    // working area, and this tree is not touched.
    var up = try runZit(gpa, io, tmp.dir, &.{ "landing", "update", ids[0][0..12], "--user", "u" });
    defer freeResult(gpa, &up);
    try expectExitDiag(up.term, 0, up);
    try std.testing.expect(std.mem.startsWith(u8, up.stdout, "updated "));

    // One merge check-in on seg1: old seg1 tip as the primary parent, trunk's
    // tip as the other.
    const seg1_after = try tipHashOf(gpa, io, tmp.dir, "seg1");
    defer gpa.free(seg1_after);
    try std.testing.expect(!std.mem.eql(u8, seg1_before, seg1_after));
    var art = try runZit(gpa, io, tmp.dir, &.{ "artifact", seg1_after, "--raw" });
    defer freeResult(gpa, &art);
    try expectExit(art.term, 0);
    const p_at = std.mem.indexOf(u8, art.stdout, "\nP ") orelse return error.TestUnexpectedResult;
    const p_end = std.mem.indexOfScalarPos(u8, art.stdout, p_at + 3, '\n') orelse art.stdout.len;
    var parents = std.mem.tokenizeScalar(u8, art.stdout[p_at + 3 .. p_end], ' ');
    try std.testing.expectEqualStrings(seg1_before, parents.next() orelse return error.TestUnexpectedResult);
    try std.testing.expectEqualStrings(trunk_tip, parents.next() orelse return error.TestUnexpectedResult);
    try std.testing.expect(parents.next() == null);

    // The record carries the refresh: the second output line is the reply,
    // whose body opens with the marker and the merge hash.
    const nl = std.mem.indexOfScalar(u8, up.stdout, '\n') orelse return error.TestUnexpectedResult;
    const reply = std.mem.trim(u8, up.stdout[nl + 1 ..], "\n");
    var post = try runZit(gpa, io, tmp.dir, &.{ "artifact", reply, "--raw" });
    defer freeResult(gpa, &post);
    try expectExit(post.term, 0);
    const marker_line = try std.fmt.allocPrint(gpa, "zit-landing-updated: {s}", .{seg1_after});
    defer gpa.free(marker_line);
    try std.testing.expect(std.mem.indexOf(u8, post.stdout, marker_line) != null);

    // The checkout: still on trunk, nothing to report.
    var st = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &st);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "tags:         trunk") != null);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "nothing to report") != null);

    // Both probes now read clean: seg1 against trunk's new tip, and the rung
    // stacked on seg1 against seg1's new tip.
    for (ids) |id| {
        var shown = try runZit(gpa, io, tmp.dir, &.{ "landing", "show", id[0..12] });
        defer freeResult(gpa, &shown);
        try expectExit(shown.term, 0);
        try std.testing.expect(std.mem.indexOf(u8, shown.stdout, "merges   clean") != null);
    }

    // Again: nothing to do, nothing written, exit 0.
    var again = try runZit(gpa, io, tmp.dir, &.{ "landing", "update", ids[0][0..12], "--user", "u" });
    defer freeResult(gpa, &again);
    try expectExitDiag(again.term, 0, again);
    try std.testing.expect(std.mem.indexOf(u8, again.stdout, "already contains") != null);
    const seg1_final = try tipHashOf(gpa, io, tmp.dir, "seg1");
    defer gpa.free(seg1_final);
    try std.testing.expectEqualStrings(seg1_after, seg1_final);
}

test "cli landing: a conflicting update writes nothing and names the path; a settled record is refused" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    const depth = 1;
    var ids: [depth][]u8 = undefined;
    try seedStack(gpa, io, tmp.dir, depth, &ids);
    defer for (ids) |id| gpa.free(id);

    // trunk rewrites the very line seg1 rewrote.
    try editSharedLine(gpa, io, tmp.dir, 1, "TRUNK1", "trunk collides");
    const before = try tipHashOf(gpa, io, tmp.dir, "seg1");
    defer gpa.free(before);

    var up = try runZit(gpa, io, tmp.dir, &.{ "landing", "update", ids[0][0..12], "--user", "u", "-R", ".zit" });
    defer freeResult(gpa, &up);
    try expectExit(up.term, 5);
    try std.testing.expect(std.mem.indexOf(u8, up.stderr, "shared.txt") != null);
    try std.testing.expect(std.mem.indexOf(u8, up.stderr, "nothing was written") != null);
    const after = try tipHashOf(gpa, io, tmp.dir, "seg1");
    defer gpa.free(after);
    try std.testing.expectEqualStrings(before, after);

    var rej = try runZit(gpa, io, tmp.dir, &.{ "landing", "reject", ids[0][0..12], "-m", "no", "--user", "u" });
    defer freeResult(gpa, &rej);
    try expectExit(rej.term, 0);
    var settled = try runZit(gpa, io, tmp.dir, &.{ "landing", "update", ids[0][0..12], "--user", "u" });
    defer freeResult(gpa, &settled);
    try expectExit(settled.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, settled.stderr, "is already rejected") != null);
}

test "cli: an empty check-in name is refused by every name-taking verb, never a crash" {
    // `zit update ""` reached the store resolver's non-empty precondition and
    // died (a segfault in release builds). Every verb that resolves a name
    // must answer an empty one with a diagnostic and a non-zero exit.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var c = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "u" }, &.{ "commit", "-m", "one" });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);

    const cases = [_][]const []const u8{
        &.{ "update", "" },
        &.{ "merge", "" },
        &.{ "info", "" },
        &.{ "ls", "" },
        &.{ "cat", "a.txt", "" },
        &.{ "annotate", "a.txt", "" },
        &.{ "log", "" },
        &.{ "tag", "list", "" },
        &.{ "branch", "new", "x", "" },
        &.{ "diff", "--from", "", "--to", "tip" },
    };
    for (cases) |argv| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        const code: u8 = switch (res.term) {
            .exited => |n| n,
            else => 255, // a signal (the crash this test exists to catch)
        };
        if (code == 0 or code > 2 or res.stderr.len == 0) {
            std.debug.print("\nzit {s}: term {any}, stdout {s}, stderr {s}\n", .{ argv[0], res.term, res.stdout, res.stderr });
            return error.EmptyNameNotRefused;
        }
    }
}

/// The `ZIT_MANIFEST_READS` count a verb reports, or null when it did not
/// report one. The seam prints on the last line of stderr.
fn manifestReadsOf(res: *const std.process.RunResult) ?u64 {
    const marker = "manifest-reads: ";
    const at = std.mem.lastIndexOf(u8, res.stderr, marker) orelse return null;
    const rest = res.stderr[at + marker.len ..];
    const end = std.mem.indexOfScalar(u8, rest, '\n') orelse rest.len;
    return std.fmt.parseInt(u64, std.mem.trim(u8, rest[0..end], " \r"), 10) catch null;
}

/// Build a repository of `commits` check-ins, each editing one tracked file.
fn seedLinearHistory(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, commits: usize) !void {
    try initRepo(gpa, io, dir);
    try writeFile(io, dir, "a.txt", "0\n");
    // Committed once and never touched again: `annotate stable.txt` must not
    // read more manifests just because history grew AROUND the file.
    try writeFile(io, dir, "stable.txt", "one\ntwo\n");
    try addManaged(gpa, io, dir, &.{ "a.txt", "stable.txt" });
    for (0..commits) |i| {
        var buf: [32]u8 = undefined;
        try writeFile(io, dir, "a.txt", try std.fmt.bufPrint(&buf, "line {d}\n", .{i}));
        var msg: [32]u8 = undefined;
        var res = try runCommitEnv(gpa, io, dir, .{}, &.{ "commit", "-m", try std.fmt.bufPrint(&msg, "c{d}", .{i}) });
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
    }
}

test "cli timeline: renders the same from the image as from a derive" {
    // Timeline reads the zero-parse image now; every column it renders must be
    // there. A merge (the §3.2 mark), a tag, and two authors cover the row
    // surface across every format, with the cache present and then removed.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var fx = try buildMergeDiamond(gpa, io, tmp.dir, "A\nB\nC\n", "A\nB\nC1\n", "A1\nB\nC\n");
    defer fx.deinit(gpa);
    var mg = try runZit(gpa, io, tmp.dir, &.{ "merge", fx.other });
    defer freeResult(gpa, &mg);
    try expectExitDiag(mg.term, 0, mg);
    var mc = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "merger" }, &.{ "commit", "-m", "merge other" });
    defer freeResult(gpa, &mc);
    try expectExitDiag(mc.term, 0, mc);
    var tg = try runCommitEnv(gpa, io, tmp.dir, .{ .user = "merger" }, &.{ "tag", "add", "v1", "tip" });
    defer freeResult(gpa, &tg);
    try expectExitDiag(tg.term, 0, tg);

    const forms = [_][]const []const u8{
        &.{ "timeline", "-n", "0" },
        &.{ "timeline", "-n", "0", "-F", "full" },
        &.{ "timeline", "-n", "0", "-v" },
        &.{ "timeline", "-n", "0", "-F", "html" },
        &.{ "timeline", "-n", "0", "-F", "oneline" },
        &.{ "timeline", "-n", "0", "-p", "poem" },
    };
    var warm: [forms.len][]u8 = undefined;
    for (forms, 0..) |argv, i| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expect(std.mem.indexOf(u8, res.stdout, "merge other") != null);
        warm[i] = try gpa.dupe(u8, res.stdout);
    }
    defer for (warm) |w| gpa.free(w);
    // No cache: the same open derives from the log instead of the image.
    try tmp.dir.deleteTree(io, ".zit-cache");
    for (forms, 0..) |argv, i| {
        var res = try runZit(gpa, io, tmp.dir, argv);
        defer freeResult(gpa, &res);
        try expectExitDiag(res.term, 0, res);
        try std.testing.expectEqualStrings(warm[i], res.stdout);
        // Rewritten by that derive; the next form reads the image again.
        try tmp.dir.deleteTree(io, ".zit-cache");
    }
}

test "cli: a listing's manifest reads do not grow with history" {
    // The regression this pins is invisible to every other test: a verb that
    // reads a manifest per row prints exactly the right answer, just
    // proportional to the store. It cost `zit leaves` a second on SQLite and
    // `zit timeline` sixty-six, and neither showed up as a failure — only as a
    // slow afternoon. Wall time cannot pin it (a loaded machine varies by a
    // third); the read COUNT can, and the sharpest form needs no magic
    // constant: run the same verb over two histories of different lengths and
    // require the count to be identical. A per-row read fails that by
    // construction, whatever the absolute numbers are.
    const gpa = std.testing.allocator;
    const io = std.testing.io;

    // Flat means IDENTICAL across history sizes, not zero: `info` and `ls`
    // legitimately read the one manifest they render, and 1 == 1 passes.
    const listings = [_][]const []const u8{
        &.{ "leaves", "--all" },
        &.{ "timeline", "-n", "0" },
        &.{ "timeline", "-n", "0", "-F", "oneline" },
        &.{ "timeline", "-n", "0", "-F", "medium" },
        &.{ "timeline", "-n", "0", "-F", "html" },
        &.{ "branch", "-v" },
        &.{ "tag", "list" },
        &.{"status"},
        &.{ "info", "tip" },
        &.{"info"}, // bare form: infoCheckout's own fast_read open
        &.{"ls"},
        &.{ "annotate", "stable.txt" },
        &.{ "grep", "one", "stable.txt" }, // one version, one read
    };

    var counts: [2][listings.len]u64 = undefined;
    for ([2]usize{ 4, 12 }, 0..) |commits, size_idx| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        try seedLinearHistory(gpa, io, tmp.dir, commits);
        for (listings, 0..) |argv, i| {
            var res = try runZitEnv(gpa, io, tmp.dir, &.{.{ "ZIT_MANIFEST_READS", "1" }}, argv);
            defer freeResult(gpa, &res);
            try expectExitDiag(res.term, 0, res);
            counts[size_idx][i] = manifestReadsOf(&res) orelse {
                std.debug.print("\n{s}: no manifest-reads report\n", .{argv[0]});
                return error.NoReadCount;
            };
        }
    }

    for (listings, 0..) |argv, i| {
        if (counts[0][i] != counts[1][i]) {
            std.debug.print(
                "\n{s} {s}: manifest reads grew with history — {d} at 4 check-ins, {d} at 12.\n" ++
                    "A listing renders comment, user, branch and tags from the derived state;\n" ++
                    "a read per row means a verb fell back to the artifact for one of them.\n",
                .{ argv[0], if (argv.len > 1) argv[argv.len - 1] else "", counts[0][i], counts[1][i] },
            );
            return error.ManifestReadsScaleWithHistory;
        }
    }
}

test "checkout settings: every tree-scanning verb hides dot names by default and shows them on request" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, "plain.txt");
    try touch(io, tmp.dir, ".hidden");
    try tmp.dir.createDir(io, ".dir", .default_dir);
    try touch(io, tmp.dir, ".dir/inside");
    try tmp.dir.createDir(io, "sub", .default_dir);
    try touch(io, tmp.dir, "sub/.deep");

    // extras
    {
        var ex = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try std.testing.expectEqualStrings("plain.txt\n", ex.stdout);
    }
    {
        var ex = try runZit(gpa, io, tmp.dir, &.{ "extras", "--dotfiles" });
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try std.testing.expectEqualStrings(".dir/inside\n.hidden\nplain.txt\nsub/.deep\n", ex.stdout);
    }

    // status --extra
    {
        var st = try runZit(gpa, io, tmp.dir, &.{ "status", "--extra" });
        defer freeResult(gpa, &st);
        try expectExitDiag(st.term, 0, st);
        try std.testing.expect(std.mem.indexOf(u8, st.stdout, ".hidden") == null);
        try std.testing.expect(std.mem.indexOf(u8, st.stdout, "plain.txt") != null);
    }
    {
        var st = try runZit(gpa, io, tmp.dir, &.{ "status", "--extra", "--dotfiles" });
        defer freeResult(gpa, &st);
        try expectExitDiag(st.term, 0, st);
        try std.testing.expect(std.mem.indexOf(u8, st.stdout, ".hidden") != null);
    }

    // clean --dry-run
    {
        var cl = try runZit(gpa, io, tmp.dir, &.{ "clean", "--dry-run" });
        defer freeResult(gpa, &cl);
        try expectExitDiag(cl.term, 0, cl);
        try std.testing.expectEqualStrings("plain.txt\n", cl.stdout);
    }
    {
        var cl = try runZit(gpa, io, tmp.dir, &.{ "clean", "--dry-run", "--dotfiles" });
        defer freeResult(gpa, &cl);
        try expectExitDiag(cl.term, 0, cl);
        try std.testing.expect(std.mem.indexOf(u8, cl.stdout, ".hidden") != null);
    }

    // addremove --dry-run
    {
        var ar = try runZit(gpa, io, tmp.dir, &.{ "addremove", "--dry-run" });
        defer freeResult(gpa, &ar);
        try expectExitDiag(ar.term, 0, ar);
        try std.testing.expectEqualStrings("ADDED plain.txt\n", ar.stdout);
    }
    {
        var ar = try runZit(gpa, io, tmp.dir, &.{ "addremove", "--dry-run", "--dotfiles" });
        defer freeResult(gpa, &ar);
        try expectExitDiag(ar.term, 0, ar);
        try std.testing.expect(std.mem.indexOf(u8, ar.stdout, "ADDED .hidden") != null);
    }

    // `add .` skips them, but naming one explicitly always adds it.
    {
        var a = try runZit(gpa, io, tmp.dir, &.{ "add", "." });
        defer freeResult(gpa, &a);
        try expectExitDiag(a.term, 0, a);
        try std.testing.expectEqualStrings("ADDED  plain.txt\n", a.stdout);
    }
    {
        var a = try runZit(gpa, io, tmp.dir, &.{ "add", ".hidden" });
        defer freeResult(gpa, &a);
        try expectExitDiag(a.term, 0, a);
        try std.testing.expectEqualStrings("ADDED  .hidden\n", a.stdout);
    }
    try expectManaged(gpa, io, tmp.dir, &.{ ".hidden", "plain.txt" });
}

test "checkout settings: dotfiles spellings agree across tiers" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try touch(io, tmp.dir, ".hidden");

    // The repository tier, in fossil's boolean spellings.
    for ([_][]const u8{ "on", "yes", "true", "2" }) |value| {
        var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "dotfiles", value });
        defer freeResult(gpa, &w);
        try expectExitDiag(w.term, 0, w);
        var ex = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try std.testing.expectEqualStrings(".hidden\n", ex.stdout);
    }
    for ([_][]const u8{ "off", "no", "false", "0" }) |value| {
        var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "dotfiles", value });
        defer freeResult(gpa, &w);
        try expectExitDiag(w.term, 0, w);
        var ex = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try std.testing.expectEqualStrings("", ex.stdout);
    }

    // A versioned `.zit-settings/dotfiles` wins over the stored `off`.
    try tmp.dir.createDir(io, ".zit-settings", .default_dir);
    try writeFile(io, tmp.dir, ".zit-settings/dotfiles", "true\n");
    {
        var ex = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try std.testing.expect(std.mem.indexOf(u8, ex.stdout, ".hidden") != null);
    }
}

test "checkout settings: add of a named dot directory expands it" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDirPath(io, ".dir/.inner");
    try touch(io, tmp.dir, ".dir/inside");
    try touch(io, tmp.dir, ".dir/.inner/x");
    try tmp.dir.createDir(io, ".fossil-settings", .default_dir);
    try touch(io, tmp.dir, ".fossil-settings/ignore-glob");
    try touch(io, tmp.dir, "plain.txt");

    // Naming the directory adds what is beneath it, minus the dot names the
    // expansion itself discovers — fossil's `add .dir` shape.
    var a = try runZit(gpa, io, tmp.dir, &.{ "add", ".dir" });
    defer freeResult(gpa, &a);
    try expectExitDiag(a.term, 0, a);
    try std.testing.expectEqualStrings("ADDED  .dir/inside\n", a.stdout);

    // The versioned-settings directory is the workflow this rule exists for.
    var b = try runZit(gpa, io, tmp.dir, &.{ "add", ".fossil-settings" });
    defer freeResult(gpa, &b);
    try expectExitDiag(b.term, 0, b);
    try std.testing.expectEqualStrings("ADDED  .fossil-settings/ignore-glob\n", b.stdout);

    try expectManaged(gpa, io, tmp.dir, &.{ ".dir/inside", ".fossil-settings/ignore-glob" });

    // A plain directory argument still hides dot names, and the shared walk is
    // not poisoned by the exemption the two arguments above needed.
    var c = try runZit(gpa, io, tmp.dir, &.{ "add", "." });
    defer freeResult(gpa, &c);
    try expectExitDiag(c.term, 0, c);
    try std.testing.expectEqualStrings("ADDED  plain.txt\n", c.stdout);
}

test "checkout settings: fossil's checkout-state names are never listed and never added" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, ".fslckout", "junk");
    try writeFile(io, tmp.dir, ".fslckout-journal", "junk");
    try writeFile(io, tmp.dir, "_FOSSIL_", "junk");
    try writeFile(io, tmp.dir, "_FOSSIL_-wal", "junk");
    try writeFile(io, tmp.dir, ".fos", "junk");
    try writeFile(io, tmp.dir, ".fos-shm", "junk");
    try tmp.dir.createDir(io, "sub", .default_dir);
    try writeFile(io, tmp.dir, "sub/_FOSSIL_", "junk");
    try writeFile(io, tmp.dir, "sub/.fslckout-wal", "junk");
    try writeFile(io, tmp.dir, "sub/keep.txt", "real\n");

    // Even with --dotfiles, which is what would otherwise reveal them.
    var ex = try runZit(gpa, io, tmp.dir, &.{ "extras", "--dotfiles" });
    defer freeResult(gpa, &ex);
    try expectExitDiag(ex.term, 0, ex);
    try std.testing.expectEqualStrings("sub/keep.txt\n", ex.stdout);

    var ar = try runZit(gpa, io, tmp.dir, &.{ "addremove", "--dotfiles" });
    defer freeResult(gpa, &ar);
    try expectExitDiag(ar.term, 0, ar);
    try expectManaged(gpa, io, tmp.dir, &.{"sub/keep.txt"});

    // Naming one explicitly is refused, at the root and below it.
    for ([_][]const u8{ ".fslckout", "_FOSSIL_", ".fos", "sub/_FOSSIL_" }) |name| {
        var a = try runZit(gpa, io, tmp.dir, &.{ "add", name });
        defer freeResult(gpa, &a);
        try expectExit(a.term, 1);
        try std.testing.expect(std.mem.indexOf(u8, a.stderr, "control file") != null);
    }
    try expectManaged(gpa, io, tmp.dir, &.{"sub/keep.txt"});
}

test "checkout settings: the manifest stamps are hidden from extras and refused by add while they are written" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    var c1: std.process.RunResult = undefined;
    defer freeResult(gpa, &c1);
    _ = try commitFile(gpa, io, tmp.dir, "a.txt", "x\n", "root", &c1);

    // A file named `manifest` is ordinary content while no stamp selects it.
    try writeFile(io, tmp.dir, "manifest", "mine\n");
    {
        var ex = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try std.testing.expectEqualStrings("manifest\n", ex.stdout);
        var a = try runZit(gpa, io, tmp.dir, &.{ "add", "manifest" });
        defer freeResult(gpa, &a);
        try expectExitDiag(a.term, 0, a);
        var r = try runZit(gpa, io, tmp.dir, &.{ "rm", "manifest" });
        defer freeResult(gpa, &r);
        try expectExitDiag(r.term, 0, r);
    }

    var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "manifest", "on" });
    defer freeResult(gpa, &w);
    try expectExitDiag(w.term, 0, w);
    var up = try runZit(gpa, io, tmp.dir, &.{"update"});
    defer freeResult(gpa, &up);
    try expectExitDiag(up.term, 0, up);
    try std.testing.expect(fileExists(io, tmp.dir, "manifest.uuid"));

    // Now the stamps are generated files: not extras, and not addable.
    var ex = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &ex);
    try expectExitDiag(ex.term, 0, ex);
    try std.testing.expectEqualStrings("", ex.stdout);

    var a = try runZit(gpa, io, tmp.dir, &.{ "add", "manifest" });
    defer freeResult(gpa, &a);
    try expectExit(a.term, 1);
    try std.testing.expect(std.mem.indexOf(u8, a.stderr, "control file") != null);
    try expectManaged(gpa, io, tmp.dir, &.{"a.txt"});
}

test "cli add: clean-glob matches are not added by expansion but are listed" {
    // fossil hands BOTH clean-glob and ignore-glob to the scan that expands a
    // directory argument (add.c, addremove; vfile_scan skips on either), while
    // `extras`/`status`/`clean` build their scan from ignore-glob alone. So a
    // clean-glob match is never swept up by `add .`, and is still reported as
    // an extra — the two verbs deliberately disagree.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, ".fossil-settings", .default_dir);
    try writeFile(io, tmp.dir, ".fossil-settings/clean-glob", "*.o\n");
    try tmp.dir.createDir(io, "src", .default_dir);
    try writeFile(io, tmp.dir, "src/a.c", "int main(void){return 0;}\n");
    try writeFile(io, tmp.dir, "src/a.o", "\x00object\n");

    var a = try runZit(gpa, io, tmp.dir, &.{ "add", "src" });
    defer freeResult(gpa, &a);
    try expectExitDiag(a.term, 0, a);
    try std.testing.expectEqualStrings("ADDED  src/a.c\n", a.stdout);

    // Still an extra, in both listings.
    var ex = try runZit(gpa, io, tmp.dir, &.{"extras"});
    defer freeResult(gpa, &ex);
    try expectExitDiag(ex.term, 0, ex);
    try std.testing.expectEqualStrings("src/a.o\n", ex.stdout);

    var st = try runZit(gpa, io, tmp.dir, &.{ "status", "--extra" });
    defer freeResult(gpa, &st);
    try expectExitDiag(st.term, 0, st);
    try std.testing.expect(std.mem.indexOf(u8, st.stdout, "src/a.o") != null);

    // Naming it takes it: fossil never consults clean-glob for an explicit path.
    var named = try runZit(gpa, io, tmp.dir, &.{ "add", "src/a.o" });
    defer freeResult(gpa, &named);
    try expectExitDiag(named.term, 0, named);
    try std.testing.expectEqualStrings("ADDED  src/a.o\n", named.stdout);
}

test "cli addremove: clean-glob matches are skipped by the reconcile scan" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, ".fossil-settings", .default_dir);
    try writeFile(io, tmp.dir, ".fossil-settings/clean-glob", "*.o\n");
    try tmp.dir.createDir(io, "src", .default_dir);
    try writeFile(io, tmp.dir, "src/a.c", "int main(void){return 0;}\n");
    try writeFile(io, tmp.dir, "src/a.o", "\x00object\n");

    var ar = try runZit(gpa, io, tmp.dir, &.{"addremove"});
    defer freeResult(gpa, &ar);
    try expectExitDiag(ar.term, 0, ar);
    try expectManaged(gpa, io, tmp.dir, &.{"src/a.c"});
}

test "cli clean: clean-glob deletes, the remainder still refuses" {
    // fossil's clean removes a clean-glob match without prompting (checkin.c
    // skips the prompt for it) and prompts for everything else. zit has no
    // prompt: the glob-matched files go, the rest keeps today's refusal, and
    // the note about what WAS deleted precedes the refusal so the output can
    // never read as if nothing happened.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, ".fossil-settings", .default_dir);
    try writeFile(io, tmp.dir, ".fossil-settings/clean-glob", "*.o\n");
    try writeFile(io, tmp.dir, "x.o", "object\n");
    try writeFile(io, tmp.dir, "notes.txt", "keep me\n");

    var c = try runZit(gpa, io, tmp.dir, &.{"clean"});
    defer freeResult(gpa, &c);
    try expectExit(c.term, 1);
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "x.o", .{}));
    try std.testing.expect(fileExists(io, tmp.dir, "notes.txt"));
    const note = std.mem.indexOf(u8, c.stderr, "deleted by clean-glob") orelse
        return error.MissingCleanGlobNote;
    const refusal = std.mem.indexOf(u8, c.stderr, "refusing without --force") orelse
        return error.MissingRefusal;
    try std.testing.expect(note < refusal);
}

test "cli clean: keep-glob wins over clean-glob" {
    // fossil tests keep-glob first and `continue`s, so a file in both lists is
    // KEPT. The preview shows both candidates and deletes neither.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, ".fossil-settings", .default_dir);
    try writeFile(io, tmp.dir, ".fossil-settings/clean-glob", "*.o\n");
    try writeFile(io, tmp.dir, ".fossil-settings/keep-glob", "keep.o\n");
    try writeFile(io, tmp.dir, "keep.o", "protected\n");
    try writeFile(io, tmp.dir, "x.o", "object\n");
    try writeFile(io, tmp.dir, "notes.txt", "keep me\n");

    // --dry-run deletes nothing at all, clean-glob match included.
    var dry = try runZit(gpa, io, tmp.dir, &.{ "clean", "--dry-run" });
    defer freeResult(gpa, &dry);
    try expectExitDiag(dry.term, 0, dry);
    try std.testing.expectEqualStrings("notes.txt\nx.o\n", dry.stdout);
    try std.testing.expect(fileExists(io, tmp.dir, "x.o"));

    var c = try runZit(gpa, io, tmp.dir, &.{"clean"});
    defer freeResult(gpa, &c);
    try expectExit(c.term, 1);
    try std.testing.expect(fileExists(io, tmp.dir, "keep.o"));
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "x.o", .{}));
    try std.testing.expect(fileExists(io, tmp.dir, "notes.txt"));

    // --force takes the remainder and still never takes the kept file.
    var f = try runZit(gpa, io, tmp.dir, &.{ "clean", "--force" });
    defer freeResult(gpa, &f);
    try expectExitDiag(f.term, 0, f);
    try std.testing.expect(fileExists(io, tmp.dir, "keep.o"));
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "notes.txt", .{}));
}

test "cli clean: --emptydirs prunes what clean-glob emptied, and spares an empty-dirs name" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try tmp.dir.createDir(io, ".fossil-settings", .default_dir);
    try writeFile(io, tmp.dir, ".fossil-settings/clean-glob", "*.o\n");
    try tmp.dir.createDir(io, "build", .default_dir);
    try writeFile(io, tmp.dir, "build/x.o", "object\n");
    try writeFile(io, tmp.dir, "notes.txt", "keep me\n");

    // The directory pass runs even though the run ends in a refusal: fossil
    // gates it on the flag alone, after the file pass, whatever was answered.
    var c = try runZit(gpa, io, tmp.dir, &.{ "clean", "--emptydirs" });
    defer freeResult(gpa, &c);
    try expectExit(c.term, 1);
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "build/x.o", .{}));
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "build", .{}));
    try std.testing.expect(fileExists(io, tmp.dir, "notes.txt"));

    // With the name listed in empty-dirs the directory stands, because the
    // project asked for it to exist with nothing in it.
    try writeFile(io, tmp.dir, ".fossil-settings/empty-dirs", "build\n");
    try tmp.dir.createDir(io, "build", .default_dir);
    try writeFile(io, tmp.dir, "build/x.o", "object\n");
    var c2 = try runZit(gpa, io, tmp.dir, &.{ "clean", "--emptydirs" });
    defer freeResult(gpa, &c2);
    try expectExit(c2.term, 1);
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "build/x.o", .{}));
    var d = try tmp.dir.openDir(io, "build", .{});
    d.close(io);
}

test "checkout settings: update creates the empty-dirs directories, and only from the value it started with" {
    // fossil reads `empty-dirs` BEFORE the update runs (update.c), so a
    // versioned value the update itself brings in takes effect at the NEXT
    // update, not this one. zit reads it from the same config frame at the
    // same point, so the two agree.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "a.txt", "one\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    const first = try commitHashDup(gpa, io, tmp.dir, "one");
    defer gpa.free(first);

    try tmp.dir.createDir(io, ".fossil-settings", .default_dir);
    try writeFile(io, tmp.dir, ".fossil-settings/empty-dirs", "build  tmp/cache\n");
    var add = try runZit(gpa, io, tmp.dir, &.{ "add", ".fossil-settings" });
    defer freeResult(gpa, &add);
    try expectExitDiag(add.term, 0, add);
    const second = try commitHashDup(gpa, io, tmp.dir, "two");
    defer gpa.free(second);

    // Back to the check-in that does not carry the setting, then clear whatever
    // the move already created so the next assertion is unambiguous.
    var back = try runZit(gpa, io, tmp.dir, &.{ "update", first });
    defer freeResult(gpa, &back);
    try expectExitDiag(back.term, 0, back);
    tmp.dir.deleteDir(io, "tmp/cache") catch {};
    tmp.dir.deleteDir(io, "tmp") catch {};
    tmp.dir.deleteDir(io, "build") catch {};

    // The update that BRINGS the setting in creates nothing: the value it read
    // was the pre-update one, which was empty.
    var bring = try runZit(gpa, io, tmp.dir, &.{"update"});
    defer freeResult(gpa, &bring);
    try expectExitDiag(bring.term, 0, bring);
    try std.testing.expect(fileExists(io, tmp.dir, ".fossil-settings/empty-dirs"));
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "build", .{}));

    // A preview creates nothing either.
    var dry = try runZit(gpa, io, tmp.dir, &.{ "update", "--dry-run" });
    defer freeResult(gpa, &dry);
    try expectExitDiag(dry.term, 0, dry);
    try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "build", .{}));

    // The next update sees it and lays down both names, parents included.
    var again = try runZit(gpa, io, tmp.dir, &.{"update"});
    defer freeResult(gpa, &again);
    try expectExitDiag(again.term, 0, again);
    var b = try tmp.dir.openDir(io, "build", .{});
    b.close(io);
    var t = try tmp.dir.openDir(io, "tmp/cache", .{});
    t.close(io);

    // Idempotent, and `zit clean` leaves them: they hold nothing, and the
    // --emptydirs walk only ever probes what a deletion emptied.
    var third = try runZit(gpa, io, tmp.dir, &.{"update"});
    defer freeResult(gpa, &third);
    try expectExitDiag(third.term, 0, third);
    var cl = try runZit(gpa, io, tmp.dir, &.{ "clean", "--force", "--emptydirs" });
    defer freeResult(gpa, &cl);
    try expectExitDiag(cl.term, 0, cl);
    var b2 = try tmp.dir.openDir(io, "build", .{});
    b2.close(io);
}

test "checkout settings: open creates the empty-dirs directories the check-in carries" {
    // `open` reads the value AFTER the materialization, because the versioned
    // file does not exist on disk before it. fossil's checkout reads the same
    // list out of the check-in it is installing, so the two see the same names.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp_a = std.testing.tmpDir(.{});
    defer tmp_a.cleanup();
    try initRepo(gpa, io, tmp_a.dir);
    try writeFile(io, tmp_a.dir, "hello.txt", "hi\n");
    try tmp_a.dir.createDir(io, ".fossil-settings", .default_dir);
    try writeFile(io, tmp_a.dir, ".fossil-settings/empty-dirs", "build  tmp/cache\n");
    try addManaged(gpa, io, tmp_a.dir, &.{ "hello.txt", ".fossil-settings/empty-dirs" });
    const tip = try commitHashDup(gpa, io, tmp_a.dir, "root");
    defer gpa.free(tip);

    const port = try freshLoopbackPort(io);
    var server = try spawnServe(gpa, io, tmp_a.dir, port);
    defer server.kill(io);
    try waitForListen(io, port);
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/", .{port});
    defer gpa.free(url);

    var tmp_b = std.testing.tmpDir(.{});
    defer tmp_b.cleanup();
    {
        var clone = try runZit(gpa, io, tmp_b.dir, &.{ "clone", "--no-open", url, "repo" });
        defer freeResult(gpa, &clone);
        try expectExitDiag(clone.term, 0, clone);
    }
    var b_dir = try tmp_b.dir.openDir(io, "repo", .{});
    defer b_dir.close(io);

    var op = try runZit(gpa, io, b_dir, &.{"open"});
    defer freeResult(gpa, &op);
    try expectExitDiag(op.term, 0, op);
    var build = try b_dir.openDir(io, "build", .{});
    build.close(io);
    var cache = try b_dir.openDir(io, "tmp/cache", .{});
    cache.close(io);
}

/// The zit half of the settings fixture. The fixture itself lives in
/// `zit.settings_fixture` because `src/smoke.zig` S14 builds the same one on
/// the fossil side and compares the two tools byte for byte; this test is what
/// keeps zit's half pinned in a lane CI runs.
fn writeSettingsFixture(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) !void {
    try dir.createDir(io, ".fossil-settings", .default_dir);
    for (settings_fixture.settings) |s| {
        const path = try std.fmt.allocPrint(gpa, ".fossil-settings/{s}", .{s.name});
        defer gpa.free(path);
        try writeFile(io, dir, path, s.value);
    }
    try writeFile(io, dir, settings_fixture.tracked_file.name, settings_fixture.tracked_file.value);
}

fn writeSettingsFixtureJunk(io: std.Io, dir: std.Io.Dir) !void {
    for (settings_fixture.junk_dirs) |d| try dir.createDir(io, d, .default_dir);
    for (settings_fixture.junk) |j| try writeFile(io, dir, j.path, j.data);
}

test "checkout settings: a fully populated .fossil-settings fixture drives extras and clean as one" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeSettingsFixture(gpa, io, tmp.dir);
    {
        var add = try runZit(gpa, io, tmp.dir, &.{ "add", ".fossil-settings", settings_fixture.tracked_file.name });
        defer freeResult(gpa, &add);
        try expectExitDiag(add.term, 0, add);
    }
    const tip = try commitHashDup(gpa, io, tmp.dir, "settings fixture");
    defer gpa.free(tip);

    // The update the settings are already committed for: `manifest on` lays the
    // stamps down and `empty-dirs` creates its directory.
    {
        var up = try runZit(gpa, io, tmp.dir, &.{"update"});
        defer freeResult(gpa, &up);
        try expectExitDiag(up.term, 0, up);
    }
    try std.testing.expect(fileExists(io, tmp.dir, "manifest"));
    try std.testing.expect(fileExists(io, tmp.dir, "manifest.uuid"));
    var emptyd = try tmp.dir.openDir(io, "emptyd", .{});
    emptyd.close(io);

    try writeSettingsFixtureJunk(io, tmp.dir);

    // ignore-glob removes three of them whatever the separator; the manifest
    // stamps and the `.fos` name are never listed; `dotfiles off` hides
    // `.hidden` until it is asked for.
    {
        var ex = try runZit(gpa, io, tmp.dir, &.{"extras"});
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try std.testing.expectEqualStrings(
            "notes.txt\nprecious.dat\nscratch.tmp\nsub/deep.log\nwide.wide\n",
            ex.stdout,
        );
    }
    {
        var ex = try runZit(gpa, io, tmp.dir, &.{ "extras", "--dotfiles" });
        defer freeResult(gpa, &ex);
        try expectExitDiag(ex.term, 0, ex);
        try std.testing.expectEqualStrings(
            ".hidden\nnotes.txt\nprecious.dat\nscratch.tmp\nsub/deep.log\nwide.wide\n",
            ex.stdout,
        );
    }
    // The preview is the extras set minus what keep-glob holds back; the
    // clean-glob match is in it, because fossil's dry run skips the clean-glob
    // test entirely (checkin.c clean_cmd) and lists every non-kept candidate.
    {
        var cl = try runZit(gpa, io, tmp.dir, &.{ "clean", "--dry-run" });
        defer freeResult(gpa, &cl);
        try expectExitDiag(cl.term, 0, cl);
        try std.testing.expectEqualStrings(
            "notes.txt\nscratch.tmp\nsub/deep.log\nwide.wide\n",
            cl.stdout,
        );
        try std.testing.expect(std.mem.indexOf(u8, cl.stderr, "kept by keep-glob") != null);
    }
    // Nothing in the fixture warns: `ignore-glob.no-warn` covers the one name
    // that also gains a stored value below it.
    {
        var w = try runZit(gpa, io, tmp.dir, &.{ "settings", "ignore-glob", "*.zzz" });
        defer freeResult(gpa, &w);
        try expectExitDiag(w.term, 0, w);
        try std.testing.expectEqualStrings("", w.stderr);
    }
}

test "attach: add, list, replace, rm, size limit, stdin, wiki target, prefix strip, stub" {
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);
    try writeFile(io, tmp.dir, "shot.png", "PNG bytes v1");

    // A ticket to attach to.
    var t = try runZit(gpa, io, tmp.dir, &.{ "ticket", "new", "--user", "u", "title=bug" });
    defer freeResult(gpa, &t);
    try expectExit(t.term, 0);
    const ticket_id = std.mem.trim(u8, t.stdout[0..std.mem.indexOfScalar(u8, t.stdout, ' ').?], " \n");

    // add: prints the A artifact's hash, then the blob's hash.
    var a1 = try runZit(gpa, io, tmp.dir, &.{ "attach", "add", ticket_id, "shot.png", "--user", "u", "--comment", "first" });
    defer freeResult(gpa, &a1);
    try expectExit(a1.term, 0);
    var lines = std.mem.splitScalar(u8, std.mem.trim(u8, a1.stdout, "\n"), '\n');
    const a1_hash = lines.next().?;
    const blob1 = lines.next().?;
    try std.testing.expectEqual(@as(usize, 64), a1_hash.len);
    try std.testing.expectEqual(@as(usize, 64), blob1.len);
    try std.testing.expect(lines.next() == null);

    // The artifact round-trips: type attachment, one A card naming the blob.
    var art = try runZit(gpa, io, tmp.dir, &.{ "artifact", a1_hash });
    defer freeResult(gpa, &art);
    try expectExit(art.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, art.stdout, "type: attachment") != null);
    try std.testing.expect(std.mem.indexOf(u8, art.stdout, blob1) != null);

    // list shows it with the blob's size.
    var l1 = try runZit(gpa, io, tmp.dir, &.{ "attach", "list", ticket_id });
    defer freeResult(gpa, &l1);
    try expectExit(l1.term, 0);
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, l1.stdout, "\n"));
    try std.testing.expect(std.mem.startsWith(u8, l1.stdout, "shot.png  12  -  "));
    try std.testing.expect(std.mem.indexOf(u8, l1.stdout, blob1) != null);

    // Same name again replaces: one entry, the new blob.
    try writeFile(io, tmp.dir, "shot.png", "PNG bytes v2!");
    var a2 = try runZit(gpa, io, tmp.dir, &.{ "attach", "add", ticket_id, "shot.png", "--user", "u", "--name", "some/dir/shot.png", "--mimetype", "image/png" });
    defer freeResult(gpa, &a2);
    try expectExit(a2.term, 0);
    var l2 = try runZit(gpa, io, tmp.dir, &.{ "attach", "list", ticket_id });
    defer freeResult(gpa, &l2);
    try expectExit(l2.term, 0);
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, l2.stdout, "\n"));
    try std.testing.expect(std.mem.startsWith(u8, l2.stdout, "shot.png  13  image/png  "));
    try std.testing.expect(std.mem.indexOf(u8, l2.stdout, blob1) == null);

    // rm: the association ends; nothing leaves the store.
    var r = try runZit(gpa, io, tmp.dir, &.{ "attach", "rm", ticket_id, "shot.png", "--user", "u" });
    defer freeResult(gpa, &r);
    try expectExit(r.term, 0);
    var l3 = try runZit(gpa, io, tmp.dir, &.{ "attach", "list", ticket_id });
    defer freeResult(gpa, &l3);
    try expectExit(l3.term, 0);
    try std.testing.expectEqualStrings("", l3.stdout);
    var still = try runZit(gpa, io, tmp.dir, &.{ "artifact", blob1 });
    defer freeResult(gpa, &still);
    try expectExit(still.term, 0);

    // Over the default 1 MiB limit refuses and stores nothing; 0 lifts it.
    const big = try gpa.alloc(u8, (1 << 20) + 1);
    defer gpa.free(big);
    @memset(big, 'x');
    try writeFile(io, tmp.dir, "big.bin", big);
    var over = try runZit(gpa, io, tmp.dir, &.{ "attach", "add", ticket_id, "big.bin", "--user", "u" });
    defer freeResult(gpa, &over);
    try std.testing.expect(over.term.exited != 0);
    try std.testing.expect(std.mem.indexOf(u8, over.stderr, "attachment-size-limit is 1048576 bytes") != null);
    try std.testing.expectEqualStrings("", over.stdout);
    var lift = try runZit(gpa, io, tmp.dir, &.{ "settings", "attachment-size-limit", "0" });
    defer freeResult(gpa, &lift);
    try expectExit(lift.term, 0);
    var ok_big = try runZit(gpa, io, tmp.dir, &.{ "attach", "add", ticket_id, "big.bin", "--user", "u" });
    defer freeResult(gpa, &ok_big);
    try expectExit(ok_big.term, 0);

    // A wiki-name target, from stdin, needs --name.
    try writeFile(io, tmp.dir, "notes.txt", "hello wiki\n");
    var noname = try runZitStdin(gpa, io, tmp.dir, "notes.txt", &.{ "attach", "add", "Home", "-", "--user", "u" });
    defer freeResult(gpa, &noname);
    try std.testing.expect(noname.term.exited != 0);
    var w = try runZitStdin(gpa, io, tmp.dir, "notes.txt", &.{ "attach", "add", "Home", "-", "--name", "notes.txt", "--user", "u" });
    defer freeResult(gpa, &w);
    try expectExit(w.term, 0);
    var lw = try runZit(gpa, io, tmp.dir, &.{ "attach", "list", "Home" });
    defer freeResult(gpa, &lw);
    try expectExit(lw.term, 0);
    try std.testing.expect(std.mem.startsWith(u8, lw.stdout, "notes.txt  11  -  "));

    // A quarry stub lists as external with the size it points at,
    // and `zit artifact` on the blob says so.
    const stub = "quarry-attachment 1\nsha256 " ++ ("4f" ** 32) ++ "\nsize 18349022\ntype video/mp4\nname screen-recording.mp4\n";
    try writeFile(io, tmp.dir, "stub.txt", stub);
    var sa = try runZitStdin(gpa, io, tmp.dir, "stub.txt", &.{ "attach", "add", ticket_id, "-", "--name", "screen-recording.mp4", "--mimetype", "video/mp4", "--user", "u" });
    defer freeResult(gpa, &sa);
    try expectExit(sa.term, 0);
    var sl = std.mem.splitScalar(u8, std.mem.trim(u8, sa.stdout, "\n"), '\n');
    _ = sl.next().?;
    const stub_blob = sl.next().?;
    var ls = try runZit(gpa, io, tmp.dir, &.{ "attach", "list", ticket_id });
    defer freeResult(gpa, &ls);
    try expectExit(ls.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, ls.stdout, "screen-recording.mp4  18349022  video/mp4  external:sha256:" ++ ("4f" ** 32)) != null);
    var sd = try runZit(gpa, io, tmp.dir, &.{ "artifact", stub_blob });
    defer freeResult(gpa, &sd);
    try expectExit(sd.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, sd.stdout, "attachment-stub: screen-recording.mp4, 18349022 bytes external") != null);

    // The whole-repository listing names the target first.
    var all = try runZit(gpa, io, tmp.dir, &.{ "attach", "list" });
    defer freeResult(gpa, &all);
    try expectExit(all.term, 0);
    try std.testing.expect(std.mem.indexOf(u8, all.stdout, "Home  notes.txt  11  ") != null);
}

test "cli log: a PATH-filtered log costs reads per ENTRY NAMED, not per check-in walked" {
    // The defect: the predicate reconstructed a manifest for every check-in
    // the walk visited, so one file's history on a mirror of SQLite took 129s
    // and the cost grew with how much unrelated history sat behind it.
    //
    // The invariant the file index buys, stated so it cannot regress quietly:
    // a history where most check-ins never touch PATH costs reads in
    // proportion to the entries NAMED, not to the check-ins WALKED. Most of
    // this repository's check-ins touch only `filler.txt`, and answering for
    // `a.txt` must not pay for them.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "a.txt", "a-seed\n");
    try writeFile(io, tmp.dir, "filler.txt", "f-seed\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "filler.txt" });
    var tip = try commitHashDup(gpa, io, tmp.dir, "seed");
    defer gpa.free(tip);
    const total = 40;
    var named: usize = 1; // the seed names a.txt
    var i: usize = 0;
    while (i < total) : (i += 1) {
        var body: [16]u8 = undefined;
        // Only every eighth check-in touches a.txt; the rest are noise the
        // index must let the walk skip without reading anything.
        if (i % 8 == 0) {
            try writeFile(io, tmp.dir, "a.txt", std.fmt.bufPrint(&body, "a{d}\n", .{i}) catch unreachable);
            named += 1;
        } else {
            try writeFile(io, tmp.dir, "filler.txt", std.fmt.bufPrint(&body, "f{d}\n", .{i}) catch unreachable);
        }
        gpa.free(tip);
        tip = try commitHashDup(gpa, io, tmp.dir, "edit");
    }

    const seam = [_][2][]const u8{.{ "ZIT_MANIFEST_READS", "1" }};
    var res = try runZitEnv(gpa, io, tmp.dir, &seam, &.{ "log", tip, "a.txt" });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    const entries = std.mem.count(u8, res.stdout, "checkin ");
    try std.testing.expectEqual(named, entries);
    const reads = manifestReadsOf(&res) orelse return error.NoCounter;

    // Confirming a candidate reconstructs it and its primary parent, so a few
    // reads per named entry is the floor. What must NOT happen is a read per
    // walked check-in: that is the defect, and at this shape it is >= 41.
    if (reads > 4 * named) {
        std.debug.print(
            "\nlog PATH filter: naming {d} entries out of {d} check-ins cost {d} manifest reads — " ++
                "the walk is paying for check-ins that never touched the path again.\n",
            .{ entries, total + 1, reads },
        );
        return error.ReadsScaleWithHistory;
    }
}
test "cli log: the file index narrows the walk without changing one byte of the answer" {
    // The index (`captureFileFacts`, inverted by path) and the B-log-2
    // predicate are NOT the same relation: an F-card-level diff reports a
    // delta manifest that restates a path with the hash it already had, while
    // the path's effective content did not move. So the index is allowed to
    // over-report and is used only to NARROW; every candidate is confirmed by
    // reconstruction. What must never happen is an OMISSION — a check-in the
    // index leaves out is skipped unread, so a miss would silently drop a
    // file's history.
    //
    // This pins exactly that: for every tracked path, the indexed answer and
    // the reconstruct-everything answer are byte-identical.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    // A history with the shapes that make the two relations diverge: repeated
    // edits (delta manifests over a shared baseline), a file added late, one
    // deleted, and a branch merged back.
    try writeFile(io, tmp.dir, "a.txt", "a1\n");
    try writeFile(io, tmp.dir, "b.txt", "b1\n");
    try addManaged(gpa, io, tmp.dir, &.{ "a.txt", "b.txt" });
    var tip = try commitHashDup(gpa, io, tmp.dir, "seed");
    defer gpa.free(tip);
    var i: usize = 0;
    while (i < 8) : (i += 1) {
        var body: [16]u8 = undefined;
        try writeFile(io, tmp.dir, "a.txt", std.fmt.bufPrint(&body, "a{d}\n", .{i}) catch unreachable);
        if (i % 3 == 0) try writeFile(io, tmp.dir, "b.txt", std.fmt.bufPrint(&body, "b{d}\n", .{i}) catch unreachable);
        gpa.free(tip);
        tip = try commitHashDup(gpa, io, tmp.dir, "edit");
    }
    try writeFile(io, tmp.dir, "c.txt", "c1\n");
    try addManaged(gpa, io, tmp.dir, &.{"c.txt"});
    gpa.free(tip);
    tip = try commitHashDup(gpa, io, tmp.dir, "add c");
    var rm = try runZit(gpa, io, tmp.dir, &.{ "rm", "--hard", "b.txt" });
    defer freeResult(gpa, &rm);
    try expectExit(rm.term, 0);
    gpa.free(tip);
    tip = try commitHashDup(gpa, io, tmp.dir, "drop b");

    const off = [_][2][]const u8{.{ "ZIT_NO_FILE_INDEX", "1" }};
    for ([_][]const u8{ "a.txt", "b.txt", "c.txt", "never.txt" }) |path| {
        // Indexed (built on the first query, adopted after).
        var indexed = try runZit(gpa, io, tmp.dir, &.{ "log", "--long", tip, path });
        defer freeResult(gpa, &indexed);
        try expectExit(indexed.term, 0);
        // Reconstructed, the answer the index must reproduce exactly.
        var plain = try runZitEnv(gpa, io, tmp.dir, &off, &.{ "log", "--long", tip, path });
        defer freeResult(gpa, &plain);
        try expectExit(plain.term, 0);
        if (!std.mem.eql(u8, indexed.stdout, plain.stdout)) {
            std.debug.print(
                "\nfile index: '{s}' answers differently with the index than without it.\n--- reconstructed ---\n{s}\n--- indexed ---\n{s}\n",
                .{ path, plain.stdout, indexed.stdout },
            );
            return error.IndexChangedTheAnswer;
        }
    }
}

/// The `fileindex-walk: N` the ZIT_FILEINDEX_WALK seam prints on stderr.
fn fileIndexWalkOf(res: *const std.process.RunResult) ?u64 {
    const marker = "fileindex-walk: ";
    const at = std.mem.lastIndexOf(u8, res.stderr, marker) orelse return null;
    const rest = res.stderr[at + marker.len ..];
    const end = std.mem.indexOfScalar(u8, rest, '\n') orelse rest.len;
    return std.fmt.parseInt(u64, std.mem.trim(u8, rest[0..end], " \r"), 10) catch null;
}

test "cli commit: a commit extends the file index over the new check-ins, never rebuilds it" {
    // As first shipped, the index validated against an exact log tail, so ANY
    // commit, pull or push threw the whole thing away and the next file-history
    // query rebuilt it from the first check-in — 164s on a mirror of SQLite. On
    // a repository that receives commits the fast path would almost never be
    // there, which is the case that matters for a forge.
    //
    // The invariant: the walk that builds the index is bounded by the check-ins
    // it has NOT already seen. A query that follows no change walks nothing; a
    // query after one commit walks one.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try initRepo(gpa, io, tmp.dir);

    try writeFile(io, tmp.dir, "a.txt", "a-seed\n");
    try addManaged(gpa, io, tmp.dir, &.{"a.txt"});
    var tip = try commitHashDup(gpa, io, tmp.dir, "seed");
    defer gpa.free(tip);
    const history = 15;
    var i: usize = 0;
    while (i < history) : (i += 1) {
        var body: [16]u8 = undefined;
        try writeFile(io, tmp.dir, "a.txt", std.fmt.bufPrint(&body, "a{d}\n", .{i}) catch unreachable);
        gpa.free(tip);
        tip = try commitHashDup(gpa, io, tmp.dir, "edit");
    }

    // The derive builds the index as it captures — fossil's mlink, at ingest —
    // so the FIRST file-history query already finds it and walks nothing.
    // Before, the first query paid a full pass over the log to build it, and
    // on a forge that restarted more often than that pass took the index
    // never existed at all.
    const seam = [_][2][]const u8{.{ "ZIT_FILEINDEX_WALK", "1" }};
    var built = try runZitEnv(gpa, io, tmp.dir, &seam, &.{ "log", tip, "a.txt" });
    defer freeResult(gpa, &built);
    try expectExit(built.term, 0);
    const first = fileIndexWalkOf(&built) orelse return error.NoCounter;
    if (first != 0) {
        std.debug.print("\nthe first file-history query walked {d} check-ins; the derive should have built the index already\n", .{first});
        return error.IndexNotBuiltByDerive;
    }
    try std.testing.expectEqual(@as(u64, 0), fileIndexUnknownOf(&built) orelse return error.NoCounter);
    try std.testing.expectEqual(@as(usize, history + 1), std.mem.count(u8, built.stdout, "checkin "));

    // One commit: the next open extends the derive over the new check-in and
    // the index with it, so the query still walks nothing and still answers.
    try writeFile(io, tmp.dir, "a.txt", "a-new\n");
    gpa.free(tip);
    tip = try commitHashDup(gpa, io, tmp.dir, "one more");
    var extended = try runZitEnv(gpa, io, tmp.dir, &seam, &.{ "log", tip, "a.txt" });
    defer freeResult(gpa, &extended);
    try expectExit(extended.term, 0);
    try std.testing.expectEqual(@as(u64, 0), fileIndexWalkOf(&extended) orelse return error.NoCounter);
    try std.testing.expectEqual(@as(usize, history + 2), std.mem.count(u8, extended.stdout, "checkin "));

    // The lazy build is now the fallback for a store whose cache predates the
    // derive-built index. Remove the index and it is rebuilt on demand — in
    // checkpointed chunks, so an interrupted build resumes rather than
    // restarts — with the same answer, and the next query walks nothing.
    try tmp.dir.deleteFile(io, ".zit-cache/finfo");
    var rebuilt = try runZitEnv(gpa, io, tmp.dir, &seam, &.{ "log", tip, "a.txt" });
    defer freeResult(gpa, &rebuilt);
    try expectExit(rebuilt.term, 0);
    try std.testing.expectEqual(@as(u64, history + 2), fileIndexWalkOf(&rebuilt) orelse return error.NoCounter);
    try std.testing.expectEqualStrings(extended.stdout, rebuilt.stdout);
    var warm = try runZitEnv(gpa, io, tmp.dir, &seam, &.{ "log", tip, "a.txt" });
    defer freeResult(gpa, &warm);
    try expectExit(warm.term, 0);
    try std.testing.expectEqual(@as(u64, 0), fileIndexWalkOf(&warm) orelse return error.NoCounter);
}

/// Initialises a repository whose commits mint TREE check-ins: three files
/// under three top-level directories with `submanifest enable --k 1`, which
/// is three partition groups. Submanifests §6.2 step 2 mints flat below two
/// groups, so two files at the root — the obvious small fixture — never
/// produce a tree and prove nothing. Returns the seed check-in's hash.
fn initTreeRepo(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]const u8 {
    try initRepo(gpa, io, dir);
    {
        var en = try runZit(gpa, io, dir, &.{ "submanifest", "enable", "--k", "1" });
        defer freeResult(gpa, &en);
        try expectExitDiag(en.term, 0, en);
    }
    for ([_][]const u8{ "a", "b", "c" }) |d| try dir.createDirPath(io, d);
    try writeFile(io, dir, "a/x.txt", "x1\n");
    try writeFile(io, dir, "b/y.txt", "y1\n");
    try writeFile(io, dir, "c/z.txt", "z1\n");
    try addManaged(gpa, io, dir, &.{ "a/x.txt", "b/y.txt", "c/z.txt" });
    return commitHashDup(gpa, io, dir, "seed");
}

/// Lines of `text` that begin with `prefix`.
fn countLinesStarting(text: []const u8, prefix: []const u8) usize {
    var n: usize = 0;
    var it = std.mem.splitScalar(u8, text, '\n');
    while (it.next()) |line| {
        if (std.mem.startsWith(u8, line, prefix)) n += 1;
    }
    return n;
}

test "cli artifact: renders a tree check-in's S cards, one per card, agreeing with --raw" {
    // `renderCardLines` had a lane per card letter and no S lane, so a tree
    // check-in — whose entire file set is S cards and no F cards — printed as
    // a manifest with no files at all while `ls` on the same hash listed
    // thousands. Raw output always showed the cards; rendered output is
    // checked against it here so the two can never disagree again.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const tip = try initTreeRepo(gpa, io, tmp.dir);
    defer gpa.free(tip);

    var raw = try runZit(gpa, io, tmp.dir, &.{ "artifact", "--raw", tip });
    defer freeResult(gpa, &raw);
    try expectExitDiag(raw.term, 0, raw);
    var shown = try runZit(gpa, io, tmp.dir, &.{ "artifact", tip });
    defer freeResult(gpa, &shown);
    try expectExitDiag(shown.term, 0, shown);

    const raw_s = countLinesStarting(raw.stdout, "S ");
    try std.testing.expect(raw_s >= 1); // the fixture really is a tree
    try std.testing.expectEqual(@as(usize, 0), countLinesStarting(raw.stdout, "F "));
    try std.testing.expectEqual(raw_s, countLinesStarting(shown.stdout, "S "));
    try std.testing.expectEqual(@as(usize, 0), countLinesStarting(shown.stdout, "F "));
    // Every rendered S line is a raw S line verbatim: filename then hash.
    var it = std.mem.splitScalar(u8, shown.stdout, '\n');
    while (it.next()) |line| {
        if (!std.mem.startsWith(u8, line, "S ")) continue;
        try std.testing.expect(std.mem.indexOf(u8, raw.stdout, line) != null);
    }
}

/// The `fileindex-unknown: N` the ZIT_FILEINDEX_WALK seam prints on stderr:
/// how many check-ins the adopted index could not answer for.
fn fileIndexUnknownOf(res: *const std.process.RunResult) ?u64 {
    const marker = "fileindex-unknown: ";
    const at = std.mem.lastIndexOf(u8, res.stderr, marker) orelse return null;
    const rest = res.stderr[at + marker.len ..];
    const end = std.mem.indexOfScalar(u8, rest, '\n') orelse rest.len;
    return std.fmt.parseInt(u64, std.mem.trim(u8, rest[0..end], " \r"), 10) catch null;
}

test "cli log: on a tree-manifest store the file index answers, and answers exactly what reconstruction does" {
    // The change-set capture refused every tree manifest as "unknown at this
    // layer", so on a submanifest store the index recorded the whole log as
    // unknown: valid, adopted, never rebuilt, and answering nothing — a PATH
    // query paid the build and then reconstructed per check-in anyway. On a
    // pkgsrc mirror that was 39 minutes a query. The answer was always right;
    // this pins that it stays right AND that the index now carries it.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var tip = try initTreeRepo(gpa, io, tmp.dir);
    defer gpa.free(tip);

    // a/x.txt changes in the seed, the 1st and the 3rd commit; b/y.txt in the
    // seed and the 2nd; c/z.txt only in the seed.
    try writeFile(io, tmp.dir, "a/x.txt", "x2\n");
    gpa.free(tip);
    tip = try commitHashDup(gpa, io, tmp.dir, "x again");
    try writeFile(io, tmp.dir, "b/y.txt", "y2\n");
    gpa.free(tip);
    tip = try commitHashDup(gpa, io, tmp.dir, "y again");
    try writeFile(io, tmp.dir, "a/x.txt", "x3\n");
    gpa.free(tip);
    tip = try commitHashDup(gpa, io, tmp.dir, "x third");

    const seam = [_][2][]const u8{.{ "ZIT_FILEINDEX_WALK", "1" }};
    const off = [_][2][]const u8{.{ "ZIT_NO_FILE_INDEX", "1" }};
    const want = [_]struct { path: []const u8, n: usize }{
        .{ .path = "a/x.txt", .n = 3 },
        .{ .path = "b/y.txt", .n = 2 },
        .{ .path = "c/z.txt", .n = 1 },
    };
    for (want) |w| {
        var indexed = try runZitEnv(gpa, io, tmp.dir, &seam, &.{ "log", tip, w.path });
        defer freeResult(gpa, &indexed);
        try expectExitDiag(indexed.term, 0, indexed);
        // THE FIX: a tree check-in's change set is known to the index.
        const unknown = fileIndexUnknownOf(&indexed) orelse return error.NoCounter;
        if (unknown != 0) {
            std.debug.print("\nfile index: recorded {d} check-ins as unknown on a tree-manifest store\n", .{unknown});
            return error.TreeCheckinsUnknownToIndex;
        }
        try std.testing.expectEqual(w.n, std.mem.count(u8, indexed.stdout, "checkin "));

        var plain = try runZitEnv(gpa, io, tmp.dir, &off, &.{ "log", tip, w.path });
        defer freeResult(gpa, &plain);
        try expectExitDiag(plain.term, 0, plain);
        try std.testing.expectEqualStrings(plain.stdout, indexed.stdout);
    }
}

fn footprintCounter(res: *const std.process.RunResult, key: []const u8) ?u64 {
    const at = std.mem.lastIndexOf(u8, res.stderr, key) orelse return null;
    const rest = res.stderr[at + key.len ..];
    var end: usize = 0;
    while (end < rest.len and rest[end] >= '0' and rest[end] <= '9') end += 1;
    return std.fmt.parseInt(u64, rest[0..end], 10) catch null;
}

test "log opens on the image path: a warm log replays no records on flat and tree stores" {
    // `log` reached past the open funnel and inherited the full record replay
    // meant for write verbs, twice for a symbolic name. On a million-artifact
    // store that was most of a second before the first row, and it was the
    // whole of the cold-open cost the file index could not remove. Every
    // route `log` has — literal ID, symbolic name, PATH, `--long` — now opens
    // once on the zero-parse image, and this pins that none of them replays.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    const seam = [_][2][]const u8{.{ "ZIT_DERIVE_FOOTPRINT", "1" }};
    for ([_]bool{ false, true }) |tree| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var tip: []const u8 = undefined;
        if (tree) {
            tip = try initTreeRepo(gpa, io, tmp.dir);
        } else {
            try initRepo(gpa, io, tmp.dir);
            try tmp.dir.createDirPath(io, "a");
            try writeFile(io, tmp.dir, "a/x.txt", "x1\n");
            try addManaged(gpa, io, tmp.dir, &.{"a/x.txt"});
            tip = try commitHashDup(gpa, io, tmp.dir, "seed");
        }
        defer gpa.free(tip);
        try writeFile(io, tmp.dir, "a/x.txt", "x2\n");
        gpa.free(tip);
        tip = try commitHashDup(gpa, io, tmp.dir, "x again");

        // One read warms the cache; the commit above left a tail to adopt.
        {
            var warm = try runZit(gpa, io, tmp.dir, &.{ "log", tip });
            defer freeResult(gpa, &warm);
            try expectExitDiag(warm.term, 0, warm);
        }
        const routes = [_][]const []const u8{
            &.{ "log", tip },
            &.{ "log", "trunk" },
            &.{ "log", "-n", "1", "trunk" },
            &.{ "log", "--long", "-n", "5", "trunk", "a/x.txt" },
            &.{ "log", "-n", "5", tip, "a/x.txt" },
        };
        for (routes) |route| {
            var res = try runZitEnv(gpa, io, tmp.dir, &seam, route);
            defer freeResult(gpa, &res);
            try expectExitDiag(res.term, 0, res);
            try std.testing.expect(std.mem.count(u8, res.stdout, "\n") >= 1);
            const replayed = footprintCounter(&res, "state-records=") orelse return error.NoCounter;
            const derived = footprintCounter(&res, "records=") orelse return error.NoCounter;
            if (replayed != 0 or derived != 0) {
                std.debug.print("\nlog {s}: replayed {d} records and derived {d} on a warm {s} store\n", .{ route[1], replayed, derived, if (tree) "tree" else "flat" });
                return error.LogReplaysRecords;
            }
        }
    }
}

test "cli log: a flat store and its tree-manifest twin give the same file history for every path" {
    // The same history minted both ways — `submanifest convert` re-mints every
    // check-in as a tree under a new project code — so shape is the only
    // variable. The differential suite cannot host this: it compares zit
    // against a real fossil, and no released fossil speaks submanifests.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDirPath(io, "flat");
    var flat = try tmp.dir.openDir(io, "flat", .{});
    defer flat.close(io);
    try initRepo(gpa, io, flat);
    for ([_][]const u8{ "a", "b", "c" }) |d| try flat.createDirPath(io, d);
    try writeFile(io, flat, "a/x.txt", "x1\n");
    try writeFile(io, flat, "b/y.txt", "y1\n");
    try writeFile(io, flat, "c/z.txt", "z1\n");
    try addManaged(gpa, io, flat, &.{ "a/x.txt", "b/y.txt", "c/z.txt" });
    gpa.free(try commitHashDup(gpa, io, flat, "seed"));
    try writeFile(io, flat, "a/x.txt", "x2\n");
    gpa.free(try commitHashDup(gpa, io, flat, "x again"));
    try writeFile(io, flat, "b/y.txt", "y2\n");
    gpa.free(try commitHashDup(gpa, io, flat, "y again"));

    {
        var conv = try runZit(gpa, io, tmp.dir, &.{ "submanifest", "convert", "flat", "sub", "--k", "1" });
        defer freeResult(gpa, &conv);
        try expectExitDiag(conv.term, 0, conv);
    }
    var sub = try tmp.dir.openDir(io, "sub", .{});
    defer sub.close(io);

    const seam = [_][2][]const u8{.{ "ZIT_FILEINDEX_WALK", "1" }};
    for ([_][]const u8{ "a/x.txt", "b/y.txt", "c/z.txt" }) |path| {
        var f = try runZit(gpa, io, flat, &.{ "log", "tip", path });
        defer freeResult(gpa, &f);
        try expectExitDiag(f.term, 0, f);
        var s = try runZitEnv(gpa, io, sub, &seam, &.{ "log", "-R", ".zit", "tip", path });
        defer freeResult(gpa, &s);
        try expectExitDiag(s.term, 0, s);
        // Hashes differ across the rebirth; the history's SHAPE must not.
        try std.testing.expectEqual(
            std.mem.count(u8, f.stdout, "checkin "),
            std.mem.count(u8, s.stdout, "checkin "),
        );
        try std.testing.expectEqual(@as(u64, 0), fileIndexUnknownOf(&s) orelse return error.NoCounter);
    }
}

test "crosslink reports a tree check-in's files instead of 'files unknown'" {
    // The derive computes every check-in's change set at ingest, trees
    // included (by S-card diff), and the crosslink stream carries it. A tree
    // used to read as `files unknown` there, because diffing one meant
    // flattening it and the derive runs on every cold open.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const tip = try initTreeRepo(gpa, io, tmp.dir);
    defer gpa.free(tip);
    try writeFile(io, tmp.dir, "b/y.txt", "y2\n");
    gpa.free(try commitHashDup(gpa, io, tmp.dir, "touch b"));

    var cl = try runZit(gpa, io, tmp.dir, &.{"crosslink"});
    defer freeResult(gpa, &cl);
    try expectExitDiag(cl.term, 0, cl);
    try std.testing.expect(std.mem.indexOf(u8, cl.stdout, "files unknown") == null);
    // The root tree: every file an add. The second: exactly the touched one.
    try std.testing.expect(std.mem.indexOf(u8, cl.stdout, "file A a/x.txt") != null);
    try std.testing.expect(std.mem.indexOf(u8, cl.stdout, "file M b/y.txt") != null);
}

/// The `fileindex-subs: N` the seam prints: sub-manifests the tree diff
/// resolved to build the index.
fn fileIndexSubsOf(res: *const std.process.RunResult) ?u64 {
    const marker = "fileindex-subs: ";
    const at = std.mem.lastIndexOf(u8, res.stderr, marker) orelse return null;
    const rest = res.stderr[at + marker.len ..];
    const end = std.mem.indexOfScalar(u8, rest, '\n') orelse rest.len;
    return std.fmt.parseInt(u64, std.mem.trim(u8, rest[0..end], " \r"), 10) catch null;
}

test "the tree-manifest index build resolves only the sub-manifests that changed, not the whole tree" {
    // Diffing two trees by flattening both cost files-per-check-in, not
    // files-changed: 34 minutes and ~1 GB on a pkgsrc mirror. The S-card diff
    // skips every sub-manifest whose hash did not move. Three top-level
    // directories at k=1 are three leaf sub-manifests; a commit touching one
    // file resolves that sub-manifest's two versions and nothing else.
    const gpa = std.testing.allocator;
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var tip = try initTreeRepo(gpa, io, tmp.dir);
    defer gpa.free(tip);
    const seam = [_][2][]const u8{.{ "ZIT_FILEINDEX_WALK", "1" }};
    {
        var warm = try runZitEnv(gpa, io, tmp.dir, &seam, &.{ "log", tip, "a/x.txt" });
        defer freeResult(gpa, &warm);
        try expectExitDiag(warm.term, 0, warm);
    }
    try writeFile(io, tmp.dir, "a/x.txt", "x2\n");
    gpa.free(tip);
    tip = try commitHashDup(gpa, io, tmp.dir, "touch a");

    // The query's own open extends the derive over the one new check-in and
    // the index with it — that is where the tree diff runs — so the query
    // walks nothing, and the sub-manifests resolved in this process are the
    // extend's.
    var q = try runZitEnv(gpa, io, tmp.dir, &seam, &.{ "log", tip, "a/x.txt" });
    defer freeResult(gpa, &q);
    try expectExitDiag(q.term, 0, q);
    try std.testing.expectEqual(@as(u64, 0), fileIndexWalkOf(&q) orelse return error.NoCounter);
    try std.testing.expectEqual(@as(u64, 0), fileIndexUnknownOf(&q) orelse return error.NoCounter);
    const subs = fileIndexSubsOf(&q) orelse return error.NoCounter;
    if (subs > 2) {
        std.debug.print("\nthe tree diff resolved {d} sub-manifests for a one-file commit in a three-leaf tree; it should resolve the changed leaf's two versions only\n", .{subs});
        return error.TreeDiffNotProportional;
    }
    try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, q.stdout, "checkin "));
    // b and c untouched: their history is unchanged and the index still knows it.
    var b = try runZitEnv(gpa, io, tmp.dir, &seam, &.{ "log", tip, "b/y.txt" });
    defer freeResult(gpa, &b);
    try expectExitDiag(b.term, 0, b);
    try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, b.stdout, "checkin "));
}