Skip to content
zit Artifact [82d0043c09]
Login

Artifact [82d0043c09]

Artifact 82d0043c092089206c72141986d66667c92227278238b9488f1dbfb74874aa58:


//! 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;
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 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;
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 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;
        },
    }
}

/// 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;
}

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`.
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: [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;
    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);
}

/// 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" });

    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)
    \\
;

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 directory and a symlink are rejected; 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", .{});

    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: 'd' is a directory; name individual files (directory recursion is not yet supported)\n" ++
            "zit add: 's' is not a regular file\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(!fileExists(io, tmp.dir, managed.filename));
}

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-managed" });
    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-managed' is a zit control file and cannot be tracked\n",
        res.stderr,
    );
    try std.testing.expect(!fileExists(io, tmp.dir, managed.filename));
}

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: not a zit repository (no .zit found in this directory or any parent)\n",
        res.stderr,
    );
    try std.testing.expect(!fileExists(io, tmp.dir, managed.filename));
}

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(!fileExists(io, tmp.dir, managed.filename));

    // 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(!fileExists(io, tmp.dir, managed.filename));
}

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] [--] <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).
    \\
    \\Only individual files are accepted; naming a directory is an error.
    \\
    \\options:
    \\  -n, --dry-run   show what would be added; change nothing
    \\  -q, --quiet     do not print the added files
    \\  -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, a directory, 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(!fileExists(io, tmp.dir, managed.filename));
    }
}

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(!fileExists(io, tmp.dir, managed.filename));
}

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.
    {
        var f = try tmp.dir.createFile(io, managed.filename, .{});
        defer f.close(io);
        try f.writePositionalAll(io, "this is not a valid managed file", 0);
    }

    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-managed 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.setPermissions(io, File.Permissions.fromMode(0o555));
    defer tmp.dir.setPermissions(io, File.Permissions.fromMode(0o777)) 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(!fileExists(io, tmp.dir, managed.filename));
}

// ==========================================================================
// 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, "not a zit repository") != 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(!fileExists(io, tmp.dir, managed.filename));
}

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(!fileExists(io, tmp.dir, managed.filename));
    }
}

test "cli add B-add-5: 'zit add .' at the root gives the directory guidance, not '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);

    // `.` resolves to the checkout root itself — a directory inside the repo, so
    // it must produce the B-add-5 directory diagnostic, not B-add-6's "outside
    // the repository" (the most likely novice invocation).
    var res = try runZit(gpa, io, tmp.dir, &.{ "add", "." });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit add: '.' is a directory; name individual files (directory recursion is not yet supported)\n",
        res.stderr,
    );
    try std.testing.expect(!fileExists(io, tmp.dir, managed.filename));
}

// ==========================================================================
// `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(
        "removed a (file kept on disk)\nremoved b (file kept on disk)\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("removed gone (file kept on disk)\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("removed 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("removed 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("removed a (file kept on disk)\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 .' at the root gives the directory guidance, not '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);

    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 a directory; name individual files (directory recursion is not yet supported)\n",
        res.stderr,
    );
    try std.testing.expect(!fileExists(io, tmp.dir, managed.filename));
}

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(!fileExists(io, tmp.dir, managed.filename));
}

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-managed" });
    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-managed' 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(fileExists(io, tmp.dir, managed.filename));
    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: not a zit repository (no .zit found in this directory or any parent)\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 (file kept on disk)\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. Only individual files are
    \\accepted; naming a directory is an error.
    \\
    \\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, a directory, 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)
    \\
;

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(!fileExists(io, tmp.dir, managed.filename));
    }
}

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("removed a (file kept on disk)\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.setPermissions(io, File.Permissions.fromMode(0o555));
    defer tmp.dir.setPermissions(io, File.Permissions.fromMode(0o777)) 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.setPermissions(io, File.Permissions.fromMode(0o555));
    defer tmp.dir.setPermissions(io, File.Permissions.fromMode(0o777)) 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.setPermissions(io, File.Permissions.fromMode(0o777));
    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.
    {
        var f = try tmp.dir.createFile(io, managed.filename, .{});
        defer f.close(io);
        try f.writePositionalAll(io, "this is not a valid managed file", 0);
    }

    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-managed 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("removed 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\nremoved g1 (file kept on disk)\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\nremoved g2 (file kept on disk)\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(
        "removed g3a (file kept on disk)\nremoved g3b (file kept on disk)\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 D4: 'zit remove'/'zit delete' suggest 'rm' (git-synonym redirect)" {
    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).
    inline for (.{ "remove", "delete" }) |cmd| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var res = try runZit(gpa, io, tmp.dir, &.{cmd});
        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 command '" ++ cmd ++ "'") != null);
        try std.testing.expect(std.mem.indexOf(u8, res.stderr, "did you mean 'rm'?") != 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 });
}

/// 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 [-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.
    \\
    \\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
    \\  missing <path>   tracked, but not present in the working tree
    \\
    \\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.
    \\
    \\Not yet reported (these arrive with `zit commit` / `zit diff`): edits to
    \\already-committed files, files deleted from a check-in, and untracked
    \\("extra") files in the working tree.
    \\
    \\options:
    \\  -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)
    \\
;

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.expectEqualStrings(
        "On branch trunk (no check-ins yet)\nnothing tracked yet\n",
        res.stdout,
    );
    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(!fileExists(io, tmp.dir, managed.filename));
}

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, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "On branch trunk (no check-ins yet)\nadded 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, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "On branch trunk (no check-ins yet)\nmissing 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, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "On branch trunk (no check-ins yet)\nadded 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, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "On branch trunk (no check-ins yet)\nmissing a\n",
        res.stdout,
    );
    try std.testing.expectEqualStrings("", res.stderr);
}

test "cli status B-status-4/-5/-16: born state, present tracked file NOT classified, absent → 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, "p");
    try touch(io, tmp.dir, "q");
    try addManaged(gpa, io, tmp.dir, &.{ "p", "q" });
    try tmp.dir.deleteFile(io, "q");
    const born_hash = "a" ** 64;
    try seedCheckout(io, tmp.dir, born_hash, "trunk"); // reach the born state

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    // The full hash is echoed verbatim (no abbreviation, B-status-4/-16); the
    // present `p` produces NO line (needs the deferred diff), only `q` is missing.
    try std.testing.expectEqualStrings(
        "On branch trunk (check-in " ++ born_hash ++ ")\nmissing q\n",
        res.stdout,
    );
    try std.testing.expectEqualStrings("", res.stderr);
    try expectCheckout(io, tmp.dir, born_hash, "trunk"); // read-only
}

test "cli status B-status-7: born state, no missing → 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 touch(io, tmp.dir, "p");
    try addManaged(gpa, io, tmp.dir, &.{"p"});
    const born_hash = "b" ** 40;
    try seedCheckout(io, tmp.dir, born_hash, "trunk");

    var res = try runZit(gpa, io, tmp.dir, &.{"status"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings(
        "On branch trunk (check-in " ++ born_hash ++ ")\nnothing to report\n",
        res.stdout,
    );
    try std.testing.expectEqualStrings("", 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.expectEqualStrings(
        "On branch trunk (no check-ins yet)\nnothing tracked yet\n",
        res.stdout,
    );
    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: not a zit repository (no .zit found in this directory or any parent)\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);
    {
        var f = try tmp.dir.createFile(io, managed.filename, .{});
        defer f.close(io);
        try f.writePositionalAll(io, "this is not a valid managed file", 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); // header not yet printed
    try std.testing.expectEqualStrings(
        "zit status: cannot read tracked files: .zit-managed 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: 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"});
    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);
}

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"});
    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 readFile(gpa, io, tmp.dir, ".zit-managed");
    defer gpa.free(before_managed);

    var res = try runZit(gpa, io, tmp.dir, &.{"diff"});
    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 readFile(gpa, io, tmp.dir, ".zit-managed");
    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"});
    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"});
    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"});
    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"});
    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"});
    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"});
    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"});
    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"});
    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"});
    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"});
    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"});
    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"});
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    try std.testing.expectEqualStrings("", res.stdout);
    try std.testing.expectEqualStrings(
        "zit: not a zit repository (no .zit found in this directory or any parent)\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"});
    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);
    {
        var f = try tmp.dir.createFile(io, managed.filename, .{});
        defer f.close(io);
        try f.writePositionalAll(io, "this is not a valid managed file\n", 0);
    }

    var res = try runZit(gpa, io, tmp.dir, &.{"diff"});
    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-managed 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"});
    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 [-h|--help]\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", "extra" });
        defer freeResult(gpa, &positional);
        try expectExit(positional.term, 2);
        try std.testing.expectEqualStrings("", positional.stdout);
        try std.testing.expectEqualStrings(
            "zit diff: unexpected argument 'extra'\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";

/// 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",
};

/// Spawns `zit <tail...>` with the pinned base environment plus the commit
/// overrides. Caller owns `result.stdout`/`result.stderr`.
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);

    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.open(gpa, fs.storageFile(), 0);
    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> [--user <login>] [--branch <name>] [--close]
    \\                  [-q|--quiet] [--] [<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.
    \\
    \\A check-in message is required (-m). Only regular files are captured.
    \\
    \\options:
    \\  -m, --message <message>   the check-in comment (required)
    \\      --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
    \\  -q, --quiet               do not print the check-in line
    \\  -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)
    \\
;

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, child_hash) == null);
    try std.testing.expect(std.mem.indexOf(u8, lv.stdout, 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("reverted 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"});
    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("reverted 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("reverted 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"});
    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 surfaces SymlinkUnsupported and is left 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);

    // Track a regular file so the managed set names 'link'; the worktree copy
    // stays a placeholder revert must not overwrite (it will fault first).
    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, and point the
    // checkout at it. `zit commit` never mints a symlink card, so this is the
    // one legitimate way to exercise the reader's §4.7.6-m5 rejection.
    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" });

    var res = try runZit(gpa, io, tmp.dir, &.{ "revert", "link" });
    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, "symlink") != null);

    // The placeholder file is untouched (the fault precedes any write).
    const kept = try readFile(gpa, io, tmp.dir, "link");
    defer gpa.free(kept);
    try std.testing.expectEqualStrings("placeholder\n", kept);
}

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);
}

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: -m <message>\ntry '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: not a zit repository (no .zit found in this directory or any parent)\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("", 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" });
    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);
}

// ==========================================================================
// `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 std.testing.expectEqualStrings("", update.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 readFile(gpa, io, tmp.dir, ".zit-managed");
    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 readFile(gpa, io, tmp.dir, ".zit-managed");
    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"});
}

test "cli update: a dirty checkout leaves its managed content and 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", "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);
    const child = try checkinHash(child_commit.stdout);

    try writeFile(io, tmp.dir, "tracked.txt", "uncommitted change\n");
    const before_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(before_checkout);
    const before_managed = try readFile(gpa, io, tmp.dir, ".zit-managed");
    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", root });
    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 readFile(gpa, io, tmp.dir, ".zit-managed");
    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, child);
    try expectManaged(gpa, io, tmp.dir, &.{"tracked.txt"});
}

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 std.testing.expectEqualStrings("", update.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 std.testing.expectEqualStrings("", set_update.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 std.testing.expectEqualStrings("", reset.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 std.testing.expectEqualStrings("", cancel_update.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-managed", "../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 readFile(gpa, io, repo, ".zit-managed");
        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 readFile(gpa, io, repo, ".zit-managed");
        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 readFile(gpa, io, repo, ".zit-managed");
    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 readFile(gpa, io, repo, ".zit-managed");
    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 readFile(gpa, io, repo, ".zit-managed");
    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 readFile(gpa, io, repo, ".zit-managed");
    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: a clean managed file can become a directory containing a managed 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, "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 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 std.testing.expectEqualStrings("", update.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"});
}

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 std.testing.expectEqualStrings("", update.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: 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 readFile(gpa, io, tmp.dir, ".zit-managed");
    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 readFile(gpa, io, tmp.dir, ".zit-managed");
    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 std.testing.expectEqualStrings("", update.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: dirty managed membership 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 writeFile(io, tmp.dir, "added.txt", "new membership\n");
    try addManaged(gpa, io, tmp.dir, &.{"added.txt"});

    const before_checkout = try readFile(gpa, io, tmp.dir, ".zit-checkout");
    defer gpa.free(before_checkout);
    const before_managed = try readFile(gpa, io, tmp.dir, ".zit-managed");
    defer gpa.free(before_managed);
    const before_tracked = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(before_tracked);
    const before_added = try readFile(gpa, io, tmp.dir, "added.txt");
    defer gpa.free(before_added);

    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 readFile(gpa, io, tmp.dir, ".zit-managed");
    defer gpa.free(after_managed);
    const after_tracked = try readFile(gpa, io, tmp.dir, "tracked.txt");
    defer gpa.free(after_tracked);
    const after_added = try readFile(gpa, io, tmp.dir, "added.txt");
    defer gpa.free(after_added);
    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_added, after_added);
    try expectCheckoutVersion(io, tmp.dir, child);
    try expectManaged(gpa, io, tmp.dir, &.{ "tracked.txt", "added.txt" });
}

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 readFile(gpa, io, tmp.dir, ".zit-managed");
    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 readFile(gpa, io, tmp.dir, ".zit-managed");
    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: managed executable-mode drift preserves the current checkout" {
    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);
    const child = try checkinHash(child_commit.stdout);
    {
        var script = try tmp.dir.openFile(io, "script", .{ .mode = .read_write });
        defer script.close(io);
        try script.setPermissions(io, File.Permissions.executable_file);
    }

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

    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 readFile(gpa, io, tmp.dir, ".zit-managed");
    defer gpa.free(after_managed);
    const after_script = try readFile(gpa, io, tmp.dir, "script");
    defer gpa.free(after_script);
    try std.testing.expectEqualSlices(u8, before_checkout, after_checkout);
    try std.testing.expectEqualSlices(u8, before_managed, after_managed);
    try std.testing.expectEqualSlices(u8, before_script, 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 expectCheckoutVersion(io, tmp.dir, child);
    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 readFile(gpa, io, tmp.dir, ".zit-managed");
    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 readFile(gpa, io, tmp.dir, ".zit-managed");
    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: malformed and missing check-in IDs 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{
        .{ .id = "not-an-id", .diagnostic = "invalid check-in ID" },
        .{
            .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 readFile(gpa, io, tmp.dir, ".zit-managed");
        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 readFile(gpa, io, tmp.dir, ".zit-managed");
        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"},
            .stderr = "zit update: no check-in given\ntry 'zit update --help'\n",
        },
        .{
            .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.?);

    var res = try runZit(gpa, io, tmp.dir, &.{ "log", child });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    var want: [80]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "checkin {s}\n", .{child});
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings(
        "zit log: reachable check-in is not a valid manifest\n",
        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);
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", top });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    var want: [80]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "checkin {s}\n", .{top});
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings(
        "zit log: reachable check-in is not a valid manifest\n",
        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);
    var res = try runZit(gpa, io, tmp.dir, &.{ "log", top });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 1);
    var want: [80]u8 = undefined;
    const expected = try std.fmt.bufPrint(&want, "checkin {s}\n", .{top});
    try std.testing.expectEqualStrings(expected, res.stdout);
    try std.testing.expectEqualStrings(
        "zit log: reachable check-in is not a valid manifest\n",
        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();

    var res = try runZit(gpa, io, tmp.dir, &.{ "log", wrong });
    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,
    );
}

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 CHECKIN
        \\
        \\Show CHECKIN followed by every reachable parent check-in in depth-first
        \\P-card order. CHECKIN must be an exact 40- or 64-character lowercase
        \\hexadecimal artifact ID. Each check-in is shown once; a referenced parent
        \\missing from this repository is shown once as a phantom.
        \\
        \\options:
        \\  -h, --help    show this help and exit
        \\
    ;

    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,
    );

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

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

    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);
    var want: [192]u8 = undefined;
    const expected = try std.fmt.bufPrint(
        &want,
        "zit log: invalid check-in ID '{s}' (expected 40 or 64 lowercase hexadecimal digits)\n",
        .{uppercase},
    );
    try std.testing.expectEqualStrings(expected, invalid.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 shows the check-in's
        \\full artifact ID and its branch. Leaves are ordered most-recent first; the
        \\leaf matching the current checkout is marked with a leading "*".
        \\
        \\options:
        \\      --all       also list closed leaves, each marked "(closed)"
        \\      --no-cache  bypass the cache and re-derive from the full log,
        \\                  re-verifying whole-log integrity (the cache is refreshed)
        \\  -h, --help      show this help and exit
        \\
    ;
    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, "not a zit repository") != 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;
    const expected = try std.fmt.bufPrint(&want, "* {s}  trunk\n", .{root});
    try std.testing.expectEqualStrings(expected, res.stdout);
    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" });
    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;
    const expected = try std.fmt.bufPrint(&want, "* {s}  trunk\n  {s}  trunk\n", .{ fork.child2, fork.child1 });
    try std.testing.expectEqualStrings(expected, res.stdout);
    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;
    const expected_def = try std.fmt.bufPrint(&want_def, "* {s}  trunk\n", .{fork.child2});
    try std.testing.expectEqualStrings(expected_def, def.stdout);

    // --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;
    const expected_all = try std.fmt.bufPrint(&want_all, "* {s}  trunk\n  {s}  trunk  (closed)\n", .{ fork.child2, fork.child1 });
    try std.testing.expectEqualStrings(expected_all, all.stdout);
}

// ==========================================================================
// `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 [--no-cache] [-h|--help]
    \\
    \\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)".
    \\
    \\options:
    \\      --no-cache  bypass the cache and re-derive from the full log,
    \\                  re-verifying whole-log integrity (the cache is refreshed)
    \\  -h, --help      show this help and exit
    \\
;

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, "not a zit repository") != 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" });

    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 [CHECKIN] [--no-cache]
    \\
    \\List tags. With no CHECKIN, print the tag names in use across the
    \\repository, one per line, sorted. With a CHECKIN, print the tags visible
    \\on that check-in and their values.
    \\
    \\  ls          alias for list
    \\
    \\      --no-cache  bypass the cache and re-derive from the full log,
    \\                  re-verifying whole-log integrity (the cache is refreshed)
    \\  -h, --help  show this help
    \\
;

/// 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: tag list shows branch names with sym- stripped" {
    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: a symbolic name is stored 'sym-trunk' and surfaced 'trunk'. The
    // machinery 'branch' tag is not listed as a bare name in the no-arg view.
    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list" });
    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 tag §5.2: tag list CHECKIN shows branch trunk and the sym 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);
    const hash = try gpa.dupe(u8, try checkinHash(commit.stdout));
    defer gpa.free(hash);

    // §5.1/§5.2/§6.4: a root trunk tip carries the active 'branch'=trunk tag
    // (surfaced 'branch trunk') and the 'sym-trunk' tag (surfaced 'trunk'),
    // ascending by raw name.
    var res = try runZit(gpa, io, tmp.dir, &.{ "tag", "list", hash });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("branch trunk\ntrunk\n", res.stdout);
    try std.testing.expectEqualStrings("", res.stderr);
}

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();

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

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("trunk\n", 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 on its 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);

    // Anchors the raw skeleton: a valid `*sym-alpha` self-tag (§5.1 propagate
    // sign) crosslinks and surfaces stripped as 'alpha'. 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", hash });
    defer freeResult(gpa, &res);
    try expectExit(res.term, 0);
    try std.testing.expectEqualStrings("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. The listing stays
    // exactly the trunk from the real commit.
    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("trunk\n", 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 [--no-cache]
    \\
    \\Resolve 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,
    \\or one of the reserved forms (tip, rid:, tag:, root:, start:, merge-in:).
    \\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.
    \\
    \\options:
    \\      --no-cache  bypass the cache and re-derive from the full log,
    \\                  re-verifying whole-log integrity (the cache is refreshed)
    \\  -h, --help      show this help and exit
    \\
;

/// 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, §10.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;
    const expected_leaves = try std.fmt.bufPrint(&lbuf, "  {s}  trunk\n", .{fork.child1});
    try std.testing.expectEqualStrings(expected_leaves, leaves.stdout);
    // 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;
    const expected_leaves = try std.fmt.bufPrint(&lbuf, "* {s}  trunk\n", .{fork.child2});
    try std.testing.expectEqualStrings(expected_leaves, leaves.stdout);
}

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: missing NAME, extra positional, and 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.
    var no_name = try runZit(gpa, io, tmp.dir, &.{"info"});
    defer freeResult(gpa, &no_name);
    try expectExit(no_name.term, 2);
    try std.testing.expectEqualStrings("", no_name.stdout);
    try std.testing.expectEqualStrings(
        "zit info: no name given\ntry 'zit info --help'\n",
        no_name.stderr,
    );

    // A second positional is an unexpected argument.
    var extra = try runZit(gpa, io, tmp.dir, &.{ "info", "a", "b" });
    defer freeResult(gpa, &extra);
    try expectExit(extra.term, 2);
    try std.testing.expectEqualStrings("", extra.stdout);
    try std.testing.expectEqualStrings(
        "zit info: unexpected argument 'b'\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 {
    var digest: [32]u8 = undefined;
    std.crypto.hash.sha3.Sha3_256.hash(content, &digest, .{});
    const hex = "0123456789abcdef";
    for (digest) |b| {
        try list.append(gpa, hex[b >> 4]);
        try list.append(gpa, hex[b & 0x0f]);
    }
    try list.append(gpa, ' ');
    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, "not a zit repository") != 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" });
    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);
    }
}

// --- 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;

/// 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 {
    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.
    return std.process.spawn(io, .{
        .argv = &.{ abs_exe, "serve", "--port", port_str },
        .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 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: u16 = 47187;
    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. The URL names the served
    // /xfer endpoint verbatim (sync §4 r1 — the client sends the repo URL path
    // unchanged and never appends /xfer; `zit serve` advertises this endpoint).
    const url = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/xfer", .{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 expectExit(clone.term, 0);
    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);
}

// --- 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: u16 = 47251;
    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 expectExit(clone.term, 0);
    // 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: u16 = 47252;
    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 expectExit(pull.term, 0);
    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: u16 = 47253;
    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 expectExit(clone.term, 0);
    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 expectExit(pull.term, 0);
    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: u16 = 47261;
    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 expectExit(clone.term, 0);
    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: u16 = 47262;
    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 expectExit(push.term, 0);
    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);
}