Artifact 44c962d7310a86075a7e2faa73813a5b1e3c8d4a7dac1c71980cc2f4a1cd5d63:
- File
src/git_import_cli_test.zig
— part of check-in
[ce3c87e25c]
at
2026-08-10 23:48:29
on branch sub-manifest
— Move the git-interop implementation into src/git/
Seven files relocate with unchanged basenames: git_fast_export(+test), git_import, git_import_impl, git_import_cli, git_import_verify, git_import_benchmark_support. The six standalone module roots (test, refs_test, cli_test, e2e, benchmark, pipe_helper) stay flat because a Zig module cannot import above its root file's directory — the same rule that keeps main/e2e/dst/smoke at src/. Future export-git files follow the same split: roots flat, implementation in src/git/. Path-string edits only; build.zig unchanged. (user: dmestas, size: 115648) [annotate] [blame] [check-ins using]
//! 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("cli.zig"); const fast_export = @import("zit").git_fast_export; const import_cli = @import("git/git_import_cli.zig"); const checkout = @import("zit").checkout; 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, .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); var entries = destination.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")); var entries = tmp.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, ) import_cli.ScriptResult { return import_cli.runScripted( std.testing.allocator, std.testing.io, .{ .source = "fixture.git", .destination_parent = tmp.dir, .destination_name = destination, .submanifest = .{ .tree = 5 }, .attribution = .committer, .default_branch = null, .quiet = quiet, }, script, fault, ); } 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, &.{ helper_options.git_import_pipe_helper, mode }, ); 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 code = import_cli.run( std.testing.allocator, io, .{ .source = "fixture.git", .destination_parent = tmp.dir, .destination_name = 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); defer session.deinit(); return import_cli.run( std.testing.allocator, std.testing.io, .{ .source = source, .destination_parent = tmp.dir, .destination_name = 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 }); const result = scripted(tmp, &script, "rejected", false, .real_filesystem); 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 }); const rejected = scripted(tmp, &script, "detached", false, .real_filesystem); 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"; const accepted = scripted(tmp, &script, "detached-with-branch", false, .real_filesystem); 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 }); const result = scripted(tmp, &script, "argv", false, .real_filesystem); 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); try std.testing.expect(result.err.len < 4096); 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 }); const result = scripted(tmp, &script, "verified", false, .real_filesystem); 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, }); const result = scripted(tmp, &script, "raced", false, .real_filesystem); 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, }); const result = scripted(tmp, &script, "raced-staging", false, .real_filesystem); 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, }); const result = scripted(tmp, &script, "at-limit", false, .real_filesystem); 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}); const result = scripted(tmp, &script, destination_name, false, .real_filesystem); 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, }, }); const result = scripted(tmp, &script, "command-ceiling", false, .real_filesystem); 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, }, }); const result = scripted(tmp, &script, "payload-ceiling", false, .real_filesystem); 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 }); const result = scripted(tmp, &script, "boundary-failed", false, .{ .fail_at = failure }); 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 }); const result = scripted(tmp, &script, "post-rename", false, .{ .fail_at = post_rename_failure_point }); 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, }); const result = scripted(tmp, &script, "cancelled", false, .real_filesystem); 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, }); const result = scripted(tmp, &script, "child-failed", false, .real_filesystem); 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 }); const result = scripted(tmp, &script, "observed", false, recorder.hooks()); 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 }); const result = scripted(tmp, &script, "durability-uncertain", false, .{ .fail_at = parent_sync_failure_point }); 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, &.{ 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, }); const result = scripted(tmp, &script, "replacement-after-rename", false, .real_filesystem); 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 }); const result = scripted(tmp, &script, "second-generation", false, .real_filesystem); 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, }); const result = scripted(tmp, &script, "packed-raced", false, .real_filesystem); 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, }); const result = scripted(tmp, &script, "stray", false, .real_filesystem); 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, }); const result = scripted(tmp, &script, destination, false, .real_filesystem); // 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 }); const census = scripted(tmp, &script, "mirror-census", false, .real_filesystem); 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, }); const result = scripted(tmp, &script, "verifier-clean", false, .real_filesystem); 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 }); const result = scripted(tmp, &script, "fixpoint", false, .real_filesystem); 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); }