Skip to content
zit Artifact [723ad4594b]
Login

Artifact [723ad4594b]

Artifact 723ad4594b77f416d066002250d07e0bbe362c6187d2678a1c13ce74050e288c:


//! RED coverage for Task 10's transactional `zit import-git` command.
//!
//! This file deliberately names the smallest testable production seam that does
//! not execute a shell command or contact a remote:
//!
//!   `git_import_cli.run(gpa, io, request, streams, dependencies) -> cli.Exit`
//!
//! `dependencies` supplies a `ChildFactory`/`ChildSession` and filesystem hooks.
//! A `ChildSession` must expose its stdout reader plus `terminate`, `drainStderr`,
//! and `reap`; each lifecycle operation is observable in the supplied fake. The
//! real CLI dispatch will construct OS-backed dependencies. The filesystem hook
//! is only needed to deterministically fail publication flush/close/sync/rename
//! boundaries; ordinary scenarios exercise a real `std.testing.tmpDir`.
//!
//! The opt-in suite intentionally remains RED until Task 10 provides command
//! preflight, real-pipe lifecycle, and publication-boundary fault seams. It is
//! not part of `zig build test`.

const std = @import("std");
const builtin = @import("builtin");
const cli = @import("zit").cli;
const fast_export = @import("zit").git_fast_export;
const import_cli = @import("git_import_cli.zig");
const checkout = @import("zit").checkout;
const sync = @import("zit").sync;
const artifact = @import("zit").artifact;
const git_import = @import("zit").git_import;
const logstore = @import("zit").logstore;
const storage = @import("zit").storage;
const pack = @import("zit").pack;
const helper_options = @import("git_import_cli_options");

const valid_export =
    "feature done\n" ++
    "blob\n" ++
    "mark :1\n" ++
    "data 5\n" ++
    "hello\n" ++
    "commit refs/heads/main\n" ++
    "mark :2\n" ++
    "author A <[email protected]> 0 +0000\n" ++
    "committer A <[email protected]> 0 +0000\n" ++
    "data 4\n" ++
    "init\n" ++
    "M 100644 :1 README\n" ++
    "\n" ++
    "done\n";

const valid_export_with_remote_ref =
    "feature done\n" ++
    "blob\n" ++
    "mark :1\n" ++
    "data 5\n" ++
    "hello\n" ++
    "commit refs/heads/main\n" ++
    "mark :2\n" ++
    "author A <[email protected]> 0 +0000\n" ++
    "committer A <[email protected]> 0 +0000\n" ++
    "data 4\n" ++
    "init\n" ++
    "M 100644 :1 README\n" ++
    "\n" ++
    "reset refs/remotes/origin/main\n" ++
    "from :2\n" ++
    "\n" ++
    "done\n";

const valid_export_with_oids =
    "feature done\n" ++
    "blob\n" ++
    "mark :1\n" ++
    "original-oid 100b93820ade4c16225673b4ca62bb3ade63c313\n" ++
    "data 5\n" ++
    "hello\n" ++
    "commit refs/heads/main\n" ++
    "mark :2\n" ++
    "original-oid ed9b38a10070b504cecfacd892507af78fcb24a4\n" ++
    "author A <[email protected]> 0 +0000\n" ++
    "committer A <[email protected]> 0 +0000\n" ++
    "data 4\n" ++
    "init\n" ++
    "M 100644 :1 README\n" ++
    "\n" ++
    "done\n";

const malformed_export =
    "feature done\n" ++
    "commit refs/heads/main\n" ++
    "mark :not-a-number\n";

const marked_tag_export =
    "feature done\n" ++
    "blob\n" ++
    "mark :1\n" ++
    "data 5\n" ++
    "hello\n" ++
    "commit refs/heads/main\n" ++
    "mark :2\n" ++
    "author A <[email protected]> 0 +0000\n" ++
    "committer A <[email protected]> 0 +0000\n" ++
    "data 4\n" ++
    "init\n" ++
    "M 100644 :1 README.md\n" ++
    "\n" ++
    "tag v1\n" ++
    "mark :3\n" ++
    "from :2\n" ++
    "tagger A <[email protected]> 0 +0000\n" ++
    "data 0\n" ++
    "\n" ++
    "done\n";

const Result = struct {
    code: cli.Exit,
    out: []const u8,
    err: []const u8,
    lifecycle: import_cli.ChildLifecycle,
};

/// The wished test adapter. Task 10's production implementation owns no fake
/// process: it consumes this narrow interface, while the CLI passes the
/// OS-backed implementation and this file passes a scripted one.
const LifecycleFault = enum { terminate, drain_stderr, reap };

const ScriptedGit = struct {
    fast_export: []const u8,
    export_exit: u8 = 0,
    stderr: []const u8 = "",
    launched: usize = 0,
    terminated: usize = 0,
    stderr_drained: usize = 0,
    reaped: usize = 0,
    lifecycle_fault: ?LifecycleFault = null,
    limits: import_cli.ImportLimits = .{},
    /// What preflight's §6.3 analysis would have returned. Null is "the
    /// analysis produced nothing", which is what every test that does not care
    /// about the analyzed path wants.
    analyzed_submanifest_k: ?u4 = null,

    fn dependencies(self: *ScriptedGit, publication: import_cli.PublicationHooks) import_cli.Dependencies {
        return .{
            .git = .{
                .context = self,
                .vtable = &.{
                    .start_export = startExport,
                    .terminate = terminate,
                    .drain_stderr = drainStderr,
                    .reap = reap,
                },
            },
            .publication = publication,
            .limits = self.limits,
        };
    }

    fn startExport(context: *anyopaque, _: []const u8) !import_cli.ExportPipes {
        const self: *ScriptedGit = @ptrCast(@alignCast(context));
        self.launched += 1;
        return .{
            .stdout = .fixed(self.fast_export),
            .stderr = .fixed(self.stderr),
            .exit_code = self.export_exit,
            .analyzed_submanifest_k = self.analyzed_submanifest_k,
        };
    }

    fn terminate(context: *anyopaque) !void {
        const self: *ScriptedGit = @ptrCast(@alignCast(context));
        self.terminated += 1;
        if (self.lifecycle_fault == .terminate) return error.InjectedLifecycleFailure;
    }

    fn drainStderr(context: *anyopaque) !void {
        const self: *ScriptedGit = @ptrCast(@alignCast(context));
        self.stderr_drained += 1;
        if (self.lifecycle_fault == .drain_stderr) return error.InjectedLifecycleFailure;
    }

    fn reap(context: *anyopaque) !void {
        const self: *ScriptedGit = @ptrCast(@alignCast(context));
        self.reaped += 1;
        if (self.lifecycle_fault == .reap) return error.InjectedLifecycleFailure;
    }
};

fn runFixture(
    tmp: std.testing.TmpDir,
    source: []const u8,
    destination: []const u8,
    quiet: bool,
    git: *ScriptedGit,
    publication: import_cli.PublicationHooks,
) Result {
    return runFixtureShaped(tmp, source, destination, quiet, git, publication, .{ .tree = 5 });
}

/// `runFixture` with the one request field these tests vary. Everything else --
/// source, dependencies, streams, attribution, default branch -- is held
/// identical on purpose: the whole point of the flag is that flat and tree
/// imports differ in tree emission and in nothing else.
fn runFixtureShaped(
    tmp: std.testing.TmpDir,
    source: []const u8,
    destination: []const u8,
    quiet: bool,
    git: *ScriptedGit,
    publication: import_cli.PublicationHooks,
    submanifest: git_import.SubmanifestChoice,
) Result {
    return runFixtureFull(tmp, source, destination, quiet, git, publication, submanifest, false);
}

/// The whole request surface these tests reach. `provenance` mirrors the
/// production default (off): a test that reads the published map or refs opts
/// in exactly as an operator would.
fn runFixtureFull(
    tmp: std.testing.TmpDir,
    source: []const u8,
    destination: []const u8,
    quiet: bool,
    git: *ScriptedGit,
    publication: import_cli.PublicationHooks,
    submanifest: git_import.SubmanifestChoice,
    provenance: bool,
) Result {
    const Capture = struct {
        var out_storage: [4096]u8 = undefined;
        var err_storage: [4096]u8 = undefined;
    };
    var out = std.Io.Writer.fixed(&Capture.out_storage);
    var err = std.Io.Writer.fixed(&Capture.err_storage);
    const streams: cli.Streams = .{ .out = &out, .err = &err };

    const code = import_cli.run(
        std.testing.allocator,
        std.testing.io,
        .{
            .source = source,
            .destination_parent = tmp.dir,
            .destination_name = destination,
            .project_code = sync.deriveProjectCode(&.{destination}),
            .submanifest = submanifest,
            .attribution = .committer,
            .default_branch = null,
            .quiet = quiet,
            .provenance = provenance,
        },
        streams,
        git.dependencies(publication),
    );

    return .{
        .code = code,
        .out = out.buffered(),
        .err = err.buffered(),
        .lifecycle = .{
            .launched = git.launched,
            .terminated = git.terminated,
            .stderr_drained = git.stderr_drained,
            .reaped = git.reaped,
        },
    };
}
fn retainedBytesForExport(bytes: []const u8) !u64 {
    var reader = fast_export.Reader.init(std.testing.allocator, .fixed(bytes), .{
        .max_line = 1024 * 1024,
        .max_data = 1024 * 1024 * 1024,
        .max_path = 4096,
        .max_events = std.math.maxInt(u64),
    });
    defer reader.deinit();

    var retained_bytes: u64 = 0;
    while (try reader.next()) |lease| {
        const charge = blk: {
            var event = lease;
            defer event.deinit();
            break :blk try import_cli.retainedByteCharge(event.value);
        };
        retained_bytes = try std.math.add(u64, retained_bytes, charge);
    }
    return retained_bytes;
}

/// How far a fast-export stream gets before it stops, and how it stopped.
const ExportReach = struct { events: usize, rejected: bool };

/// Replays `bytes` through the same reader the import uses.
///
/// This is the discriminator the two export-fault tests are named for, and
/// `create_store` cannot supply it: that boundary is checked BEFORE
/// `createRawGeneration`, so a stream rejected at record zero and a stream
/// truncated after a blob observe it identically. The count of records this
/// reader accepts is what separates them -- at least one accepted record means
/// the failure lands with generation 1 open and already written, which is the
/// window those tests claim and the harder one to unwind.
fn exportReach(bytes: []const u8) !ExportReach {
    var reader = fast_export.Reader.init(std.testing.allocator, .fixed(bytes), .{
        .max_line = 1024 * 1024,
        .max_data = 1024 * 1024 * 1024,
        .max_path = 4096,
        .max_events = std.math.maxInt(u64),
    });
    defer reader.deinit();
    var events: usize = 0;
    while (true) {
        const next = reader.next() catch return .{ .events = events, .rejected = true };
        var lease = next orelse return .{ .events = events, .rejected = false };
        lease.deinit();
        events += 1;
    }
}

fn exists(io: std.Io, dir: std.Io.Dir, path: []const u8) bool {
    var file = dir.openFile(io, path, .{}) catch return false;
    file.close(io);
    return true;
}
fn directoryExists(io: std.Io, dir: std.Io.Dir, path: []const u8) bool {
    var child = dir.openDir(io, path, .{}) catch return false;
    child.close(io);
    return true;
}

/// Records which publication boundaries a run actually reached.
///
/// This answers HOW FAR the transaction got, and nothing else. It cannot answer
/// "was generation 1 open", because `create_store` is checked BEFORE
/// `createRawGeneration` and is the only boundary in the whole prologue: every
/// failure that reaches `importInto` at all observes it, including one that
/// never opened a store. What it does prove is the negative half -- that the
/// fault unwound from the export and not from somewhere downstream with a
/// different abort path.
///
/// Works through either harness: `runFixture` passes hooks through untouched,
/// and `CommandScript.dependencies` chains a caller's observer behind its own
/// rather than replacing it.
const PointRecorder = struct {
    seen: std.EnumSet(FailurePoint) = .initEmpty(),

    fn notify(context: *anyopaque, point: FailurePoint) void {
        const self: *PointRecorder = @ptrCast(@alignCast(context));
        self.seen.insert(point);
    }

    /// No `fail_at`: the fault under test is upstream of publication, and this
    /// only watches the boundaries the transaction walks on its way out.
    fn hooks(self: *PointRecorder) import_cli.PublicationHooks {
        return .{ .observer = .{ .context = self, .notify = notify } };
    }
};

/// A published destination holds the publish manifest and nothing else.
///
/// The expected set is BORROWED from production rather than retyped: this is
/// the same list `assertPublishManifest` checks staging against before the
/// rename, and a copy here could agree with a stale idea of what a published
/// import contains.
fn expectPublishManifestExactly(io: std.Io, destination: std.Io.Dir) !void {
    const manifest = import_cli.PublicationBundle.publish_manifest;
    var seen: [manifest.len]bool = @splat(false);
    // Reopened for iteration: the caller's handle may lack iterate rights,
    // which Linux enforces at readdir time (macOS does not).
    var it_dir = try destination.openDir(io, ".", .{ .iterate = true });
    defer it_dir.close(io);
    var entries = it_dir.iterate();
    while (try entries.next(io)) |entry| {
        const index = for (manifest, 0..) |name, position| {
            if (std.mem.eql(u8, entry.name, name)) break position;
        } else {
            std.debug.print("unexpected published entry: {s}\n", .{entry.name});
            return error.UnexpectedPublishedEntry;
        };
        if (seen[index]) return error.DuplicatePublishedEntry;
        seen[index] = true;
    }
    for (seen, manifest) |present, name| {
        if (!present) {
            std.debug.print("missing published entry: {s}\n", .{name});
            return error.MissingPublishedEntry;
        }
    }
}

fn expectNoPublication(io: std.Io, tmp: std.testing.TmpDir, destination: []const u8) !void {
    try std.testing.expect(!directoryExists(io, tmp.dir, destination));
    try std.testing.expect(!exists(io, tmp.dir, ".zit-import-map.jsonl"));
    // Reopened for iteration: the caller's handle may lack iterate rights,
    // which Linux enforces at readdir time (macOS does not).
    var it_dir = try tmp.dir.openDir(io, ".", .{ .iterate = true });
    defer it_dir.close(io);
    var entries = it_dir.iterate();
    while (try entries.next(io)) |entry| {
        try std.testing.expect(!std.mem.startsWith(u8, entry.name, ".zit-import-tmp-"));
    }
}

test "import-git --provenance publishes .zit refs and map together only after a complete export" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var git: ScriptedGit = .{ .fast_export = valid_export, .stderr = "Enumerating objects\n" };
    const result = runFixtureFull(
        tmp,
        "fixture.git",
        "imported",
        false,
        &git,
        .real_filesystem,
        .{ .tree = 5 },
        true,
    );

    try std.testing.expectEqual(cli.Exit.ok, result.code);
    try std.testing.expectEqualStrings("imported\n", result.out);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "import-git:") != null);
    var destination = try tmp.dir.openDir(std.testing.io, "imported", .{});
    defer destination.close(std.testing.io);
    try std.testing.expect(exists(std.testing.io, destination, ".zit"));
    try std.testing.expect(exists(std.testing.io, destination, "refs"));
    try std.testing.expect(exists(std.testing.io, destination, ".zit-import-map.jsonl"));
    var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined;
    const imported_checkout = (try checkout.read(std.testing.io, destination, &checkout_buf)).?;
    try std.testing.expectEqualStrings("", imported_checkout.version);
    // The checkout records `trunk`, not the source's `main`: an import adopts
    // this format's name for the main line, which is what every downstream
    // consumer that must name it without being told already looks for.
    try std.testing.expectEqualStrings("trunk", imported_checkout.branch);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched);
    try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
}

test "import-git finalizes remote refs emitted with all refs" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var git: ScriptedGit = .{ .fast_export = valid_export_with_remote_ref };
    const result = runFixtureFull(tmp, "fixture.git", "with-remote", false, &git, .real_filesystem, .{ .tree = 5 }, true);

    try std.testing.expectEqual(cli.Exit.ok, result.code);
    var destination = try tmp.dir.openDir(std.testing.io, "with-remote", .{});
    defer destination.close(std.testing.io);
    // One sorted packed file, not a directory tree. Reading the bytes rather
    // than probing for paths is the point: the file's whole job is to be
    // legible, and its ordering is what makes two imports comparable.
    const packed_refs = try destination.readFileAlloc(std.testing.io, "refs", std.testing.allocator, .limited(4096));
    defer std.testing.allocator.free(packed_refs);
    try std.testing.expectEqualStrings(
        import_cli.packed_refs_header ++
            "sym-heads/main 104f740458ab03713ed37f67639379c91e4ab3c8992986a2a3483f90f8eb7b19\n" ++
            "sym-refs/remotes/origin/main 104f740458ab03713ed37f67639379c91e4ab3c8992986a2a3483f90f8eb7b19\n",
        packed_refs,
    );
}

test "import-git refuses an existing destination before Git starts" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.createDir(std.testing.io, "already-there", .default_dir);

    var git: ScriptedGit = .{ .fast_export = valid_export };
    const result = runFixture(tmp, "fixture.git", "already-there", false, &git, .real_filesystem);

    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expectEqualStrings("", result.out);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "already exists") != null);
    try std.testing.expectEqual(@as(usize, 0), result.lifecycle.launched);
    try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated);
    try std.testing.expectEqual(@as(usize, 0), result.lifecycle.stderr_drained);
    try std.testing.expectEqual(@as(usize, 0), result.lifecycle.reaped);
}

test "import-git aborts malformed export by terminating draining and reaping once" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var git: ScriptedGit = .{ .fast_export = malformed_export, .stderr = "fatal fixture diagnostic\n" };
    const result = runFixture(tmp, "fixture.git", "broken", false, &git, .real_filesystem);

    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expectEqualStrings("", result.out);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "fatal fixture diagnostic") != null);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.terminated);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
    try expectNoPublication(std.testing.io, tmp, "broken");
}

const FailurePoint = import_cli.PublicationHooks.FailurePoint;

// The publication boundaries this file drives, partitioned by the answer each
// one is owed, and hoisted to file scope so the partition itself can be
// asserted rather than believed.
//
// A `FailurePoint` that appears in no class is a fault arm no test ever runs,
// and nothing about the tag says so: it compiles, it is selectable, and a
// caller who asks for it gets a clean green because `check` never fires it. The
// alias map already produced that shape once -- five of the eight legacy
// selectors were inert and passing. These five names are the only places a new
// boundary can be registered.
//
// What the gate below proves is CLASSIFICATION, not execution: a new
// `FailurePoint` that nobody puts in a class fails it, which is the drift that
// actually happens. It does not prove the three singletons still have tests --
// deleting one leaves its `const` classified and the gate green. Making
// execution observable would need runtime registration, and that is
// order-dependent and breaks under `--test-filter`, so the cure is worse than
// the disease. Deleting a whole test is a visible line in a diff; adding an
// enum member is not.
const create_failure_point: FailurePoint = .create_store;
const post_rename_failure_point: FailurePoint = .after_rename_no_replace;
const parent_sync_failure_point: FailurePoint = .sync_parent_after_publish;

const legacy_failure_points = [_]FailurePoint{
    .flush_store,              .sync_store,
    .close_store,              .flush_map,
    .sync_map,                 .close_map,
    .sync_temporary_directory, .rename_no_replace,
};

test "import-git removes all temporary output when storage creation fails" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var git: ScriptedGit = .{ .fast_export = valid_export };
    const result = runFixture(
        tmp,
        "fixture.git",
        "storage-failed",
        false,
        &git,
        .{ .fail_at = create_failure_point },
    );

    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expectEqualStrings("", result.out);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
    try expectNoPublication(std.testing.io, tmp, "storage-failed");
}

// Every legacy selector, not a sample of three.
//
// The one-sided selectors are aliases: `check` maps each to its `before_*`
// twin, so a selector whose arm is missing from that map does not fail the
// import at all -- it PUBLISHES, and the caller that asked for a fault reads
// the resulting green as evidence the boundary is safe. Three of these eight
// arms were exercised and five were not, which is the shape where a silently
// inert selector survives. Asserting `Exit.failure` per alias is what makes
// each arm's presence load-bearing.
test "import-git removes all temporary output when publication flush or rename fails" {
    for (legacy_failure_points) |failure| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var git: ScriptedGit = .{ .fast_export = valid_export };
        const result = runFixture(
            tmp,
            "fixture.git",
            "publication-failed",
            false,
            &git,
            .{ .fail_at = failure },
        );

        try std.testing.expectEqual(cli.Exit.failure, result.code);
        try std.testing.expectEqualStrings("", result.out);
        // No child left behind: launched once, reaped once, every alias.
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched);
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
        try expectNoPublication(std.testing.io, tmp, "publication-failed");
    }
}

test "import-git writes only a machine-readable destination to stdout and progress to stderr" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var git: ScriptedGit = .{ .fast_export = valid_export, .stderr = "git progress\n" };
    const result = runFixture(tmp, "fixture.git", "machine", false, &git, .real_filesystem);

    try std.testing.expectEqual(cli.Exit.ok, result.code);
    try std.testing.expectEqualStrings("machine\n", result.out);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "git progress") != null);
    try std.testing.expect(std.mem.indexOf(u8, result.out, "progress") == null);
}

test "import-git --quiet suppresses progress but not diagnostics" {
    var quiet_tmp = std.testing.tmpDir(.{});
    defer quiet_tmp.cleanup();
    var quiet_git: ScriptedGit = .{ .fast_export = valid_export, .stderr = "git progress\n" };
    const quiet = runFixture(quiet_tmp, "fixture.git", "quiet", true, &quiet_git, .real_filesystem);
    try std.testing.expectEqual(cli.Exit.ok, quiet.code);
    try std.testing.expectEqualStrings("quiet\n", quiet.out);
    try std.testing.expectEqualStrings("", quiet.err);

    var error_tmp = std.testing.tmpDir(.{});
    defer error_tmp.cleanup();
    var error_git: ScriptedGit = .{ .fast_export = malformed_export, .stderr = "fatal fixture diagnostic\n" };
    const failed = runFixture(error_tmp, "fixture.git", "quiet-error", true, &error_git, .real_filesystem);
    try std.testing.expectEqual(cli.Exit.failure, failed.code);
    try std.testing.expectEqualStrings("", failed.out);
    try std.testing.expect(std.mem.indexOf(u8, failed.err, "fatal fixture diagnostic") != null);
}

// The remaining tests specify the Task 10 seam not yet present in
// `git_import_cli`: CommandScript records every preflight/export invocation,
// supplies bounded stdout/stderr streams, and exposes deterministic
// cancellation and publication-boundary faults. Production dispatch constructs
// the OS adapter; tests use no shell or network.
fn scripted(
    tmp: std.testing.TmpDir,
    script: *import_cli.CommandScript,
    destination: []const u8,
    quiet: bool,
    fault: import_cli.PublicationHooks,
    capture: *import_cli.ScriptCapture,
) import_cli.ScriptResult {
    return import_cli.runScripted(
        std.testing.allocator,
        std.testing.io,
        .{
            .source = "fixture.git",
            .destination_parent = tmp.dir,
            .destination_name = destination,
            .project_code = sync.deriveProjectCode(&.{destination}),
            .submanifest = .{ .tree = 5 },
            .attribution = .committer,
            .default_branch = null,
            .quiet = quiet,
        },
        script,
        fault,
        capture,
    );
}

const ProcessResult = struct {
    code: cli.Exit,
    out: []const u8,
    err: []const u8,
    exit_code: u8,
    reaped: bool,
    reap_calls: usize,
    child_gone: bool,
    stderr_bytes_drained: usize,
    retained_diagnostic_len: usize,
};

/// Drives the real ProductionGit session against the build-wired helper. The
/// only substitution is argv; spawning, pipe adapters, concurrent diagnostic
/// drain, terminate, join, and wait are the production implementation.
fn runProductionPipe(
    tmp: std.testing.TmpDir,
    destination: []const u8,
    mode: []const u8,
) !ProcessResult {
    var threaded = std.Io.Threaded.init(std.testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();
    var session = import_cli.ProductionGit.initForTest(
        std.testing.allocator,
        io,
        std.testing.environ,
        &.{ helper_options.git_import_pipe_helper, mode },
    );
    const Capture = struct {
        var out_storage: [4096]u8 = undefined;
        var err_storage: [8192]u8 = undefined;
    };
    var out = std.Io.Writer.fixed(&Capture.out_storage);
    var err = std.Io.Writer.fixed(&Capture.err_storage);
    const code = import_cli.run(
        std.testing.allocator,
        io,
        .{
            .source = "fixture.git",
            .destination_parent = tmp.dir,
            .destination_name = destination,
            .project_code = sync.deriveProjectCode(&.{destination}),
        },
        .{ .out = &out, .err = &err },
        session.dependencies(.real_filesystem),
    );
    return .{
        .code = code,
        .out = out.buffered(),
        .err = err.buffered(),
        .exit_code = session.exit_code,
        .reaped = session.reaped,
        .child_gone = session.child == null,
        .stderr_bytes_drained = session.stderr_bytes_drained,
        .reap_calls = session.reap_calls,
        .retained_diagnostic_len = session.stderr_ring_len,
    };
}
fn runGit(io: std.Io, argv: []const []const u8) !void {
    const result = try std.process.run(std.testing.allocator, io, .{ .argv = argv });
    defer std.testing.allocator.free(result.stdout);
    defer std.testing.allocator.free(result.stderr);
    switch (result.term) {
        .exited => |code| if (code == 0) return,
        else => {},
    }
    return error.GitSetupFailed;
}

fn writeFixtureFile(io: std.Io, dir: std.Io.Dir, path: []const u8, bytes: []const u8) !void {
    var file = try dir.createFile(io, path, .{ .read = true, .truncate = true });
    defer file.close(io);
    try file.writeStreamingAll(io, bytes);
}

fn runProductionPreflight(
    tmp: std.testing.TmpDir,
    source: []const u8,
    destination: []const u8,
) cli.Exit {
    const Capture = struct {
        var out_storage: [1024]u8 = undefined;
        var err_storage: [1024]u8 = undefined;
    };
    var out = std.Io.Writer.fixed(&Capture.out_storage);
    var err = std.Io.Writer.fixed(&Capture.err_storage);
    var session = import_cli.ProductionGit.init(std.testing.allocator, std.testing.io, std.testing.environ);
    defer session.deinit();
    return import_cli.run(
        std.testing.allocator,
        std.testing.io,
        .{
            .source = source,
            .destination_parent = tmp.dir,
            .destination_name = destination,
            .project_code = sync.deriveProjectCode(&.{destination}),
        },
        .{ .out = &out, .err = &err },
        session.dependencies(.real_filesystem),
    );
}

test "import-git parser leaves k unset for analysis and is a real dispatched verb" {
    // Absent means "analyze", not "5": the parser must not manufacture a pin
    // the runner would then be unable to distinguish from an operator's.
    const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" });
    try std.testing.expectEqual(@as(?u4, null), defaulted.submanifest_k);
    try std.testing.expectEqual(cli.Verb.import_git, cli.verbOf("import-git"));
    try std.testing.expect(!cli.isPlanned("import-git"));

    const parsed = try cli.parseImportGitArgs(&.{
        "--submanifest-k",  "7",
        "--attribution",    "author",
        "--default-branch", "main",
        "--quiet",          "fixture.git",
        "dest",
    });
    try std.testing.expectEqual(@as(?u4, 7), parsed.submanifest_k);
    try std.testing.expectEqual(.author, parsed.attribution);
    try std.testing.expectEqualStrings("main", parsed.default_branch.?);
    try std.testing.expect(parsed.quiet);
    const min_k = try cli.parseImportGitArgs(&.{ "--submanifest-k", "1", "fixture.git", "dest" });
    try std.testing.expectEqual(@as(?u4, 1), min_k.submanifest_k);
    const max_k = try cli.parseImportGitArgs(&.{ "--submanifest-k", "12", "fixture.git", "dest" });
    try std.testing.expectEqual(@as(?u4, 12), max_k.submanifest_k);
    for ([_][]const u8{ "0", "13", "14", "15" }) |value| {
        try std.testing.expectError(
            cli.ImportGitParseError.InvalidSubmanifestK,
            cli.parseImportGitArgs(&.{ "--submanifest-k", value, "fixture.git", "dest" }),
        );
    }
}

test "import-git parser accepts --no-submanifests and refuses it paired with a pinned k" {
    const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" });
    try std.testing.expect(!defaulted.no_submanifests);

    // The flag declines the mechanism; it does not manufacture a k, and it must
    // not be confusable with one -- `submanifest_k` stays absent.
    const flat = try cli.parseImportGitArgs(&.{ "--no-submanifests", "fixture.git", "dest" });
    try std.testing.expect(flat.no_submanifests);
    try std.testing.expectEqual(@as(?u4, null), flat.submanifest_k);

    // BOTH orders. An in-arm check would only fire for whichever token happened
    // to be seen second, which is exactly the bug this pairing invites.
    try std.testing.expectError(
        cli.ImportGitParseError.ConflictingSubmanifestOptions,
        cli.parseImportGitArgs(&.{ "--no-submanifests", "--submanifest-k", "5", "fixture.git", "dest" }),
    );
    try std.testing.expectError(
        cli.ImportGitParseError.ConflictingSubmanifestOptions,
        cli.parseImportGitArgs(&.{ "--submanifest-k", "5", "--no-submanifests", "fixture.git", "dest" }),
    );

    // A malformed token still reports its own cause: the conflict is ranked
    // below `first_err`, so an invalid k is not masked by the pairing.
    try std.testing.expectError(
        cli.ImportGitParseError.InvalidSubmanifestK,
        cli.parseImportGitArgs(&.{ "--submanifest-k", "0", "--no-submanifests", "fixture.git", "dest" }),
    );

    // Same shape as every other boolean flag on this verb.
    try std.testing.expectError(
        cli.ImportGitParseError.RepeatedOption,
        cli.parseImportGitArgs(&.{ "--no-submanifests", "--no-submanifests", "fixture.git", "dest" }),
    );

    // Help wins over the conflict, so `--help` stays informative no matter what
    // else is on the line.
    const helped = try cli.parseImportGitArgs(&.{ "--no-submanifests", "--submanifest-k", "5", "--help" });
    try std.testing.expect(helped.help);

    // It composes with every other behavioral flag; none of them interact.
    const composed = try cli.parseImportGitArgs(&.{
        "--no-submanifests", "--refuse-signed",
        "--attribution",     "author",
        "--default-branch",  "trunk",
        "--quiet",           "fixture.git",
        "dest",
    });
    try std.testing.expect(composed.no_submanifests);
    try std.testing.expect(composed.refuse_signed);
    try std.testing.expectEqual(.author, composed.attribution);
    try std.testing.expectEqualStrings("trunk", composed.default_branch.?);
    try std.testing.expect(composed.quiet);
}

test "import-git parser exposes the export payload budget as an operator flag" {
    // Every ceiling an honest source can reach must be raisable without a
    // recompile. This one budget carries both the cumulative payload limit and
    // the derived per-commit file-command ceiling, so it is the only knob the
    // verb needs.
    const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" });
    try std.testing.expectEqual(@as(?u64, null), defaulted.max_export_payload_bytes);

    const raised = try cli.parseImportGitArgs(&.{
        "--max-export-payload-bytes", "68719476736",
        "fixture.git",                "dest",
    });
    try std.testing.expectEqual(@as(?u64, 68719476736), raised.max_export_payload_bytes);

    // Zero derives a per-commit ceiling of zero: a budget no import can ever
    // satisfy is a typo, not a policy.
    try std.testing.expectError(
        cli.ImportGitParseError.InvalidExportPayloadBytes,
        cli.parseImportGitArgs(&.{ "--max-export-payload-bytes", "0", "fixture.git", "dest" }),
    );
    try std.testing.expectError(
        cli.ImportGitParseError.InvalidExportPayloadBytes,
        cli.parseImportGitArgs(&.{ "--max-export-payload-bytes", "many", "fixture.git", "dest" }),
    );
    try std.testing.expectError(
        cli.ImportGitParseError.MissingOptionValue,
        cli.parseImportGitArgs(&.{ "fixture.git", "dest", "--max-export-payload-bytes" }),
    );
    try std.testing.expectError(
        cli.ImportGitParseError.RepeatedOption,
        cli.parseImportGitArgs(&.{
            "--max-export-payload-bytes", "1024",
            "--max-export-payload-bytes", "2048",
            "fixture.git",                "dest",
        }),
    );
}

/// Whether `token` appears anywhere in `argv`. Positional indices would encode
/// the argv's shape, which is not what this asserts.
fn argvHas(argv: []const []const u8, token: []const u8) bool {
    for (argv) |candidate| {
        if (std.mem.eql(u8, candidate, token)) return true;
    }
    return false;
}

test "import-git strips signatures by default and aborts only under --refuse-signed" {
    // Fossil has no signature card, so refusing by default withheld the import
    // without saving anything. The default must therefore strip.
    const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" });
    try std.testing.expect(!defaulted.refuse_signed);
    const stripping = import_cli.exportArgv("fixture.git", defaulted.refuse_signed);
    try std.testing.expect(argvHas(&stripping, "--signed-commits=strip"));
    try std.testing.expect(argvHas(&stripping, "--signed-tags=strip"));
    try std.testing.expect(!argvHas(&stripping, "--signed-commits=abort"));
    try std.testing.expect(!argvHas(&stripping, "--signed-tags=abort"));

    // The opt-out restores the refusal, and nothing else about the argv moves.
    const opted_out = try cli.parseImportGitArgs(&.{ "--refuse-signed", "fixture.git", "dest" });
    try std.testing.expect(opted_out.refuse_signed);
    const aborting = import_cli.exportArgv("fixture.git", opted_out.refuse_signed);
    try std.testing.expect(argvHas(&aborting, "--signed-commits=abort"));
    try std.testing.expect(argvHas(&aborting, "--signed-tags=abort"));
    try std.testing.expectEqual(stripping.len, aborting.len);
    for (stripping, aborting) |strip_token, abort_token| {
        if (std.mem.startsWith(u8, strip_token, "--signed-")) continue;
        try std.testing.expectEqualStrings(strip_token, abort_token);
    }

    // Same error shape as every other boolean flag: repeated is a usage error,
    // and the retired spelling is simply unknown.
    try std.testing.expectError(
        cli.ImportGitParseError.RepeatedOption,
        cli.parseImportGitArgs(&.{ "--refuse-signed", "--refuse-signed", "fixture.git", "dest" }),
    );
    try std.testing.expectError(
        cli.ImportGitParseError.UnknownOption,
        cli.parseImportGitArgs(&.{ "--drop-signatures", "fixture.git", "dest" }),
    );
}

test "import-git help exits zero and malformed argument forms exit usage without process launch" {
    const help = import_cli.dispatchForTest(&.{"--help"});
    try std.testing.expectEqual(cli.Exit.ok, help.code);
    try std.testing.expectEqualStrings("", help.err);
    try std.testing.expect(std.mem.startsWith(u8, help.out, "usage: zit import-git SOURCE DEST"));
    // Substring checks are what let the previous hand transcription rot two
    // options behind the real help, so pin every option the usage line owns.
    for ([_][]const u8{
        "--submanifest-k K | --no-submanifests",
        "--attribution",
        "--default-branch",
        "--refuse-signed",
        "--provenance",
        "--max-export-payload-bytes",
        "--quiet",
    }) |option| {
        try std.testing.expect(std.mem.indexOf(u8, help.out, option) != null);
    }

    const accepted_flat = import_cli.dispatchForTest(&.{ "--no-submanifests", "fixture.git", "dest" });
    try std.testing.expectEqual(cli.Exit.failure, accepted_flat.code);
    try std.testing.expectEqual(@as(usize, 0), accepted_flat.lifecycle.launched);
    try std.testing.expectEqualStrings("test dispatch requires scripted dependencies\n", accepted_flat.err);

    const accepted_max = import_cli.dispatchForTest(&.{ "--submanifest-k", "12", "fixture.git", "dest" });
    try std.testing.expectEqual(cli.Exit.failure, accepted_max.code);
    try std.testing.expectEqual(@as(usize, 0), accepted_max.lifecycle.launched);
    try std.testing.expectEqualStrings("test dispatch requires scripted dependencies\n", accepted_max.err);

    for ([_][]const []const u8{
        &.{ "--submanifest-k", "0", "fixture.git", "dest" },
        &.{ "--submanifest-k", "13", "fixture.git", "dest" },
        &.{ "--submanifest-k", "14", "fixture.git", "dest" },
        &.{ "--submanifest-k", "15", "fixture.git", "dest" },
        &.{ "--attribution", "committerish", "fixture.git", "dest" },
        // The conflicting pair exits `usage` in both orders and launches
        // nothing -- no process is spawned to discover an argument mistake.
        &.{ "--no-submanifests", "--submanifest-k", "5", "fixture.git", "dest" },
        &.{ "--submanifest-k", "5", "--no-submanifests", "fixture.git", "dest" },
        &.{ "--no-submanifests", "--no-submanifests", "fixture.git", "dest" },
        &.{"fixture.git"},
        &.{ "fixture.git", "dest", "extra" },
    }) |args| {
        const usage = import_cli.dispatchForTest(args);
        try std.testing.expectEqual(cli.Exit.usage, usage.code);
        try std.testing.expectEqual(@as(usize, 0), usage.lifecycle.launched);
        try std.testing.expectEqualStrings("", usage.out);
        try std.testing.expect(usage.err.len != 0);
    }
}

test "import-git rejects missing Git non-repository shallow and partial sources before export" {
    const preflight_failures = [_]import_cli.CommandScript.Preflight{
        .git_missing,
        .not_repository,
        .missing_objects,
        .shallow,
        .partial_promisor,
    };
    for (preflight_failures) |preflight| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var script = import_cli.CommandScript.init(.{ .preflight = preflight });
        var result_cap: import_cli.ScriptCapture = .{};
        const result = scripted(tmp, &script, "rejected", false, .real_filesystem, &result_cap);
        try std.testing.expectEqual(cli.Exit.failure, result.code);
        try std.testing.expectEqual(@as(usize, 0), script.export_starts);
        try std.testing.expect(script.preflight_starts != 0);
        try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated);
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
        try expectNoPublication(std.testing.io, tmp, "rejected");
    }
}
test "import-git production preflight rejects absent non-repository missing-object shallow and partial sources" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var root_buf: [128]u8 = undefined;
    const root = try std.fmt.bufPrint(&root_buf, ".zig-cache/tmp/{s}", .{tmp.sub_path});
    var source_buf: [160]u8 = undefined;
    const source = try std.fmt.bufPrint(&source_buf, "{s}/source", .{root});

    // A non-existent -C path and an existing non-repository both execute real
    // ProductionGit query children and fail before an export can launch.
    try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "missing-git"));
    try tmp.dir.createDir(std.testing.io, "not-repository", .default_dir);
    var nonrepo_buf: [160]u8 = undefined;
    const nonrepo = try std.fmt.bufPrint(&nonrepo_buf, "{s}/not-repository", .{root});
    try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, nonrepo, "not-repository-dest"));
    try expectNoPublication(std.testing.io, tmp, "missing-git");
    try expectNoPublication(std.testing.io, tmp, "not-repository-dest");

    try runGit(std.testing.io, &.{ "git", "init", source });
    try runGit(std.testing.io, &.{ "git", "-C", source, "config", "user.name", "Zit Test" });
    try runGit(std.testing.io, &.{ "git", "-C", source, "config", "user.email", "[email protected]" });
    try runGit(std.testing.io, &.{ "git", "-C", source, "commit", "--allow-empty", "-m", "init" });

    try runGit(std.testing.io, &.{ "git", "-C", source, "config", "extensions.partialClone", "origin" });
    try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "partial"));
    try runGit(std.testing.io, &.{ "git", "-C", source, "config", "--unset", "extensions.partialClone" });

    try writeFixtureFile(std.testing.io, tmp.dir, "source/.git/shallow", "0000000000000000000000000000000000000000\n");
    try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "shallow"));
    try tmp.dir.deleteFile(std.testing.io, "source/.git/shallow");

    try tmp.dir.deleteTree(std.testing.io, "source/.git/objects");
    try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "missing-objects"));
    try expectNoPublication(std.testing.io, tmp, "partial");
    try expectNoPublication(std.testing.io, tmp, "shallow");
    try expectNoPublication(std.testing.io, tmp, "missing-objects");
}

test "import-git records detached HEAD and refuses it unless default branch is explicit" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{ .preflight = .detached_head, .fast_export = valid_export });
    var rejected_cap: import_cli.ScriptCapture = .{};
    const rejected = scripted(tmp, &script, "detached", false, .real_filesystem, &rejected_cap);
    try std.testing.expectEqual(cli.Exit.failure, rejected.code);
    try std.testing.expectEqual(@as(usize, 0), script.export_starts);
    try expectNoPublication(std.testing.io, tmp, "detached");

    script.default_branch = "main";
    var accepted_cap: import_cli.ScriptCapture = .{};
    const accepted = scripted(tmp, &script, "detached-with-branch", false, .real_filesystem, &accepted_cap);
    try std.testing.expectEqual(cli.Exit.ok, accepted.code);
    try std.testing.expect(script.detached_head_recorded);
}

test "import-git starts fast-export with the exact safe argv and disabled prompt and lazy fetch" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "argv", false, .real_filesystem, &result_cap);
    try std.testing.expectEqual(cli.Exit.ok, result.code);
    try std.testing.expectEqualStrings(
        "git -C fixture.git fast-export --all --show-original-ids --mark-tags --signed-commits=strip --signed-tags=strip --use-done-feature",
        script.exportCommandLine(),
    );
    try std.testing.expectEqualStrings("0", script.environment.get("GIT_TERMINAL_PROMPT").?);
    try std.testing.expectEqualStrings("1", script.environment.get("GIT_NO_LAZY_FETCH").?);
}

test "import-git drains real OS-pipe backpressure concurrently while stdout remains active" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const result = try runProductionPipe(tmp, "flood", "success");

    try std.testing.expectEqual(cli.Exit.ok, result.code);
    try std.testing.expectEqual(@as(usize, 8 * 1024 * 1024), result.stderr_bytes_drained);
    try std.testing.expectEqual(@as(usize, 4096), result.retained_diagnostic_len);
    // Strictly smaller than the capture: a full buffer means the completion
    // line asserted below may have been silently dropped, not that output was bounded.
    try std.testing.expect(result.err.len < 8192);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "imported 'fixture.git'") != null);
    try std.testing.expectEqual(@as(u8, 0), result.exit_code);
    try std.testing.expect(result.reaped);
    try std.testing.expect(result.child_gone);
}

test "import-git cancellation terminates a real child then joins and reaps its actual status once" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const result = try runProductionPipe(tmp, "cancelled", "hold");

    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expect(result.exit_code != 0);
    try std.testing.expect(result.err.len < 4096);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "Malformed") != null);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "GitExportFailed") == null);
    try std.testing.expect(result.reaped);
    try std.testing.expect(result.child_gone);
    try expectNoPublication(std.testing.io, tmp, "cancelled");
}

test "import-git non-Darwin/Linux fallback drains stable stderr ownership then reaps once" {
    if (builtin.os.tag == .macos or builtin.os.tag == .linux) return;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const result = try runProductionPipe(tmp, "fallback-cancelled", "hold");

    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expect(result.stderr_bytes_drained != 0);
    try std.testing.expect(result.reaped);
    try std.testing.expectEqual(@as(usize, 1), result.reap_calls);
    try std.testing.expect(result.child_gone);
    try expectNoPublication(std.testing.io, tmp, "fallback-cancelled");
}

test "import-git rejects a nonzero completed export after draining and reaping it once" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const result = try runProductionPipe(tmp, "nonzero-export", "nonzero");

    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expectEqual(@as(u8, 23), result.exit_code);
    try std.testing.expect(result.reaped);
    try std.testing.expect(result.child_gone);
    try expectNoPublication(std.testing.io, tmp, "nonzero-export");
}
test "import-git prefers a nonzero truncated export status over its parser EOF" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    const result = try runProductionPipe(tmp, "signed-like-export", "truncated-nonzero");

    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expectEqual(@as(u8, 23), result.exit_code);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "GitExportFailed") != null);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "fatal: signed commit rejected") != null);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "UnexpectedEof") == null);
    try std.testing.expect(result.reaped);
    try std.testing.expect(result.child_gone);
    try expectNoPublication(std.testing.io, tmp, "signed-like-export");
}

test "import-git verifies the frozen .zit and import map before the atomic rename" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "verified", false, .real_filesystem, &result_cap);
    try std.testing.expectEqual(cli.Exit.ok, result.code);
    try std.testing.expect(script.publication.store_reopened_for_verify);
    try std.testing.expect(script.publication.map_reopened_for_verify);
    try std.testing.expect(script.publication.verify_before_publish);
}

test "import-git competing destination at commit point never clobbers it and removes its temporary sibling" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{
        .preflight = .branch_head,
        .fast_export = valid_export,
        .competing_destination_at_publish = true,
    });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "raced", false, .real_filesystem, &result_cap);
    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expect(script.competitor_destination_intact);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
    try std.testing.expect(!script.temporary_sibling_left);
}

test "import-git never publishes or deletes a replaced staging sibling" {
    const replacements = [_]import_cli.CommandScript.StagingReplacement{ .directory, .symlink };
    for (replacements) |replacement| {
        for (0..8) |_| {
            var tmp = std.testing.tmpDir(.{});
            defer tmp.cleanup();
            try tmp.dir.createDir(std.testing.io, "outside", .default_dir);
            try writeFixtureFile(std.testing.io, tmp.dir, "outside/replacement-sentinel", "survives\n");

            var script = import_cli.CommandScript.init(.{
                .preflight = .branch_head,
                .fast_export = valid_export,
                .replace_staging_at_publish = replacement,
            });
            var result_cap: import_cli.ScriptCapture = .{};
            const result = scripted(tmp, &script, "raced-staging", false, .real_filesystem, &result_cap);

            try std.testing.expectEqual(cli.Exit.failure, result.code);
            try std.testing.expect(std.mem.indexOf(u8, result.err, "StagingEntryReplaced") != null);
            try std.testing.expect(script.staging_replacement_observed);
            try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated);
            try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained);
            try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
            try std.testing.expect(!directoryExists(std.testing.io, tmp.dir, "raced-staging"));
            try std.testing.expect(!exists(std.testing.io, tmp.dir, ".zit-import-map.jsonl"));

            const staging_name = script.stagingReplacementName();
            try std.testing.expect(staging_name.len != 0);
            var outside = try tmp.dir.openDir(std.testing.io, "outside", .{ .follow_symlinks = false });
            defer outside.close(std.testing.io);
            try std.testing.expect(exists(std.testing.io, outside, "replacement-sentinel"));
            switch (replacement) {
                .directory => {
                    var preserved = try tmp.dir.openDir(std.testing.io, staging_name, .{ .follow_symlinks = false });
                    defer preserved.close(std.testing.io);
                    try std.testing.expect(exists(std.testing.io, preserved, "replacement-sentinel"));
                },
                .symlink => {
                    var target: [32]u8 = undefined;
                    const len = try tmp.dir.readLink(std.testing.io, staging_name, &target);
                    try std.testing.expectEqualStrings("outside", target[0..len]);
                },
            }
        }
    }
}

test "import-git default ceilings clear the retired 200k per-commit file wall" {
    const limits: import_cli.ImportLimits = .{};
    // The import leaves the arena budget at its default, so that default is
    // what an operator actually meets.
    const defaults: fast_export.Limits = .{ .max_line = 0, .max_data = 0, .max_path = 0, .max_events = std.math.maxInt(u64) };
    const ceiling = fast_export.fileCommandCeiling(defaults);
    // The retired ceiling was 200,000 because that is the benchmark corpus
    // width. The derived one must clear it by orders of magnitude, so no
    // ordinary monorepo root commit can reach it.
    try std.testing.expect(ceiling > 100 * 200_000);
    try std.testing.expectEqual(
        defaults.max_command_arena_bytes / fast_export.min_retained_bytes_per_file_command,
        ceiling,
    );
    // The cumulative payload budget must clear a real repository: sqlite's
    // export stream is 12.2 GiB, and a default that refuses it is a default
    // that refuses ordinary work. It must also no longer move the ceiling.
    try std.testing.expect(limits.max_export_payload_bytes > 16 * @as(u64, 1024 * 1024 * 1024));
    var raised = defaults;
    raised.max_total_data = limits.max_export_payload_bytes;
    try std.testing.expectEqual(ceiling, fast_export.fileCommandCeiling(raised));
    try std.testing.expect(limits.max_retained_bytes >= 64 * 1024 * 1024);
}

test "import-git reader limits reject before event and data parsing allocations" {
    const event_limited = "feature done\nblob\nmark :1\ndata 1\nx\n";
    var event_reader = fast_export.Reader.init(std.testing.allocator, .fixed(event_limited), .{
        .max_line = 1024,
        .max_data = 1024,
        .max_path = 1024,
        .max_events = 0,
        .max_total_data = 1024,
    });
    defer event_reader.deinit();
    try std.testing.expectError(error.EventLimitExceeded, event_reader.next());

    const data_limited = "feature done\nblob\nmark :1\ndata 1\nx\n";
    var data_reader = fast_export.Reader.init(std.testing.allocator, .fixed(data_limited), .{
        .max_line = 1024,
        .max_data = 1024,
        .max_path = 1024,
        .max_events = 1,
        .max_total_data = 0,
    });
    defer data_reader.deinit();
    try std.testing.expectError(error.DataLimitExceeded, data_reader.next());
}
test "import-git enforces total import ceilings before publication" {
    // The two budgets are separate now, so the accepted value only has to
    // clear the stream's nine payload bytes.
    const one_command_budget = fast_export.min_retained_bytes_per_file_command;
    const accepted_limits: import_cli.ImportLimits = .{
        .max_export_payload_bytes = one_command_budget,
        .max_retained_bytes = 1024,
    };
    const RejectedLimit = struct {
        limits: import_cli.ImportLimits,
        error_name: []const u8,
    };
    const rejected_limits = [_]RejectedLimit{
        .{
            // One byte below the stream's own payload. The blob's five bytes
            // admit; the commit message's four push the total past it, before
            // any file command is parsed.
            .limits = .{
                .max_export_payload_bytes = 8,
            },
            .error_name = "ExportPayloadLimitExceeded",
        },
        .{
            .limits = .{
                .max_export_payload_bytes = accepted_limits.max_export_payload_bytes,
                .max_retained_bytes = 0,
            },
            .error_name = "ImporterRetainedByteLimitExceeded",
        },
    };
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var script = import_cli.CommandScript.init(.{
            .preflight = .branch_head,
            .fast_export = valid_export_with_oids,
            .limits = accepted_limits,
        });
        var result_cap: import_cli.ScriptCapture = .{};
        const result = scripted(tmp, &script, "at-limit", false, .real_filesystem, &result_cap);
        try std.testing.expectEqual(cli.Exit.ok, result.code);
        try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated);
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained);
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
    }

    for (rejected_limits, 0..) |rejected, index| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var script = import_cli.CommandScript.init(.{
            .preflight = .branch_head,
            .fast_export = valid_export_with_oids,
            .limits = rejected.limits,
        });
        var destination: [32]u8 = undefined;
        const destination_name = try std.fmt.bufPrint(&destination, "limit-{d}", .{index});
        var result_cap: import_cli.ScriptCapture = .{};
        const result = scripted(tmp, &script, destination_name, false, .real_filesystem, &result_cap);

        try std.testing.expectEqual(cli.Exit.failure, result.code);
        try std.testing.expect(std.mem.indexOf(u8, result.err, rejected.error_name) != null);
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.terminated);
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained);
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
        try expectNoPublication(std.testing.io, tmp, destination_name);
    }
    {
        // One byte below one command's minimum retention derives a ceiling of
        // zero, so the stream's single `M` command is refused. The failure must
        // name the observed count, the ceiling, and the derivation. It must
        // NOT name a flag: this ceiling is structural, and the payload knob
        // that once raised it no longer moves it at all.
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var script = import_cli.CommandScript.init(.{
            .preflight = .branch_head,
            .fast_export = valid_export_with_oids,
            .limits = .{
                .max_command_arena_bytes = one_command_budget - 1,
                .max_retained_bytes = accepted_limits.max_retained_bytes,
            },
        });
        var result_cap: import_cli.ScriptCapture = .{};
        const result = scripted(tmp, &script, "command-ceiling", false, .real_filesystem, &result_cap);
        try std.testing.expectEqual(cli.Exit.failure, result.code);

        var expected: [512]u8 = undefined;
        const diagnostic = try std.fmt.bufPrint(
            &expected,
            "import-git: import failed: ExportFileCommandLimitExceeded\n" ++
                "import-git: a commit exceeded the per-commit file-command ceiling: " ++
                "observed at least 1, ceiling 0, derived from the {d}-byte per-commit " ++
                "arena budget at {d} minimum retained bytes per command. This is a " ++
                "structural bound on one commit, not a total: no option raises it, " ++
                "because a commit at this width is beyond what the importer can hold whole\n",
            .{ one_command_budget - 1, one_command_budget },
        );
        try std.testing.expect(std.mem.indexOf(u8, result.err, diagnostic) != null);
        try std.testing.expect(std.mem.indexOf(u8, result.err, "--max-export-payload-bytes") == null);
        try expectNoPublication(std.testing.io, tmp, "command-ceiling");
    }
    {
        // The cumulative-payload refusal must name its numbers for the same
        // reason the file-command one does. `ExportPayloadLimitExceeded` alone
        // sent a real 32k-commit import 17 minutes downstream to learn a single
        // integer, and the integer it needs is not even the one it was refused
        // at: parsing stops at the overrun, so the observed total is a lower
        // bound on the stream's payload and the budget must clear the whole of
        // it. Saying "above 9" would name a value that fails again.
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var script = import_cli.CommandScript.init(.{
            .preflight = .branch_head,
            .fast_export = valid_export_with_oids,
            .limits = .{
                .max_export_payload_bytes = 8,
                .max_retained_bytes = accepted_limits.max_retained_bytes,
            },
        });
        var result_cap: import_cli.ScriptCapture = .{};
        const result = scripted(tmp, &script, "payload-ceiling", false, .real_filesystem, &result_cap);
        try std.testing.expectEqual(cli.Exit.failure, result.code);
        try std.testing.expect(std.mem.indexOf(
            u8,
            result.err,
            "import-git: import failed: ExportPayloadLimitExceeded\n" ++
                "import-git: the export stream's cumulative payload exceeded its budget: " ++
                "observed at least 9 bytes against a budget of 8 bytes; parsing stopped there, " ++
                "so the stream's full payload is larger than the observed total; re-run with " ++
                "--max-export-payload-bytes above the stream's full payload\n",
        ) != null);
        try expectNoPublication(std.testing.io, tmp, "payload-ceiling");
    }
}

const pre_rename_failure_points = [_]FailurePoint{
    .before_flush_store,             .after_flush_store,
    .before_sync_store,              .after_sync_store,
    .before_close_store,             .after_close_store,
    .before_flush_map,               .after_flush_map,
    .before_sync_map,                .after_sync_map,
    .before_close_map,               .after_close_map,
    .capture_drop_set,               .create_packed_store,
    .before_pack,                    .after_pack,
    .before_sync_packed_store,       .after_sync_packed_store,
    .before_close_packed_store,      .after_close_packed_store,
    .verify_raw_store,               .verify_store,
    .verify_map,                     .before_discard_raw_generation,
    .after_discard_raw_generation,   .before_sync_temporary_directory,
    .after_sync_temporary_directory, .before_rename_no_replace,
};

test "import-git aborts every pre-rename writable-boundary failure without a partial destination" {
    for (pre_rename_failure_points) |failure| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export });
        var result_cap: import_cli.ScriptCapture = .{};
        const result = scripted(tmp, &script, "boundary-failed", false, .{ .fail_at = failure }, &result_cap);
        try std.testing.expectEqual(cli.Exit.failure, result.code);
        // All three clauses, per case. A pre-rename abort that only proves DEST
        // is absent is compatible with a retained staging sibling holding the
        // whole raw generation -- on a real corpus that is tens of megabytes of
        // garbage per failed import, and it is invisible to a destination-only
        // assertion.
        //
        // No child process.
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched);
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
        // No destination.
        try expectNoPublication(std.testing.io, tmp, "boundary-failed");
        // No temporary sibling -- asked a second time, fail-CLOSED, from
        // inside the harness. `expectNoPublication` scans `tmp.dir` from the
        // outside; this one is taken by `runScripted` the instant `run`
        // returns, against the parent the transaction was actually handed, and
        // it reports a LEAK whenever it cannot look -- no parent, no `io`, an
        // iterate error. A negative assertion answered "clean" on a question
        // that could not be asked is exactly how this clause would go vacuous.
        try std.testing.expect(!script.temporary_sibling_left);
    }
}

// The gate on the four fault sweeps above: every boundary the transaction
// declares is driven by one of them, and each is driven by exactly one.
//
// This is what makes "failures at EVERY new boundary" checkable instead of
// asserted. Without it, adding a `FailurePoint` -- which is what every new
// writable step in this transaction does -- silently ships an untested fault
// arm, and the suite stays green because the arm is never selected. Both
// directions are asserted for the same reason `ci_test.zig` asserts its step
// set both ways: a class list that names a point twice is a bookkeeping error
// that would otherwise let a real point go missing while the count still
// matched.
test "import-git drives every declared publication failure point in exactly one fault class" {
    var covered = std.EnumSet(FailurePoint).initEmpty();
    const singletons = [_]FailurePoint{
        create_failure_point,
        post_rename_failure_point,
        parent_sync_failure_point,
    };
    for (&[_][]const FailurePoint{
        &singletons,
        &legacy_failure_points,
        &pre_rename_failure_points,
    }) |class| {
        for (class) |point| {
            if (covered.contains(point)) {
                std.debug.print("failure point classified twice: {t}\n", .{point});
                return error.DuplicateFailurePointClass;
            }
            covered.insert(point);
        }
    }

    inline for (@typeInfo(FailurePoint).@"enum".fields) |field| {
        const point: FailurePoint = @enumFromInt(field.value);
        if (!covered.contains(point)) {
            std.debug.print("failure point never driven by any test: {s}\n", .{field.name});
            return error.UndrivenFailurePoint;
        }
    }
    try std.testing.expectEqual(@typeInfo(FailurePoint).@"enum".fields.len, covered.count());
}

// The rename is the commit point, and `after_rename_no_replace` is the first
// boundary strictly past it. Every other publication fault is answered with
// "DEST is absent"; this one must be answered with the opposite, because by the
// time it fires the verified generation is already the destination and undoing
// it would destroy a complete repository to report a failure about a step that
// happened afterwards.
//
// The staging sibling is gone here for the same reason -- it WAS the
// destination -- so `expectNoPublication`'s sibling clause is asserted directly
// rather than through a helper whose first clause would be inverted.
test "import-git keeps the published destination when a boundary strictly after the rename fails" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "post-rename", false, .{ .fail_at = post_rename_failure_point }, &result_cap);
    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
    try std.testing.expect(!script.temporary_sibling_left);

    var destination = try tmp.dir.openDir(std.testing.io, "post-rename", .{});
    defer destination.close(std.testing.io);
    // The whole manifest and NOTHING BESIDE IT. Naming only the entries that
    // must be present would pass a destination that also carried a fifth --
    // and the rename ships the staging directory whole, so anything extra in
    // it is published by accident into every imported repository.
    try expectPublishManifestExactly(std.testing.io, destination);
    // Generation 1 left staging before the rename, so it cannot have ridden
    // into the published destination on the way past this boundary.
    try std.testing.expect(!exists(std.testing.io, destination, ".zit-import-raw"));
}

// Cancellation and child failure WHILE GENERATION 1 EXISTS.
//
// Both faults land after `createRawGeneration` and before anything packs, which
// is the one window in which the transaction owns a live, half-written store
// that no later stage will ever look at. Neither seam had a test: `run` fails
// out of `consumeExport`, and the only thing that then removes generation 1 is
// the bundle's own abort path.
test "import-git unwinds a cancelled export that already opened the raw generation" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    // `cancel_export_mid_stream` replaces the stream with one that parses a blob
    // and then hits a record the reader rejects -- a truncation, not a rejection
    // at record zero, so the raw generation is open when it happens.
    var script = import_cli.CommandScript.init(.{
        .preflight = .branch_head,
        .fast_export = valid_export,
        .cancel_export_mid_stream = true,
    });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "cancelled", false, .real_filesystem, &result_cap);
    try std.testing.expectEqual(cli.Exit.failure, result.code);
    // The child is terminated and reaped exactly once: an abandoned export is
    // still a process, and leaking it would outlive the failed import.
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.terminated);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
    try expectNoPublication(std.testing.io, tmp, "cancelled");
    // Generation 1 was open and partly written when the cancel landed, so the
    // sibling holding it is exactly what must not survive. Fail-closed.
    try std.testing.expect(!script.temporary_sibling_left);
}

test "import-git unwinds a failed export child that already opened the raw generation" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    // A COMPLETE, well-formed stream whose child then exits non-zero. The
    // import has no parse error to fail on: the only thing wrong is the exit
    // status, and trusting a stream whose producer died is exactly how a
    // truncated history would publish as a whole one.
    var script = import_cli.CommandScript.init(.{
        .preflight = .branch_head,
        .fast_export = valid_export,
        .export_exit = 128,
    });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "child-failed", false, .real_filesystem, &result_cap);
    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
    try expectNoPublication(std.testing.io, tmp, "child-failed");
    try std.testing.expect(!script.temporary_sibling_left);
}

// The premise both tests above are NAMED for, pinned from the export stream and
// the boundary log instead of from a comment.
//
// Replayed through `runFixture` rather than `scripted` because the two faults
// need no script -- a stream and an exit status are the whole fixture -- and the
// pass-through harness is the smaller thing to be wrong about. (`scripted` would
// also work now: `CommandScript.dependencies` chains a caller's observer behind
// its own. It used to REPLACE it, which is how this pairing was first written
// and how it went green while observing nothing.)
test "import-git export faults that unwind generation 1 reach the raw store and nothing past it" {
    const Case = struct { name: []const u8, export_bytes: []const u8, exit_code: u8, rejected: bool };
    const cases = [_]Case{
        // The truncated stream `cancel_export_mid_stream` substitutes, BORROWED from
        // the seam rather than retyped: a hand copy would keep passing against
        // stale bytes the moment the production literal moved, and the claim
        // this case makes is a claim about that exact stream.
        .{
            .name = "cancelled-observed",
            .export_bytes = import_cli.CommandScript.cancelled_export,
            .exit_code = 0,
            .rejected = true,
        },
        // Complete and well-formed; only the child's status is wrong.
        .{ .name = "child-failed-observed", .export_bytes = valid_export, .exit_code = 128, .rejected = false },
    };
    for (cases) |case| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        // The premise, measured from the stream itself. `create_store` cannot
        // state it -- that boundary is checked before `createRawGeneration`, so
        // a stream rejected at record zero observes it exactly as a truncated
        // one does. Replaying through the same reader with the same limits says
        // which this is: at least one accepted record means the failure lands
        // with generation 1 open and already written.
        const reach = try exportReach(case.export_bytes);
        try std.testing.expect(reach.events >= 1);
        try std.testing.expectEqual(case.rejected, reach.rejected);

        var recorder: PointRecorder = .{};
        var git: ScriptedGit = .{ .fast_export = case.export_bytes, .export_exit = case.exit_code };
        const result = runFixture(tmp, "fixture.git", case.name, false, &git, recorder.hooks());
        try std.testing.expectEqual(cli.Exit.failure, result.code);
        // The transaction entered `importInto` rather than failing in
        // preflight or argument handling -- which is all this boundary can say.
        try std.testing.expect(recorder.seen.contains(create_failure_point));
        // And nothing downstream of it ran. A fault that unwound from further
        // along is a different window with a different abort path, and would
        // leave this pair claiming coverage it does not have.
        try std.testing.expect(!recorder.seen.contains(.capture_drop_set));
        try std.testing.expect(!recorder.seen.contains(.create_packed_store));
        try std.testing.expect(!recorder.seen.contains(.before_rename_no_replace));
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched);
        try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
        try expectNoPublication(std.testing.io, tmp, case.name);
    }
}

// The scripted harness passes a caller's observer THROUGH.
//
// `CommandScript.dependencies` must install its own observer -- that recorder is
// how the whole two-generation ordering in `Publication` gets written -- so the
// obvious implementation replaces the caller's, and that is what it used to do.
// The failure mode is the worst kind available to a test suite: `scripted` takes
// the observer, drops it without a diagnostic, and every assertion made through
// it passes while observing nothing.
//
// Asserted here rather than left to the doc comment, because a constraint that
// only a comment enforces is one the next caller re-discovers by shipping a
// vacuous green. A successful import walks the whole boundary sequence, so a
// dropped observer shows up as an EMPTY set against three points the run
// certainly reached.
test "import-git scripted runs report publication boundaries to a caller's observer" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var recorder: PointRecorder = .{};
    var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "observed", false, recorder.hooks(), &result_cap);
    try std.testing.expectEqual(cli.Exit.ok, result.code);
    // One from each generation and one from publication, so a chain that only
    // survived part of the transaction cannot satisfy this.
    try std.testing.expect(recorder.seen.contains(.create_store));
    try std.testing.expect(recorder.seen.contains(.create_packed_store));
    try std.testing.expect(recorder.seen.contains(.before_rename_no_replace));
    // The script's own recorder still ran: chaining must ADD a listener, not
    // trade one for the other. `raw_frozen_before_packing` is written by
    // `publicationEvent` and by nothing else.
    try std.testing.expect(script.publication.raw_frozen_before_packing);
}

test "import-git reports parent sync durability uncertainty after publish while retaining the verified destination" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "durability-uncertain", false, .{ .fail_at = parent_sync_failure_point }, &result_cap);
    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expect(result.published_durability_uncertain);
    var destination = try tmp.dir.openDir(std.testing.io, "durability-uncertain", .{});
    defer destination.close(std.testing.io);
    try std.testing.expect(exists(std.testing.io, destination, ".zit"));
    try std.testing.expect(!exists(std.testing.io, destination, ".zit-import-map.jsonl"));
}

test "import-git start session failure after stderr thread closes pipes and reaps exactly once" {
    var session = import_cli.ProductionGit.initForTest(
        std.testing.allocator,
        std.testing.io,
        std.testing.environ,
        &.{ helper_options.git_import_pipe_helper, "hold" },
    );
    defer session.deinit();
    session.test_startup_failure = .after_stderr_thread;

    const dependencies = session.dependencies(.real_filesystem);
    try std.testing.expectError(
        error.InjectedStartFailure,
        dependencies.git.vtable.start_export(dependencies.git.context, "fixture.git"),
    );
    try std.testing.expect(session.child == null);
    try std.testing.expect(session.stderr_thread == null);
    try std.testing.expect(session.stderr_file == null);
    try std.testing.expect(session.reaped);
    try std.testing.expectEqual(@as(usize, 1), session.reap_calls);
}

test "import-git rejects a replacement destination after rename without deleting it" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var script = import_cli.CommandScript.init(.{
        .preflight = .branch_head,
        .fast_export = valid_export,
        .replace_destination_after_rename = true,
    });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "replacement-after-rename", false, .real_filesystem, &result_cap);

    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "PublishedDestinationReplaced") != null);
    try std.testing.expect(script.competitor_destination_intact);
    var replacement = try tmp.dir.openDir(std.testing.io, "replacement-after-rename", .{
        .follow_symlinks = false,
    });
    defer replacement.close(std.testing.io);
    try std.testing.expect(exists(std.testing.io, replacement, "replacement-sentinel"));
    try std.testing.expect(!script.temporary_sibling_left);
}

test "import-git reaps after termination failure and reports the lifecycle error" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var git: ScriptedGit = .{
        .fast_export = malformed_export,
        .lifecycle_fault = .terminate,
    };
    const result = runFixture(tmp, "fixture.git", "termination-failure", false, &git, .real_filesystem);

    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "InjectedLifecycleFailure") != null);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.terminated);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained);
    try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped);
    try expectNoPublication(std.testing.io, tmp, "termination-failure");
}

test "import-git accounts the peeled target retained for marked tags at the exact byte boundary" {
    const exact_limit = try retainedBytesForExport(marked_tag_export);
    var exact_tmp = std.testing.tmpDir(.{});
    defer exact_tmp.cleanup();
    var exact_git: ScriptedGit = .{
        .fast_export = marked_tag_export,
        .limits = .{ .max_retained_bytes = exact_limit },
    };
    const exact = runFixture(exact_tmp, "fixture.git", "marked-tag-exact", false, &exact_git, .real_filesystem);
    try std.testing.expectEqual(cli.Exit.ok, exact.code);

    var exceeded_tmp = std.testing.tmpDir(.{});
    defer exceeded_tmp.cleanup();
    var exceeded_git: ScriptedGit = .{
        .fast_export = marked_tag_export,
        .limits = .{ .max_retained_bytes = exact_limit - 1 },
    };
    const exceeded = runFixture(exceeded_tmp, "fixture.git", "marked-tag-exceeded", false, &exceeded_git, .real_filesystem);
    try std.testing.expectEqual(cli.Exit.failure, exceeded.code);
    try std.testing.expect(std.mem.indexOf(u8, exceeded.err, "ImporterRetainedByteLimitExceeded") != null);
    try expectNoPublication(std.testing.io, exceeded_tmp, "marked-tag-exceeded");
}

/// A fast-export whose single commit lands `count` distinct paths. Wide enough,
/// at the k the tree-mode side of the comparison pins, that tree emission
/// really stores SUBMANIFEST artifacts -- without that, "the flat import stored
/// none" would be true of both modes and would prove nothing.
fn wideExport(gpa: std.mem.Allocator, count: usize) ![]u8 {
    var out: std.ArrayList(u8) = .empty;
    errdefer out.deinit(gpa);
    try out.appendSlice(
        gpa,
        "feature done\n" ++
            "blob\n" ++
            "mark :1\n" ++
            "data 5\n" ++
            "hello\n" ++
            "commit refs/heads/main\n" ++
            "mark :2\n" ++
            "author A <[email protected]> 0 +0000\n" ++
            "committer A <[email protected]> 0 +0000\n" ++
            "data 4\n" ++
            "init\n",
    );
    for (0..count) |index| {
        const line = try std.fmt.allocPrint(gpa, "M 100644 :1 file{d:0>6}.txt\n", .{index});
        defer gpa.free(line);
        try out.appendSlice(gpa, line);
    }
    try out.appendSlice(gpa, "\n" ++ "done\n");
    return out.toOwnedSlice(gpa);
}

const ImportedSubmanifestState = struct {
    enable_submanifests: bool,
    submanifest_k: u8,
    seen_submanifest: bool,
    /// Whether ANY artifact in the produced store is a SUBMANIFEST. Read off
    /// the store itself rather than off the config frame, so the two statements
    /// can disagree and be caught doing it.
    submanifests_stored: bool,
    checkin_paths: usize,
};

/// Everything this feature claims about a produced repository, read back from
/// the repository: the four persisted sub-manifest settings, whether the store
/// holds a SUBMANIFEST artifact, and the flattened path count of the one
/// check-in. The path count is what lets the caller assert that the two modes
/// imported the SAME history.
fn readImportedSubmanifestState(io: std.Io, destination: std.Io.Dir) !ImportedSubmanifestState {
    const gpa = std.testing.allocator;
    var file = try destination.openFile(io, ".zit", .{});
    defer file.close(io);
    const stat = try file.stat(io);
    var file_storage = storage.FileStorage.init(io, file, stat.size);
    var log = try logstore.LogStore.openReadOnly(gpa, file_storage.storageFile());
    defer log.deinit(gpa);

    var owned = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult;
    defer owned.deinit(gpa);
    try std.testing.expect(!owned.config.submanifest_auto_retune);

    const rids = try log.ridsInPersistedOrder(gpa);
    defer gpa.free(rids);
    var submanifests: usize = 0;
    var checkins: usize = 0;
    var checkin_paths: usize = 0;
    for (rids) |rid| {
        const bytes = (try log.content(gpa, rid)) orelse continue;
        defer gpa.free(bytes);
        // A blob is not parseable as an artifact; skipping is the whole
        // classification here, so it must not be an error.
        var manifest = artifact.parse(gpa, bytes) catch continue;
        defer manifest.deinit();
        switch (manifest.kind) {
            .submanifest => submanifests += 1,
            .manifest => {
                checkins += 1;
                checkin_paths = if (manifest.s.len != 0) manifest.s.len else manifest.f.len;
            },
            else => {},
        }
    }
    try std.testing.expectEqual(@as(usize, 1), checkins);
    return .{
        .enable_submanifests = owned.config.enable_submanifests,
        .submanifest_k = owned.config.submanifest_k,
        .seen_submanifest = owned.config.seen_submanifest,
        .submanifests_stored = submanifests != 0,
        .checkin_paths = checkin_paths,
    };
}

/// Every check-in manifest in the store carries only F cards, and no artifact
/// in it is a SUBMANIFEST. This is the store-side reading of §8.1's "MUST NOT
/// emit an S card"; it consults no flag and no config field.
fn expectNoSCardsAnywhere(io: std.Io, destination: std.Io.Dir) !void {
    const gpa = std.testing.allocator;
    var file = try destination.openFile(io, ".zit", .{});
    defer file.close(io);
    const stat = try file.stat(io);
    var file_storage = storage.FileStorage.init(io, file, stat.size);
    var log = try logstore.LogStore.openReadOnly(gpa, file_storage.storageFile());
    defer log.deinit(gpa);

    const rids = try log.ridsInPersistedOrder(gpa);
    defer gpa.free(rids);
    for (rids) |rid| {
        const bytes = (try log.content(gpa, rid)) orelse continue;
        defer gpa.free(bytes);
        var manifest = artifact.parse(gpa, bytes) catch continue;
        defer manifest.deinit();
        try std.testing.expect(manifest.kind != .submanifest);
        try std.testing.expectEqual(@as(usize, 0), manifest.s.len);
    }
}

test "import-git --no-submanifests leaves the destination at the spec-default config with no S cards" {
    const gpa = std.testing.allocator;
    // 96 paths at k=1 is comfortably past the point the canonical partition
    // yields more than one group, which the tree-mode half below proves by
    // actually storing SUBMANIFEST artifacts from this exact stream.
    const export_stream = try wideExport(gpa, 96);
    defer gpa.free(export_stream);

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

    var flat_git: ScriptedGit = .{ .fast_export = export_stream };
    const flat = runFixtureShaped(tmp, "fixture.git", "flat", false, &flat_git, .real_filesystem, .flat);
    try std.testing.expectEqual(cli.Exit.ok, flat.code);
    // "Do the default thing" reports nothing about sub-manifests.
    try std.testing.expect(std.mem.indexOf(u8, flat.err, "sub-manifest") == null);

    var tree_git: ScriptedGit = .{ .fast_export = export_stream };
    const tree = runFixtureShaped(tmp, "fixture.git", "tree", false, &tree_git, .real_filesystem, .{ .tree = 1 });
    try std.testing.expectEqual(cli.Exit.ok, tree.code);

    // The other side of the same gate. Flat's silence above only means
    // something if an unpinned tree import does report its analyzed k, so pin
    // that here too -- otherwise deleting the report entirely would leave both
    // halves green.
    var analyzed_git: ScriptedGit = .{ .fast_export = export_stream, .analyzed_submanifest_k = 3 };
    const analyzed = runFixtureShaped(tmp, "fixture.git", "analyzed", false, &analyzed_git, .real_filesystem, .{ .tree = null });
    try std.testing.expectEqual(cli.Exit.ok, analyzed.code);
    try std.testing.expect(std.mem.indexOf(u8, analyzed.err, "analyzed sub-manifest k=3") != null);

    var flat_dir = try tmp.dir.openDir(std.testing.io, "flat", .{});
    defer flat_dir.close(std.testing.io);
    var tree_dir = try tmp.dir.openDir(std.testing.io, "tree", .{});
    defer tree_dir.close(std.testing.io);

    const flat_state = try readImportedSubmanifestState(std.testing.io, flat_dir);
    const tree_state = try readImportedSubmanifestState(std.testing.io, tree_dir);

    // The tree half is asserted first because it is what gives the flat half
    // its meaning: this stream at this k really does produce sub-manifests, and
    // today's tree-mode config frame is pinned here as the contrast.
    try std.testing.expect(tree_state.submanifests_stored);
    try std.testing.expect(tree_state.enable_submanifests);
    try std.testing.expectEqual(@as(u8, 1), tree_state.submanifest_k);
    try std.testing.expect(tree_state.seen_submanifest);

    // The flat half: identical to a fresh `zit init` as far as sub-manifest
    // behavior goes. `submanifest_k = 0` is "not pinned", not "k of zero".
    try std.testing.expect(!flat_state.submanifests_stored);
    try std.testing.expect(!flat_state.enable_submanifests);
    try std.testing.expectEqual(@as(u8, 0), flat_state.submanifest_k);
    try std.testing.expect(!flat_state.seen_submanifest);
    try expectNoSCardsAnywhere(std.testing.io, flat_dir);

    // ... and it imported the same history: every path landed, as F cards.
    try std.testing.expectEqual(@as(usize, 96), flat_state.checkin_paths);
    try std.testing.expect(tree_state.checkin_paths < flat_state.checkin_paths);
}

// ---------------------------------------------------------------------------
// Second generation: every successful import publishes a PACKED store.
//
// The transaction runs `writing_raw -> raw_frozen -> packing -> packed_frozen
// -> verified -> published`. Generation 1 is written under its own name and is
// never the thing that ships: it is frozen, packed into generation 2 through
// the classifier's exact candidate-only drop set, read once more by
// verification, and then deleted from staging before the directory is synced
// and renamed. `DEST/.zit` is generation 2 and nothing else.
// ---------------------------------------------------------------------------

// The two generations must be distinct FILES, not one file rewritten. A single
// name would make "pack generation 1 into generation 2" a truncating in-place
// rewrite with no recoverable source, which is the shape this transaction
// exists to avoid.
test "import-git second generation uses a file distinct from the raw generation" {
    try std.testing.expect(!std.mem.eql(
        u8,
        import_cli.raw_generation_name,
        import_cli.published_generation_name,
    ));
    try std.testing.expectEqualStrings(".zit", import_cli.published_generation_name);
}

test "import-git second generation advances its publication stages one way only" {
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var bundle = try import_cli.PublicationBundle.create(std.testing.allocator, io, tmp.dir, "one-way");
    defer bundle.abort();

    // Skipping ahead is rejected...
    try std.testing.expectError(error.PublicationStageOutOfOrder, bundle.advance(.packing));
    try std.testing.expectEqual(import_cli.PublicationBundle.Stage.writing_raw, bundle.stage);
    // ...one step at a time is accepted...
    try bundle.advance(.raw_frozen);
    try bundle.advance(.packing);
    // ...repeating a stage is rejected...
    try std.testing.expectError(error.PublicationStageOutOfOrder, bundle.advance(.packing));
    // ...and so is going back.
    try std.testing.expectError(error.PublicationStageOutOfOrder, bundle.advance(.raw_frozen));
    try std.testing.expectEqual(import_cli.PublicationBundle.Stage.packing, bundle.stage);
}

test "import-git second generation publish rejects every state earlier than verified" {
    const io = std.testing.io;
    const early = [_]import_cli.PublicationBundle.Stage{
        .writing_raw,
        .raw_frozen,
        .packing,
        .packed_frozen,
    };
    for (early) |stage| {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var bundle = try import_cli.PublicationBundle.create(std.testing.allocator, io, tmp.dir, "too-early");
        defer bundle.abort();
        bundle.stage = stage;
        try std.testing.expectError(
            error.PublicationNotVerified,
            bundle.publish("too-early", .real_filesystem),
        );
        try std.testing.expect(!directoryExists(io, tmp.dir, "too-early"));
    }
}

test "import-git second generation publishes the packed generation and no raw generation" {
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "second-generation", false, .real_filesystem, &result_cap);
    try std.testing.expectEqual(cli.Exit.ok, result.code);

    var destination = try tmp.dir.openDir(io, "second-generation", .{});
    defer destination.close(io);
    try std.testing.expect(exists(io, destination, import_cli.published_generation_name));
    // The rename ships the whole staging directory, so a surviving generation 1
    // would land in DEST. It must be gone before the commit point.
    try std.testing.expect(!exists(io, destination, import_cli.raw_generation_name));
    // The default publication is the repository and its checkout record --
    // instrumentation leaves staging with the raw generation unless
    // --provenance kept it.
    try std.testing.expect(!exists(io, destination, ".zit-import-map.jsonl"));
    try std.testing.expect(!exists(io, destination, "refs"));

    // Stage ordering, observed rather than asserted from the source.
    try std.testing.expect(script.publication.raw_frozen_before_packing);
    try std.testing.expect(script.publication.packed_generation_created);
    try std.testing.expect(script.publication.packed_before_verify);
    try std.testing.expect(script.publication.both_generations_present_before_discard);
    try std.testing.expect(script.publication.raw_generation_discarded);
    try std.testing.expect(script.publication.discard_before_rename);
    try std.testing.expect(script.publication.verify_before_publish);

    // The published store is a complete log in its own right.
    var store_file = try destination.openFile(io, import_cli.published_generation_name, .{});
    defer store_file.close(io);
    const stat = try store_file.stat(io);
    var file_storage = storage.FileStorage.init(io, store_file, stat.size);
    var log = try logstore.LogStore.openReadOnly(std.testing.allocator, file_storage.storageFile());
    defer log.deinit(std.testing.allocator);
    try std.testing.expect(log.entryCount() != 0);
}

// The pack report is the only place an operator learns that publication is
// packed at all, and the only externally visible statement of how many mirror
// artifacts the classifier authorized dropping. A stream with no Fossil mirror
// pair must report zero -- fail-closed, stated out loud.
test "import-git second generation reports its pack census and drops nothing without a mirror pair" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var git: ScriptedGit = .{ .fast_export = valid_export };
    const result = runFixture(tmp, "fixture.git", "census", false, &git, .real_filesystem);
    try std.testing.expectEqual(cli.Exit.ok, result.code);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "import-git: packed ") != null);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "dropped 0 mirror artifacts") != null);
}

// The eleven packing-stage boundaries used to be swept again here, from a
// SIXTH `FailurePoint` array local to this test. Deleted rather than kept: all
// eleven are in `pre_rename_failure_points`, and that sweep asserts a strict
// superset of what this one did (`Exit.failure`, `launched == 1`, `reaped == 1`,
// `expectNoPublication`, and `!temporary_sibling_left`, against this test's
// failure/reaped/no-publication). A duplicate list is not extra coverage; it is
// a second registration site the partition gate above cannot see, which is the
// exact drift that gate exists to stop.

// A competitor that appears at the commit point must survive untouched even
// though the transaction now has two generations to unwind, and the staging
// sibling holding BOTH of them must still be removed.
test "import-git second generation never clobbers a competing destination and leaves no sibling" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{
        .preflight = .branch_head,
        .fast_export = valid_export,
        .competing_destination_at_publish = true,
    });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "packed-raced", false, .real_filesystem, &result_cap);
    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try std.testing.expect(script.competitor_destination_intact);
    try std.testing.expect(!script.temporary_sibling_left);
    // The competitor is a bare directory the script created: it must still be
    // bare, with neither generation written into it.
    var competitor = try tmp.dir.openDir(std.testing.io, "packed-raced", .{});
    defer competitor.close(std.testing.io);
    try std.testing.expect(!exists(std.testing.io, competitor, import_cli.published_generation_name));
    try std.testing.expect(!exists(std.testing.io, competitor, import_cli.raw_generation_name));
}

// The manifest check is what makes "only generation 2 is published" a property
// of the DIRECTORY at the instant it is renamed, rather than a property of the
// statement order that got it there. Stand in for a future edit that writes an
// intermediate file and forgets to remove it.
test "import-git second generation refuses to publish staging holding anything unmanifested" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{
        .preflight = .branch_head,
        .fast_export = valid_export,
        .stray_staging_file_after_discard = true,
    });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "stray", false, .real_filesystem, &result_cap);
    try std.testing.expectEqual(cli.Exit.failure, result.code);
    // The stray arrives AFTER the discard, so the transaction was otherwise
    // complete: only the manifest check stands between it and the rename.
    try std.testing.expect(script.publication.raw_generation_discarded);
    try expectNoPublication(std.testing.io, tmp, "stray");
}

// `.verified` is both the pre-discard and the post-discard stage, so the stage
// alone cannot say whether generation 1 left staging. The commit point checks
// the fact directly.
test "import-git second generation publish rejects a bundle still holding the raw generation" {
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var bundle = try import_cli.PublicationBundle.create(std.testing.allocator, io, tmp.dir, "raw-held");
    defer bundle.abort();
    bundle.stage = .verified;
    bundle.raw_present = true;
    try std.testing.expectError(
        error.PublicationRawGenerationPresent,
        bundle.publish("raw-held", .real_filesystem),
    );
    try std.testing.expect(!directoryExists(io, tmp.dir, "raw-held"));

    // The gate is the raw generation and nothing else: once it is gone, the
    // same bundle at the same stage publishes.
    bundle.raw_present = false;
    try bundle.publish("raw-held", .real_filesystem);
    try std.testing.expect(directoryExists(io, tmp.dir, "raw-held"));
}

// ===========================================================================
// Task 6 Step 1 -- the eleven corruption fixtures.
//
// RED BY CONSTRUCTION. The ordered verification pipeline of Step 2 does not
// exist: `verifyFrozen` today reopens both generations headers-only, reads the
// checkout, and opens the map without reading a byte of it. So every fixture
// below injects a real divergence between the frozen generations and what the
// import decided, and then asserts the ONE diagnostic that names its clause.
//
// Two assertions per fixture, and both are load-bearing:
//
//   1. `script.verifier_fault_injected` -- the corruption actually happened.
//      Without it a fixture whose injection silently no-ops would go green the
//      moment the pipeline lands, which is the failure mode this suite exists
//      to prevent. A fixture that passes for the wrong reason is worse than a
//      missing fixture.
//   2. the clause's own diagnostic in stderr -- not merely `.failure`. A
//      verifier that refuses generically is nearly as bad as none: the
//      operator cannot tell a dropped artifact from a tampered provenance
//      line, and the whole point of Task 6 is that Tasks 3-5 DELETE artifacts
//      from a repository and publish the result.
//
// The diagnostic tokens below are the contract Step 2 implements. They are
// deliberately clause names, not error names: `verifier:` plus the invariant.
// ===========================================================================

const verifier_clause_prefix = "import-git: verifier: ";

/// Two files, distinct author and committer, a two-parent merge, a lightweight
/// tag and an annotated tag. One stream so that ten of the eleven fixtures
/// differ only in what they corrupt, never in what they imported.
const verifier_export =
    "feature done\n" ++
    "blob\n" ++
    "mark :1\n" ++
    "data 5\n" ++
    "alpha\n" ++
    "blob\n" ++
    "mark :2\n" ++
    "data 4\n" ++
    "beta\n" ++
    "commit refs/heads/main\n" ++
    "mark :3\n" ++
    "original-oid 1111111111111111111111111111111111111111\n" ++
    "author Author One <[email protected]> 1700000000 +0000\n" ++
    "committer Committer Two <[email protected]> 1700000100 +0000\n" ++
    "data 4\n" ++
    "root\n" ++
    "M 100644 :1 a.txt\n" ++
    "M 100644 :2 b.txt\n" ++
    "\n" ++
    "blob\n" ++
    "mark :4\n" ++
    "data 5\n" ++
    "gamma\n" ++
    "commit refs/heads/side\n" ++
    "mark :5\n" ++
    "original-oid 2222222222222222222222222222222222222222\n" ++
    "author Author One <[email protected]> 1700000200 +0000\n" ++
    "committer Committer Two <[email protected]> 1700000300 +0000\n" ++
    "data 4\n" ++
    "side\n" ++
    "from :3\n" ++
    "M 100644 :4 b.txt\n" ++
    "\n" ++
    "commit refs/heads/main\n" ++
    "mark :6\n" ++
    "original-oid 3333333333333333333333333333333333333333\n" ++
    "author Author One <[email protected]> 1700000400 +0000\n" ++
    "committer Committer Two <[email protected]> 1700000500 +0000\n" ++
    "data 5\n" ++
    "merge\n" ++
    "from :3\n" ++
    "merge :5\n" ++
    "\n" ++
    "reset refs/tags/light\n" ++
    "from :3\n" ++
    "\n" ++
    "tag v1\n" ++
    "mark :7\n" ++
    "from :6\n" ++
    "tagger Author One <[email protected]> 1700000600 +0000\n" ++
    "data 0\n" ++
    "\n" ++
    "done\n";

fn appendFmt(out: *std.ArrayList(u8), gpa: std.mem.Allocator, comptime fmt: []const u8, args: anytype) !void {
    const rendered = try std.fmt.allocPrint(gpa, fmt, args);
    defer gpa.free(rendered);
    try out.appendSlice(gpa, rendered);
}

/// A stream carrying a VALID Fossil mirror pair, so that the drop set is not
/// empty and `retained_omitted_artifact` has a referent.
///
/// Built at runtime because two of the four blobs are hashes of the other two:
/// `manifest.uuid` must name the sterilized `manifest` bytes, and those bytes
/// must carry an F card naming the tree file. A string literal cannot close
/// that loop. Gates 1-8 of `classifyMirror` are what this satisfies -- one
/// regular-mode candidate set, a canonical uuid over the sterilized manifest,
/// a flat check-in whose file set is the candidate-free tree, no parents on a
/// root commit, a D card at the committer instant, and a well-formed tags file.
fn buildMirrorExport(gpa: std.mem.Allocator) ![]u8 {
    const sterilizer = "# Remove this line to create a well-formed Fossil manifest.\n";
    const tree_content = "alpha\n";
    const tags_content = "branch trunk\ntag trunk\n";
    // 1700000000 as a Fossil D card; the export's committer instant must be
    // the same second or gate 7 retains the pair.
    const instant = "2023-11-14T22:13:20";

    var name_buf: [64]u8 = undefined;
    const tree_hash = artifact.artifactName(.sha1, tree_content, &name_buf);

    var manifest: artifact.Manifest = .{
        .arena_state = std.heap.ArenaAllocator.init(gpa),
        .kind = .manifest,
        .a = null,
        .b = null,
        .c = .{ .comment = "mirror" },
        .d = .{ .timestamp = instant },
        .e = null,
        .g = null,
        .h = null,
        .i = null,
        .k = null,
        .l = null,
        .n = null,
        .r = null,
        .u = .{ .login = "mirror" },
        .z = null,
        .w = null,
        .f = &.{},
        .j = &.{},
        .m = &.{},
        .p = &.{},
        .q = &.{},
        .s = &.{},
        .t = &.{},
    };
    defer manifest.deinit();
    var cards = [_]artifact.FCard{.{
        .filename = "a.txt",
        .hash = try manifest.arena_state.allocator().dupe(u8, tree_hash),
        .perm = null,
        .old_name = null,
        .mode = .regular,
    }};
    manifest.f = &cards;

    const manifest_bytes = try artifact.create(&manifest, gpa);
    defer gpa.free(manifest_bytes);
    var uuid_buf: [64]u8 = undefined;
    const uuid = artifact.artifactName(.sha1, manifest_bytes, &uuid_buf);

    var out: std.ArrayList(u8) = .empty;
    errdefer out.deinit(gpa);
    try appendFmt(&out, gpa, "feature done\nblob\nmark :1\ndata {d}\n{s}\n", .{ tree_content.len, tree_content });
    try appendFmt(&out, gpa, "blob\nmark :2\ndata {d}\n{s}{s}\n", .{ manifest_bytes.len + sterilizer.len, manifest_bytes, sterilizer });
    try appendFmt(&out, gpa, "blob\nmark :3\ndata {d}\n{s}\n\n", .{ uuid.len + 1, uuid });
    try appendFmt(&out, gpa, "blob\nmark :4\ndata {d}\n{s}\n", .{ tags_content.len, tags_content });
    try out.appendSlice(
        gpa,
        "commit refs/heads/main\n" ++
            "mark :5\n" ++
            "original-oid 4444444444444444444444444444444444444444\n" ++
            "author Mirror <[email protected]> 1700000000 +0000\n" ++
            "committer Mirror <[email protected]> 1700000000 +0000\n" ++
            "data 6\nmirror\n" ++
            "M 100644 :1 a.txt\n" ++
            "M 100644 :2 manifest\n" ++
            "M 100644 :3 manifest.uuid\n" ++
            "M 100644 :4 manifest.tags\n" ++
            "\ndone\n",
    );
    return out.toOwnedSlice(gpa);
}

/// One corruption fixture. Runs the whole transaction with `fault` injected at
/// `.verify_raw_store`, then requires that the named clause -- and only the
/// named clause -- is what refused the rename.
fn expectVerifierClause(
    destination: []const u8,
    source_export: []const u8,
    fault: import_cli.CommandScript.VerifierFault,
    clause: []const u8,
) !void {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{
        .preflight = .branch_head,
        .fast_export = source_export,
        .verifier_fault = fault,
    });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, destination, false, .real_filesystem, &result_cap);

    // The corruption happened. Asserted first and separately, so a fixture
    // whose injection no-ops reports THAT and never masquerades as a verifier
    // result.
    if (!script.verifier_fault_injected) {
        std.debug.print(
            "\n  [{s}] INJECTION DID NOT HAPPEN -- the fixture corrupted nothing, so it says nothing about `{s}`.\n",
            .{ destination, clause },
        );
        return error.VerifierFaultNotInjected;
    }
    // It refused for this reason. Checked before the exit code, because
    // "refused generically" and "refused for the right reason" are different
    // results and only the second one proves a clause exists.
    if (std.mem.indexOf(u8, result.err, clause) == null) {
        std.debug.print(
            "\n  [{s}] MISSING CLAUSE `{s}`\n    exit: {s}\n    published: {}\n    stderr: {s}\n",
            .{ destination, clause, @tagName(result.code), directoryExists(std.testing.io, tmp.dir, destination), result.err },
        );
        return error.VerifierClauseMissing;
    }
    // And ONLY that clause. The pipeline returns at its first refusal, so a
    // second `verifier:` line would mean the fixture tripped something else on
    // the way and this assertion is what keeps "the clause appears" from
    // drifting into "the clause appears somewhere among several".
    var scan: usize = 0;
    var lines: usize = 0;
    while (std.mem.indexOfPos(u8, result.err, scan, verifier_clause_prefix)) |at| {
        lines += 1;
        scan = at + verifier_clause_prefix.len;
    }
    if (lines != 1) {
        std.debug.print(
            "\n  [{s}] EXPECTED EXACTLY ONE VERIFIER CLAUSE, SAW {d}\n    stderr: {s}\n",
            .{ destination, lines, result.err },
        );
        return error.VerifierClauseAmbiguous;
    }
    // The verifier refused, and DEST was never created.
    try std.testing.expectEqual(cli.Exit.failure, result.code);
    try expectNoPublication(std.testing.io, tmp, destination);
}

test "import-git exhaustive verifier rejects an omitted artifact retained in the packed generation" {
    const mirror_export = try buildMirrorExport(std.testing.allocator);
    defer std.testing.allocator.free(mirror_export);
    // Guard the fixture's own premise before asserting anything about the
    // verifier: with an empty drop set there is nothing to retain, and the
    // corruption would be unexpressible rather than undetected.
    {
        var tmp = std.testing.tmpDir(.{});
        defer tmp.cleanup();
        var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = mirror_export });
        var census_cap: import_cli.ScriptCapture = .{};
        const census = scripted(tmp, &script, "mirror-census", false, .real_filesystem, &census_cap);
        try std.testing.expectEqual(cli.Exit.ok, census.code);
        try std.testing.expect(std.mem.indexOf(u8, census.err, "dropped 0 mirror artifacts") == null);
    }
    try expectVerifierClause(
        "verifier-retained-omitted",
        mirror_export,
        .retained_omitted_artifact,
        verifier_clause_prefix ++ "retained-omitted-artifact",
    );
}

test "import-git exhaustive verifier rejects a required artifact dropped from the packed generation" {
    try expectVerifierClause(
        "verifier-dropped-required",
        verifier_export,
        .dropped_required_artifact,
        verifier_clause_prefix ++ "dropped-required-artifact",
    );
}

test "import-git exhaustive verifier rejects an artifact added to the packed generation" {
    try expectVerifierClause(
        "verifier-added",
        verifier_export,
        .added_artifact,
        verifier_clause_prefix ++ "added-artifact",
    );
}

test "import-git exhaustive verifier rejects changed artifact bytes under an unchanged name" {
    try expectVerifierClause(
        "verifier-changed-bytes",
        verifier_export,
        .changed_artifact_bytes,
        verifier_clause_prefix ++ "artifact-rehash-mismatch",
    );
}

test "import-git exhaustive verifier rejects a delta frame recorded against the wrong base" {
    try expectVerifierClause(
        "verifier-wrong-delta-base",
        verifier_export,
        .wrong_delta_base,
        verifier_clause_prefix ++ "delta-base-mismatch",
    );
}

test "import-git exhaustive verifier rejects an artifact-set-preserving tree change" {
    try expectVerifierClause(
        "verifier-tree-change",
        verifier_export,
        .tree_change_preserving_artifacts,
        verifier_clause_prefix ++ "normalized-tree-digest-mismatch",
    );
}

test "import-git exhaustive verifier rejects reordered check-in parents" {
    try expectVerifierClause(
        "verifier-parent-reorder",
        verifier_export,
        .parent_reorder,
        verifier_clause_prefix ++ "parent-order-mismatch",
    );
}

test "import-git exhaustive verifier rejects a retargeted branch ref" {
    try expectVerifierClause(
        "verifier-ref-retarget",
        verifier_export,
        .ref_retarget,
        verifier_clause_prefix ++ "ref-target-mismatch",
    );
}

test "import-git exhaustive verifier rejects a changed tag target or kind" {
    try expectVerifierClause(
        "verifier-tag-change",
        verifier_export,
        .tag_change,
        verifier_clause_prefix ++ "tag-mismatch",
    );
}

test "import-git exhaustive verifier rejects changed attribution in the finalized map" {
    try expectVerifierClause(
        "verifier-attribution",
        verifier_export,
        .attribution_change,
        verifier_clause_prefix ++ "attribution-mismatch",
    );
}

test "import-git exhaustive verifier rejects a tampered provenance line" {
    try expectVerifierClause(
        "verifier-provenance",
        verifier_export,
        .provenance_tamper,
        verifier_clause_prefix ++ "provenance-digest-mismatch",
    );
}

// The negative control. Same stream, no injection: the transaction must reach
// `verified` and publish, so no fixture above can be passing merely because
// this export is unimportable.
test "import-git exhaustive verifier admits the uncorrupted fixture" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{
        .preflight = .branch_head,
        .fast_export = verifier_export,
    });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "verifier-clean", false, .real_filesystem, &result_cap);
    try std.testing.expectEqual(cli.Exit.ok, result.code);
    try std.testing.expect(!script.verifier_fault_injected);
    try std.testing.expect(script.publication.verify_before_publish);
    try std.testing.expect(std.mem.indexOf(u8, result.err, "import-git: verifier: ") == null);
    try std.testing.expect(directoryExists(std.testing.io, tmp.dir, "verifier-clean"));
}

// The fixpoint, proved OUTSIDE the transaction and deliberately so.
//
// Design item 6 makes the B-to-C proof a qualification layer, not a runtime
// one: the production import creates generation 1 and generation 2 and stops,
// because a third generation is an extra full pack of the whole repository on
// every import to re-establish a property that does not change between runs.
// This is where that property is established instead -- on a real published
// import, over a stream carrying a validated mirror pair, so the pack whose
// determinism is under test is one that actually dropped something.
test "import-git pack is idempotent: repacking a published generation is byte-identical" {
    const io = std.testing.io;
    const gpa = std.testing.allocator;
    const mirror_export = try buildMirrorExport(gpa);
    defer gpa.free(mirror_export);

    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = mirror_export });
    var result_cap: import_cli.ScriptCapture = .{};
    const result = scripted(tmp, &script, "fixpoint", false, .real_filesystem, &result_cap);
    try std.testing.expectEqual(cli.Exit.ok, result.code);
    // The premise: generation 2 is a pack that removed something. A fixpoint
    // over a pack that dropped nothing would prove far less.
    try std.testing.expect(std.mem.indexOf(u8, result.err, "dropped 0 mirror artifacts") == null);

    var published = try tmp.dir.openDir(io, "fixpoint", .{});
    defer published.close(io);
    var b_file = try published.openFile(io, import_cli.published_generation_name, .{ .mode = .read_only });
    defer b_file.close(io);
    var b_storage = storage.FileStorage.init(io, b_file, (try b_file.stat(io)).size);
    var b = try logstore.LogStore.openReadOnly(gpa, b_storage.storageFile());
    defer b.deinit(gpa);

    // C, disposable, in the test's own directory: nothing about this generation
    // is ever published, and the import that produced B never created it.
    var c_file = try tmp.dir.createFile(io, "generation-c", .{ .read = true, .exclusive = true });
    defer c_file.close(io);
    var c_storage = storage.FileStorage.init(io, c_file, 0);
    var c = try logstore.LogStore.open(gpa, c_storage.storageFile(), b.hash_kind);
    defer c.deinit(gpa);
    // No filter and no extra shuns: everything B holds is content the import
    // already proved it should keep, so C differs from B only if packing is
    // not deterministic over its own output.
    const stats = try pack.packLog(gpa, &b, &c, pack.Filter.keep_all, &.{});
    try std.testing.expectEqual(@as(usize, 0), stats.dropped);

    const b_bytes = try published.readFileAlloc(io, import_cli.published_generation_name, gpa, .limited(1 << 24));
    defer gpa.free(b_bytes);
    const c_bytes = try tmp.dir.readFileAlloc(io, "generation-c", gpa, .limited(1 << 24));
    defer gpa.free(c_bytes);
    try std.testing.expectEqualSlices(u8, b_bytes, c_bytes);
}