Artifact 723ad4594b77f416d066002250d07e0bbe362c6187d2678a1c13ce74050e288c:
- File
src/git/git_import_cli_test.zig
— part of check-in
[add51a12d2]
at
2026-09-28 02:59:11
on branch trunk
— import-git: record a project code; verify notes a store without one
import-git wrote no project code, so every sync peer and forge refused the store it made. It now derives one from the destination's absolute path, as init does, and the import request requires it. verify notes a repository with no project code without counting it as damage. (user: dmestas, size: 118392) [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("zit").cli; const fast_export = @import("zit").git_fast_export; const import_cli = @import("git_import_cli.zig"); const checkout = @import("zit").checkout; const sync = @import("zit").sync; const artifact = @import("zit").artifact; const git_import = @import("zit").git_import; const logstore = @import("zit").logstore; const storage = @import("zit").storage; const pack = @import("zit").pack; const helper_options = @import("git_import_cli_options"); const valid_export = "feature done\n" ++ "blob\n" ++ "mark :1\n" ++ "data 5\n" ++ "hello\n" ++ "commit refs/heads/main\n" ++ "mark :2\n" ++ "author A <[email protected]> 0 +0000\n" ++ "committer A <[email protected]> 0 +0000\n" ++ "data 4\n" ++ "init\n" ++ "M 100644 :1 README\n" ++ "\n" ++ "done\n"; const valid_export_with_remote_ref = "feature done\n" ++ "blob\n" ++ "mark :1\n" ++ "data 5\n" ++ "hello\n" ++ "commit refs/heads/main\n" ++ "mark :2\n" ++ "author A <[email protected]> 0 +0000\n" ++ "committer A <[email protected]> 0 +0000\n" ++ "data 4\n" ++ "init\n" ++ "M 100644 :1 README\n" ++ "\n" ++ "reset refs/remotes/origin/main\n" ++ "from :2\n" ++ "\n" ++ "done\n"; const valid_export_with_oids = "feature done\n" ++ "blob\n" ++ "mark :1\n" ++ "original-oid 100b93820ade4c16225673b4ca62bb3ade63c313\n" ++ "data 5\n" ++ "hello\n" ++ "commit refs/heads/main\n" ++ "mark :2\n" ++ "original-oid ed9b38a10070b504cecfacd892507af78fcb24a4\n" ++ "author A <[email protected]> 0 +0000\n" ++ "committer A <[email protected]> 0 +0000\n" ++ "data 4\n" ++ "init\n" ++ "M 100644 :1 README\n" ++ "\n" ++ "done\n"; const malformed_export = "feature done\n" ++ "commit refs/heads/main\n" ++ "mark :not-a-number\n"; const marked_tag_export = "feature done\n" ++ "blob\n" ++ "mark :1\n" ++ "data 5\n" ++ "hello\n" ++ "commit refs/heads/main\n" ++ "mark :2\n" ++ "author A <[email protected]> 0 +0000\n" ++ "committer A <[email protected]> 0 +0000\n" ++ "data 4\n" ++ "init\n" ++ "M 100644 :1 README.md\n" ++ "\n" ++ "tag v1\n" ++ "mark :3\n" ++ "from :2\n" ++ "tagger A <[email protected]> 0 +0000\n" ++ "data 0\n" ++ "\n" ++ "done\n"; const Result = struct { code: cli.Exit, out: []const u8, err: []const u8, lifecycle: import_cli.ChildLifecycle, }; /// The wished test adapter. Task 10's production implementation owns no fake /// process: it consumes this narrow interface, while the CLI passes the /// OS-backed implementation and this file passes a scripted one. const LifecycleFault = enum { terminate, drain_stderr, reap }; const ScriptedGit = struct { fast_export: []const u8, export_exit: u8 = 0, stderr: []const u8 = "", launched: usize = 0, terminated: usize = 0, stderr_drained: usize = 0, reaped: usize = 0, lifecycle_fault: ?LifecycleFault = null, limits: import_cli.ImportLimits = .{}, /// What preflight's §6.3 analysis would have returned. Null is "the /// analysis produced nothing", which is what every test that does not care /// about the analyzed path wants. analyzed_submanifest_k: ?u4 = null, fn dependencies(self: *ScriptedGit, publication: import_cli.PublicationHooks) import_cli.Dependencies { return .{ .git = .{ .context = self, .vtable = &.{ .start_export = startExport, .terminate = terminate, .drain_stderr = drainStderr, .reap = reap, }, }, .publication = publication, .limits = self.limits, }; } fn startExport(context: *anyopaque, _: []const u8) !import_cli.ExportPipes { const self: *ScriptedGit = @ptrCast(@alignCast(context)); self.launched += 1; return .{ .stdout = .fixed(self.fast_export), .stderr = .fixed(self.stderr), .exit_code = self.export_exit, .analyzed_submanifest_k = self.analyzed_submanifest_k, }; } fn terminate(context: *anyopaque) !void { const self: *ScriptedGit = @ptrCast(@alignCast(context)); self.terminated += 1; if (self.lifecycle_fault == .terminate) return error.InjectedLifecycleFailure; } fn drainStderr(context: *anyopaque) !void { const self: *ScriptedGit = @ptrCast(@alignCast(context)); self.stderr_drained += 1; if (self.lifecycle_fault == .drain_stderr) return error.InjectedLifecycleFailure; } fn reap(context: *anyopaque) !void { const self: *ScriptedGit = @ptrCast(@alignCast(context)); self.reaped += 1; if (self.lifecycle_fault == .reap) return error.InjectedLifecycleFailure; } }; fn runFixture( tmp: std.testing.TmpDir, source: []const u8, destination: []const u8, quiet: bool, git: *ScriptedGit, publication: import_cli.PublicationHooks, ) Result { return runFixtureShaped(tmp, source, destination, quiet, git, publication, .{ .tree = 5 }); } /// `runFixture` with the one request field these tests vary. Everything else -- /// source, dependencies, streams, attribution, default branch -- is held /// identical on purpose: the whole point of the flag is that flat and tree /// imports differ in tree emission and in nothing else. fn runFixtureShaped( tmp: std.testing.TmpDir, source: []const u8, destination: []const u8, quiet: bool, git: *ScriptedGit, publication: import_cli.PublicationHooks, submanifest: git_import.SubmanifestChoice, ) Result { return runFixtureFull(tmp, source, destination, quiet, git, publication, submanifest, false); } /// The whole request surface these tests reach. `provenance` mirrors the /// production default (off): a test that reads the published map or refs opts /// in exactly as an operator would. fn runFixtureFull( tmp: std.testing.TmpDir, source: []const u8, destination: []const u8, quiet: bool, git: *ScriptedGit, publication: import_cli.PublicationHooks, submanifest: git_import.SubmanifestChoice, provenance: bool, ) Result { const Capture = struct { var out_storage: [4096]u8 = undefined; var err_storage: [4096]u8 = undefined; }; var out = std.Io.Writer.fixed(&Capture.out_storage); var err = std.Io.Writer.fixed(&Capture.err_storage); const streams: cli.Streams = .{ .out = &out, .err = &err }; const code = import_cli.run( std.testing.allocator, std.testing.io, .{ .source = source, .destination_parent = tmp.dir, .destination_name = destination, .project_code = sync.deriveProjectCode(&.{destination}), .submanifest = submanifest, .attribution = .committer, .default_branch = null, .quiet = quiet, .provenance = provenance, }, streams, git.dependencies(publication), ); return .{ .code = code, .out = out.buffered(), .err = err.buffered(), .lifecycle = .{ .launched = git.launched, .terminated = git.terminated, .stderr_drained = git.stderr_drained, .reaped = git.reaped, }, }; } fn retainedBytesForExport(bytes: []const u8) !u64 { var reader = fast_export.Reader.init(std.testing.allocator, .fixed(bytes), .{ .max_line = 1024 * 1024, .max_data = 1024 * 1024 * 1024, .max_path = 4096, .max_events = std.math.maxInt(u64), }); defer reader.deinit(); var retained_bytes: u64 = 0; while (try reader.next()) |lease| { const charge = blk: { var event = lease; defer event.deinit(); break :blk try import_cli.retainedByteCharge(event.value); }; retained_bytes = try std.math.add(u64, retained_bytes, charge); } return retained_bytes; } /// How far a fast-export stream gets before it stops, and how it stopped. const ExportReach = struct { events: usize, rejected: bool }; /// Replays `bytes` through the same reader the import uses. /// /// This is the discriminator the two export-fault tests are named for, and /// `create_store` cannot supply it: that boundary is checked BEFORE /// `createRawGeneration`, so a stream rejected at record zero and a stream /// truncated after a blob observe it identically. The count of records this /// reader accepts is what separates them -- at least one accepted record means /// the failure lands with generation 1 open and already written, which is the /// window those tests claim and the harder one to unwind. fn exportReach(bytes: []const u8) !ExportReach { var reader = fast_export.Reader.init(std.testing.allocator, .fixed(bytes), .{ .max_line = 1024 * 1024, .max_data = 1024 * 1024 * 1024, .max_path = 4096, .max_events = std.math.maxInt(u64), }); defer reader.deinit(); var events: usize = 0; while (true) { const next = reader.next() catch return .{ .events = events, .rejected = true }; var lease = next orelse return .{ .events = events, .rejected = false }; lease.deinit(); events += 1; } } fn exists(io: std.Io, dir: std.Io.Dir, path: []const u8) bool { var file = dir.openFile(io, path, .{}) catch return false; file.close(io); return true; } fn directoryExists(io: std.Io, dir: std.Io.Dir, path: []const u8) bool { var child = dir.openDir(io, path, .{}) catch return false; child.close(io); return true; } /// Records which publication boundaries a run actually reached. /// /// This answers HOW FAR the transaction got, and nothing else. It cannot answer /// "was generation 1 open", because `create_store` is checked BEFORE /// `createRawGeneration` and is the only boundary in the whole prologue: every /// failure that reaches `importInto` at all observes it, including one that /// never opened a store. What it does prove is the negative half -- that the /// fault unwound from the export and not from somewhere downstream with a /// different abort path. /// /// Works through either harness: `runFixture` passes hooks through untouched, /// and `CommandScript.dependencies` chains a caller's observer behind its own /// rather than replacing it. const PointRecorder = struct { seen: std.EnumSet(FailurePoint) = .initEmpty(), fn notify(context: *anyopaque, point: FailurePoint) void { const self: *PointRecorder = @ptrCast(@alignCast(context)); self.seen.insert(point); } /// No `fail_at`: the fault under test is upstream of publication, and this /// only watches the boundaries the transaction walks on its way out. fn hooks(self: *PointRecorder) import_cli.PublicationHooks { return .{ .observer = .{ .context = self, .notify = notify } }; } }; /// A published destination holds the publish manifest and nothing else. /// /// The expected set is BORROWED from production rather than retyped: this is /// the same list `assertPublishManifest` checks staging against before the /// rename, and a copy here could agree with a stale idea of what a published /// import contains. fn expectPublishManifestExactly(io: std.Io, destination: std.Io.Dir) !void { const manifest = import_cli.PublicationBundle.publish_manifest; var seen: [manifest.len]bool = @splat(false); // Reopened for iteration: the caller's handle may lack iterate rights, // which Linux enforces at readdir time (macOS does not). var it_dir = try destination.openDir(io, ".", .{ .iterate = true }); defer it_dir.close(io); var entries = it_dir.iterate(); while (try entries.next(io)) |entry| { const index = for (manifest, 0..) |name, position| { if (std.mem.eql(u8, entry.name, name)) break position; } else { std.debug.print("unexpected published entry: {s}\n", .{entry.name}); return error.UnexpectedPublishedEntry; }; if (seen[index]) return error.DuplicatePublishedEntry; seen[index] = true; } for (seen, manifest) |present, name| { if (!present) { std.debug.print("missing published entry: {s}\n", .{name}); return error.MissingPublishedEntry; } } } fn expectNoPublication(io: std.Io, tmp: std.testing.TmpDir, destination: []const u8) !void { try std.testing.expect(!directoryExists(io, tmp.dir, destination)); try std.testing.expect(!exists(io, tmp.dir, ".zit-import-map.jsonl")); // Reopened for iteration: the caller's handle may lack iterate rights, // which Linux enforces at readdir time (macOS does not). var it_dir = try tmp.dir.openDir(io, ".", .{ .iterate = true }); defer it_dir.close(io); var entries = it_dir.iterate(); while (try entries.next(io)) |entry| { try std.testing.expect(!std.mem.startsWith(u8, entry.name, ".zit-import-tmp-")); } } test "import-git --provenance publishes .zit refs and map together only after a complete export" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var git: ScriptedGit = .{ .fast_export = valid_export, .stderr = "Enumerating objects\n" }; const result = runFixtureFull( tmp, "fixture.git", "imported", false, &git, .real_filesystem, .{ .tree = 5 }, true, ); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expectEqualStrings("imported\n", result.out); try std.testing.expect(std.mem.indexOf(u8, result.err, "import-git:") != null); var destination = try tmp.dir.openDir(std.testing.io, "imported", .{}); defer destination.close(std.testing.io); try std.testing.expect(exists(std.testing.io, destination, ".zit")); try std.testing.expect(exists(std.testing.io, destination, "refs")); try std.testing.expect(exists(std.testing.io, destination, ".zit-import-map.jsonl")); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const imported_checkout = (try checkout.read(std.testing.io, destination, &checkout_buf)).?; try std.testing.expectEqualStrings("", imported_checkout.version); // The checkout records `trunk`, not the source's `main`: an import adopts // this format's name for the main line, which is what every downstream // consumer that must name it without being told already looks for. try std.testing.expectEqualStrings("trunk", imported_checkout.branch); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched); try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); } test "import-git finalizes remote refs emitted with all refs" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var git: ScriptedGit = .{ .fast_export = valid_export_with_remote_ref }; const result = runFixtureFull(tmp, "fixture.git", "with-remote", false, &git, .real_filesystem, .{ .tree = 5 }, true); try std.testing.expectEqual(cli.Exit.ok, result.code); var destination = try tmp.dir.openDir(std.testing.io, "with-remote", .{}); defer destination.close(std.testing.io); // One sorted packed file, not a directory tree. Reading the bytes rather // than probing for paths is the point: the file's whole job is to be // legible, and its ordering is what makes two imports comparable. const packed_refs = try destination.readFileAlloc(std.testing.io, "refs", std.testing.allocator, .limited(4096)); defer std.testing.allocator.free(packed_refs); try std.testing.expectEqualStrings( import_cli.packed_refs_header ++ "sym-heads/main 104f740458ab03713ed37f67639379c91e4ab3c8992986a2a3483f90f8eb7b19\n" ++ "sym-refs/remotes/origin/main 104f740458ab03713ed37f67639379c91e4ab3c8992986a2a3483f90f8eb7b19\n", packed_refs, ); } test "import-git refuses an existing destination before Git starts" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDir(std.testing.io, "already-there", .default_dir); var git: ScriptedGit = .{ .fast_export = valid_export }; const result = runFixture(tmp, "fixture.git", "already-there", false, &git, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqualStrings("", result.out); try std.testing.expect(std.mem.indexOf(u8, result.err, "already exists") != null); try std.testing.expectEqual(@as(usize, 0), result.lifecycle.launched); try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 0), result.lifecycle.stderr_drained); try std.testing.expectEqual(@as(usize, 0), result.lifecycle.reaped); } test "import-git aborts malformed export by terminating draining and reaping once" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var git: ScriptedGit = .{ .fast_export = malformed_export, .stderr = "fatal fixture diagnostic\n" }; const result = runFixture(tmp, "fixture.git", "broken", false, &git, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqualStrings("", result.out); try std.testing.expect(std.mem.indexOf(u8, result.err, "fatal fixture diagnostic") != null); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, "broken"); } const FailurePoint = import_cli.PublicationHooks.FailurePoint; // The publication boundaries this file drives, partitioned by the answer each // one is owed, and hoisted to file scope so the partition itself can be // asserted rather than believed. // // A `FailurePoint` that appears in no class is a fault arm no test ever runs, // and nothing about the tag says so: it compiles, it is selectable, and a // caller who asks for it gets a clean green because `check` never fires it. The // alias map already produced that shape once -- five of the eight legacy // selectors were inert and passing. These five names are the only places a new // boundary can be registered. // // What the gate below proves is CLASSIFICATION, not execution: a new // `FailurePoint` that nobody puts in a class fails it, which is the drift that // actually happens. It does not prove the three singletons still have tests -- // deleting one leaves its `const` classified and the gate green. Making // execution observable would need runtime registration, and that is // order-dependent and breaks under `--test-filter`, so the cure is worse than // the disease. Deleting a whole test is a visible line in a diff; adding an // enum member is not. const create_failure_point: FailurePoint = .create_store; const post_rename_failure_point: FailurePoint = .after_rename_no_replace; const parent_sync_failure_point: FailurePoint = .sync_parent_after_publish; const legacy_failure_points = [_]FailurePoint{ .flush_store, .sync_store, .close_store, .flush_map, .sync_map, .close_map, .sync_temporary_directory, .rename_no_replace, }; test "import-git removes all temporary output when storage creation fails" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var git: ScriptedGit = .{ .fast_export = valid_export }; const result = runFixture( tmp, "fixture.git", "storage-failed", false, &git, .{ .fail_at = create_failure_point }, ); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqualStrings("", result.out); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, "storage-failed"); } // Every legacy selector, not a sample of three. // // The one-sided selectors are aliases: `check` maps each to its `before_*` // twin, so a selector whose arm is missing from that map does not fail the // import at all -- it PUBLISHES, and the caller that asked for a fault reads // the resulting green as evidence the boundary is safe. Three of these eight // arms were exercised and five were not, which is the shape where a silently // inert selector survives. Asserting `Exit.failure` per alias is what makes // each arm's presence load-bearing. test "import-git removes all temporary output when publication flush or rename fails" { for (legacy_failure_points) |failure| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var git: ScriptedGit = .{ .fast_export = valid_export }; const result = runFixture( tmp, "fixture.git", "publication-failed", false, &git, .{ .fail_at = failure }, ); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqualStrings("", result.out); // No child left behind: launched once, reaped once, every alias. try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, "publication-failed"); } } test "import-git writes only a machine-readable destination to stdout and progress to stderr" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var git: ScriptedGit = .{ .fast_export = valid_export, .stderr = "git progress\n" }; const result = runFixture(tmp, "fixture.git", "machine", false, &git, .real_filesystem); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expectEqualStrings("machine\n", result.out); try std.testing.expect(std.mem.indexOf(u8, result.err, "git progress") != null); try std.testing.expect(std.mem.indexOf(u8, result.out, "progress") == null); } test "import-git --quiet suppresses progress but not diagnostics" { var quiet_tmp = std.testing.tmpDir(.{}); defer quiet_tmp.cleanup(); var quiet_git: ScriptedGit = .{ .fast_export = valid_export, .stderr = "git progress\n" }; const quiet = runFixture(quiet_tmp, "fixture.git", "quiet", true, &quiet_git, .real_filesystem); try std.testing.expectEqual(cli.Exit.ok, quiet.code); try std.testing.expectEqualStrings("quiet\n", quiet.out); try std.testing.expectEqualStrings("", quiet.err); var error_tmp = std.testing.tmpDir(.{}); defer error_tmp.cleanup(); var error_git: ScriptedGit = .{ .fast_export = malformed_export, .stderr = "fatal fixture diagnostic\n" }; const failed = runFixture(error_tmp, "fixture.git", "quiet-error", true, &error_git, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, failed.code); try std.testing.expectEqualStrings("", failed.out); try std.testing.expect(std.mem.indexOf(u8, failed.err, "fatal fixture diagnostic") != null); } // The remaining tests specify the Task 10 seam not yet present in // `git_import_cli`: CommandScript records every preflight/export invocation, // supplies bounded stdout/stderr streams, and exposes deterministic // cancellation and publication-boundary faults. Production dispatch constructs // the OS adapter; tests use no shell or network. fn scripted( tmp: std.testing.TmpDir, script: *import_cli.CommandScript, destination: []const u8, quiet: bool, fault: import_cli.PublicationHooks, capture: *import_cli.ScriptCapture, ) import_cli.ScriptResult { return import_cli.runScripted( std.testing.allocator, std.testing.io, .{ .source = "fixture.git", .destination_parent = tmp.dir, .destination_name = destination, .project_code = sync.deriveProjectCode(&.{destination}), .submanifest = .{ .tree = 5 }, .attribution = .committer, .default_branch = null, .quiet = quiet, }, script, fault, capture, ); } const ProcessResult = struct { code: cli.Exit, out: []const u8, err: []const u8, exit_code: u8, reaped: bool, reap_calls: usize, child_gone: bool, stderr_bytes_drained: usize, retained_diagnostic_len: usize, }; /// Drives the real ProductionGit session against the build-wired helper. The /// only substitution is argv; spawning, pipe adapters, concurrent diagnostic /// drain, terminate, join, and wait are the production implementation. fn runProductionPipe( tmp: std.testing.TmpDir, destination: []const u8, mode: []const u8, ) !ProcessResult { var threaded = std.Io.Threaded.init(std.testing.allocator, .{}); defer threaded.deinit(); const io = threaded.io(); var session = import_cli.ProductionGit.initForTest( std.testing.allocator, io, std.testing.environ, &.{ helper_options.git_import_pipe_helper, mode }, ); const Capture = struct { var out_storage: [4096]u8 = undefined; var err_storage: [8192]u8 = undefined; }; var out = std.Io.Writer.fixed(&Capture.out_storage); var err = std.Io.Writer.fixed(&Capture.err_storage); const code = import_cli.run( std.testing.allocator, io, .{ .source = "fixture.git", .destination_parent = tmp.dir, .destination_name = destination, .project_code = sync.deriveProjectCode(&.{destination}), }, .{ .out = &out, .err = &err }, session.dependencies(.real_filesystem), ); return .{ .code = code, .out = out.buffered(), .err = err.buffered(), .exit_code = session.exit_code, .reaped = session.reaped, .child_gone = session.child == null, .stderr_bytes_drained = session.stderr_bytes_drained, .reap_calls = session.reap_calls, .retained_diagnostic_len = session.stderr_ring_len, }; } fn runGit(io: std.Io, argv: []const []const u8) !void { const result = try std.process.run(std.testing.allocator, io, .{ .argv = argv }); defer std.testing.allocator.free(result.stdout); defer std.testing.allocator.free(result.stderr); switch (result.term) { .exited => |code| if (code == 0) return, else => {}, } return error.GitSetupFailed; } fn writeFixtureFile(io: std.Io, dir: std.Io.Dir, path: []const u8, bytes: []const u8) !void { var file = try dir.createFile(io, path, .{ .read = true, .truncate = true }); defer file.close(io); try file.writeStreamingAll(io, bytes); } fn runProductionPreflight( tmp: std.testing.TmpDir, source: []const u8, destination: []const u8, ) cli.Exit { const Capture = struct { var out_storage: [1024]u8 = undefined; var err_storage: [1024]u8 = undefined; }; var out = std.Io.Writer.fixed(&Capture.out_storage); var err = std.Io.Writer.fixed(&Capture.err_storage); var session = import_cli.ProductionGit.init(std.testing.allocator, std.testing.io, std.testing.environ); defer session.deinit(); return import_cli.run( std.testing.allocator, std.testing.io, .{ .source = source, .destination_parent = tmp.dir, .destination_name = destination, .project_code = sync.deriveProjectCode(&.{destination}), }, .{ .out = &out, .err = &err }, session.dependencies(.real_filesystem), ); } test "import-git parser leaves k unset for analysis and is a real dispatched verb" { // Absent means "analyze", not "5": the parser must not manufacture a pin // the runner would then be unable to distinguish from an operator's. const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" }); try std.testing.expectEqual(@as(?u4, null), defaulted.submanifest_k); try std.testing.expectEqual(cli.Verb.import_git, cli.verbOf("import-git")); try std.testing.expect(!cli.isPlanned("import-git")); const parsed = try cli.parseImportGitArgs(&.{ "--submanifest-k", "7", "--attribution", "author", "--default-branch", "main", "--quiet", "fixture.git", "dest", }); try std.testing.expectEqual(@as(?u4, 7), parsed.submanifest_k); try std.testing.expectEqual(.author, parsed.attribution); try std.testing.expectEqualStrings("main", parsed.default_branch.?); try std.testing.expect(parsed.quiet); const min_k = try cli.parseImportGitArgs(&.{ "--submanifest-k", "1", "fixture.git", "dest" }); try std.testing.expectEqual(@as(?u4, 1), min_k.submanifest_k); const max_k = try cli.parseImportGitArgs(&.{ "--submanifest-k", "12", "fixture.git", "dest" }); try std.testing.expectEqual(@as(?u4, 12), max_k.submanifest_k); for ([_][]const u8{ "0", "13", "14", "15" }) |value| { try std.testing.expectError( cli.ImportGitParseError.InvalidSubmanifestK, cli.parseImportGitArgs(&.{ "--submanifest-k", value, "fixture.git", "dest" }), ); } } test "import-git parser accepts --no-submanifests and refuses it paired with a pinned k" { const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" }); try std.testing.expect(!defaulted.no_submanifests); // The flag declines the mechanism; it does not manufacture a k, and it must // not be confusable with one -- `submanifest_k` stays absent. const flat = try cli.parseImportGitArgs(&.{ "--no-submanifests", "fixture.git", "dest" }); try std.testing.expect(flat.no_submanifests); try std.testing.expectEqual(@as(?u4, null), flat.submanifest_k); // BOTH orders. An in-arm check would only fire for whichever token happened // to be seen second, which is exactly the bug this pairing invites. try std.testing.expectError( cli.ImportGitParseError.ConflictingSubmanifestOptions, cli.parseImportGitArgs(&.{ "--no-submanifests", "--submanifest-k", "5", "fixture.git", "dest" }), ); try std.testing.expectError( cli.ImportGitParseError.ConflictingSubmanifestOptions, cli.parseImportGitArgs(&.{ "--submanifest-k", "5", "--no-submanifests", "fixture.git", "dest" }), ); // A malformed token still reports its own cause: the conflict is ranked // below `first_err`, so an invalid k is not masked by the pairing. try std.testing.expectError( cli.ImportGitParseError.InvalidSubmanifestK, cli.parseImportGitArgs(&.{ "--submanifest-k", "0", "--no-submanifests", "fixture.git", "dest" }), ); // Same shape as every other boolean flag on this verb. try std.testing.expectError( cli.ImportGitParseError.RepeatedOption, cli.parseImportGitArgs(&.{ "--no-submanifests", "--no-submanifests", "fixture.git", "dest" }), ); // Help wins over the conflict, so `--help` stays informative no matter what // else is on the line. const helped = try cli.parseImportGitArgs(&.{ "--no-submanifests", "--submanifest-k", "5", "--help" }); try std.testing.expect(helped.help); // It composes with every other behavioral flag; none of them interact. const composed = try cli.parseImportGitArgs(&.{ "--no-submanifests", "--refuse-signed", "--attribution", "author", "--default-branch", "trunk", "--quiet", "fixture.git", "dest", }); try std.testing.expect(composed.no_submanifests); try std.testing.expect(composed.refuse_signed); try std.testing.expectEqual(.author, composed.attribution); try std.testing.expectEqualStrings("trunk", composed.default_branch.?); try std.testing.expect(composed.quiet); } test "import-git parser exposes the export payload budget as an operator flag" { // Every ceiling an honest source can reach must be raisable without a // recompile. This one budget carries both the cumulative payload limit and // the derived per-commit file-command ceiling, so it is the only knob the // verb needs. const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" }); try std.testing.expectEqual(@as(?u64, null), defaulted.max_export_payload_bytes); const raised = try cli.parseImportGitArgs(&.{ "--max-export-payload-bytes", "68719476736", "fixture.git", "dest", }); try std.testing.expectEqual(@as(?u64, 68719476736), raised.max_export_payload_bytes); // Zero derives a per-commit ceiling of zero: a budget no import can ever // satisfy is a typo, not a policy. try std.testing.expectError( cli.ImportGitParseError.InvalidExportPayloadBytes, cli.parseImportGitArgs(&.{ "--max-export-payload-bytes", "0", "fixture.git", "dest" }), ); try std.testing.expectError( cli.ImportGitParseError.InvalidExportPayloadBytes, cli.parseImportGitArgs(&.{ "--max-export-payload-bytes", "many", "fixture.git", "dest" }), ); try std.testing.expectError( cli.ImportGitParseError.MissingOptionValue, cli.parseImportGitArgs(&.{ "fixture.git", "dest", "--max-export-payload-bytes" }), ); try std.testing.expectError( cli.ImportGitParseError.RepeatedOption, cli.parseImportGitArgs(&.{ "--max-export-payload-bytes", "1024", "--max-export-payload-bytes", "2048", "fixture.git", "dest", }), ); } /// Whether `token` appears anywhere in `argv`. Positional indices would encode /// the argv's shape, which is not what this asserts. fn argvHas(argv: []const []const u8, token: []const u8) bool { for (argv) |candidate| { if (std.mem.eql(u8, candidate, token)) return true; } return false; } test "import-git strips signatures by default and aborts only under --refuse-signed" { // Fossil has no signature card, so refusing by default withheld the import // without saving anything. The default must therefore strip. const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" }); try std.testing.expect(!defaulted.refuse_signed); const stripping = import_cli.exportArgv("fixture.git", defaulted.refuse_signed); try std.testing.expect(argvHas(&stripping, "--signed-commits=strip")); try std.testing.expect(argvHas(&stripping, "--signed-tags=strip")); try std.testing.expect(!argvHas(&stripping, "--signed-commits=abort")); try std.testing.expect(!argvHas(&stripping, "--signed-tags=abort")); // The opt-out restores the refusal, and nothing else about the argv moves. const opted_out = try cli.parseImportGitArgs(&.{ "--refuse-signed", "fixture.git", "dest" }); try std.testing.expect(opted_out.refuse_signed); const aborting = import_cli.exportArgv("fixture.git", opted_out.refuse_signed); try std.testing.expect(argvHas(&aborting, "--signed-commits=abort")); try std.testing.expect(argvHas(&aborting, "--signed-tags=abort")); try std.testing.expectEqual(stripping.len, aborting.len); for (stripping, aborting) |strip_token, abort_token| { if (std.mem.startsWith(u8, strip_token, "--signed-")) continue; try std.testing.expectEqualStrings(strip_token, abort_token); } // Same error shape as every other boolean flag: repeated is a usage error, // and the retired spelling is simply unknown. try std.testing.expectError( cli.ImportGitParseError.RepeatedOption, cli.parseImportGitArgs(&.{ "--refuse-signed", "--refuse-signed", "fixture.git", "dest" }), ); try std.testing.expectError( cli.ImportGitParseError.UnknownOption, cli.parseImportGitArgs(&.{ "--drop-signatures", "fixture.git", "dest" }), ); } test "import-git help exits zero and malformed argument forms exit usage without process launch" { const help = import_cli.dispatchForTest(&.{"--help"}); try std.testing.expectEqual(cli.Exit.ok, help.code); try std.testing.expectEqualStrings("", help.err); try std.testing.expect(std.mem.startsWith(u8, help.out, "usage: zit import-git SOURCE DEST")); // Substring checks are what let the previous hand transcription rot two // options behind the real help, so pin every option the usage line owns. for ([_][]const u8{ "--submanifest-k K | --no-submanifests", "--attribution", "--default-branch", "--refuse-signed", "--provenance", "--max-export-payload-bytes", "--quiet", }) |option| { try std.testing.expect(std.mem.indexOf(u8, help.out, option) != null); } const accepted_flat = import_cli.dispatchForTest(&.{ "--no-submanifests", "fixture.git", "dest" }); try std.testing.expectEqual(cli.Exit.failure, accepted_flat.code); try std.testing.expectEqual(@as(usize, 0), accepted_flat.lifecycle.launched); try std.testing.expectEqualStrings("test dispatch requires scripted dependencies\n", accepted_flat.err); const accepted_max = import_cli.dispatchForTest(&.{ "--submanifest-k", "12", "fixture.git", "dest" }); try std.testing.expectEqual(cli.Exit.failure, accepted_max.code); try std.testing.expectEqual(@as(usize, 0), accepted_max.lifecycle.launched); try std.testing.expectEqualStrings("test dispatch requires scripted dependencies\n", accepted_max.err); for ([_][]const []const u8{ &.{ "--submanifest-k", "0", "fixture.git", "dest" }, &.{ "--submanifest-k", "13", "fixture.git", "dest" }, &.{ "--submanifest-k", "14", "fixture.git", "dest" }, &.{ "--submanifest-k", "15", "fixture.git", "dest" }, &.{ "--attribution", "committerish", "fixture.git", "dest" }, // The conflicting pair exits `usage` in both orders and launches // nothing -- no process is spawned to discover an argument mistake. &.{ "--no-submanifests", "--submanifest-k", "5", "fixture.git", "dest" }, &.{ "--submanifest-k", "5", "--no-submanifests", "fixture.git", "dest" }, &.{ "--no-submanifests", "--no-submanifests", "fixture.git", "dest" }, &.{"fixture.git"}, &.{ "fixture.git", "dest", "extra" }, }) |args| { const usage = import_cli.dispatchForTest(args); try std.testing.expectEqual(cli.Exit.usage, usage.code); try std.testing.expectEqual(@as(usize, 0), usage.lifecycle.launched); try std.testing.expectEqualStrings("", usage.out); try std.testing.expect(usage.err.len != 0); } } test "import-git rejects missing Git non-repository shallow and partial sources before export" { const preflight_failures = [_]import_cli.CommandScript.Preflight{ .git_missing, .not_repository, .missing_objects, .shallow, .partial_promisor, }; for (preflight_failures) |preflight| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = preflight }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "rejected", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqual(@as(usize, 0), script.export_starts); try std.testing.expect(script.preflight_starts != 0); try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, "rejected"); } } test "import-git production preflight rejects absent non-repository missing-object shallow and partial sources" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var root_buf: [128]u8 = undefined; const root = try std.fmt.bufPrint(&root_buf, ".zig-cache/tmp/{s}", .{tmp.sub_path}); var source_buf: [160]u8 = undefined; const source = try std.fmt.bufPrint(&source_buf, "{s}/source", .{root}); // A non-existent -C path and an existing non-repository both execute real // ProductionGit query children and fail before an export can launch. try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "missing-git")); try tmp.dir.createDir(std.testing.io, "not-repository", .default_dir); var nonrepo_buf: [160]u8 = undefined; const nonrepo = try std.fmt.bufPrint(&nonrepo_buf, "{s}/not-repository", .{root}); try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, nonrepo, "not-repository-dest")); try expectNoPublication(std.testing.io, tmp, "missing-git"); try expectNoPublication(std.testing.io, tmp, "not-repository-dest"); try runGit(std.testing.io, &.{ "git", "init", source }); try runGit(std.testing.io, &.{ "git", "-C", source, "config", "user.name", "Zit Test" }); try runGit(std.testing.io, &.{ "git", "-C", source, "config", "user.email", "[email protected]" }); try runGit(std.testing.io, &.{ "git", "-C", source, "commit", "--allow-empty", "-m", "init" }); try runGit(std.testing.io, &.{ "git", "-C", source, "config", "extensions.partialClone", "origin" }); try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "partial")); try runGit(std.testing.io, &.{ "git", "-C", source, "config", "--unset", "extensions.partialClone" }); try writeFixtureFile(std.testing.io, tmp.dir, "source/.git/shallow", "0000000000000000000000000000000000000000\n"); try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "shallow")); try tmp.dir.deleteFile(std.testing.io, "source/.git/shallow"); try tmp.dir.deleteTree(std.testing.io, "source/.git/objects"); try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "missing-objects")); try expectNoPublication(std.testing.io, tmp, "partial"); try expectNoPublication(std.testing.io, tmp, "shallow"); try expectNoPublication(std.testing.io, tmp, "missing-objects"); } test "import-git records detached HEAD and refuses it unless default branch is explicit" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .detached_head, .fast_export = valid_export }); var rejected_cap: import_cli.ScriptCapture = .{}; const rejected = scripted(tmp, &script, "detached", false, .real_filesystem, &rejected_cap); try std.testing.expectEqual(cli.Exit.failure, rejected.code); try std.testing.expectEqual(@as(usize, 0), script.export_starts); try expectNoPublication(std.testing.io, tmp, "detached"); script.default_branch = "main"; var accepted_cap: import_cli.ScriptCapture = .{}; const accepted = scripted(tmp, &script, "detached-with-branch", false, .real_filesystem, &accepted_cap); try std.testing.expectEqual(cli.Exit.ok, accepted.code); try std.testing.expect(script.detached_head_recorded); } test "import-git starts fast-export with the exact safe argv and disabled prompt and lazy fetch" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "argv", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expectEqualStrings( "git -C fixture.git fast-export --all --show-original-ids --mark-tags --signed-commits=strip --signed-tags=strip --use-done-feature", script.exportCommandLine(), ); try std.testing.expectEqualStrings("0", script.environment.get("GIT_TERMINAL_PROMPT").?); try std.testing.expectEqualStrings("1", script.environment.get("GIT_NO_LAZY_FETCH").?); } test "import-git drains real OS-pipe backpressure concurrently while stdout remains active" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const result = try runProductionPipe(tmp, "flood", "success"); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expectEqual(@as(usize, 8 * 1024 * 1024), result.stderr_bytes_drained); try std.testing.expectEqual(@as(usize, 4096), result.retained_diagnostic_len); // Strictly smaller than the capture: a full buffer means the completion // line asserted below may have been silently dropped, not that output was bounded. try std.testing.expect(result.err.len < 8192); try std.testing.expect(std.mem.indexOf(u8, result.err, "imported 'fixture.git'") != null); try std.testing.expectEqual(@as(u8, 0), result.exit_code); try std.testing.expect(result.reaped); try std.testing.expect(result.child_gone); } test "import-git cancellation terminates a real child then joins and reaps its actual status once" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const result = try runProductionPipe(tmp, "cancelled", "hold"); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(result.exit_code != 0); try std.testing.expect(result.err.len < 4096); try std.testing.expect(std.mem.indexOf(u8, result.err, "Malformed") != null); try std.testing.expect(std.mem.indexOf(u8, result.err, "GitExportFailed") == null); try std.testing.expect(result.reaped); try std.testing.expect(result.child_gone); try expectNoPublication(std.testing.io, tmp, "cancelled"); } test "import-git non-Darwin/Linux fallback drains stable stderr ownership then reaps once" { if (builtin.os.tag == .macos or builtin.os.tag == .linux) return; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const result = try runProductionPipe(tmp, "fallback-cancelled", "hold"); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(result.stderr_bytes_drained != 0); try std.testing.expect(result.reaped); try std.testing.expectEqual(@as(usize, 1), result.reap_calls); try std.testing.expect(result.child_gone); try expectNoPublication(std.testing.io, tmp, "fallback-cancelled"); } test "import-git rejects a nonzero completed export after draining and reaping it once" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const result = try runProductionPipe(tmp, "nonzero-export", "nonzero"); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqual(@as(u8, 23), result.exit_code); try std.testing.expect(result.reaped); try std.testing.expect(result.child_gone); try expectNoPublication(std.testing.io, tmp, "nonzero-export"); } test "import-git prefers a nonzero truncated export status over its parser EOF" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const result = try runProductionPipe(tmp, "signed-like-export", "truncated-nonzero"); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqual(@as(u8, 23), result.exit_code); try std.testing.expect(std.mem.indexOf(u8, result.err, "GitExportFailed") != null); try std.testing.expect(std.mem.indexOf(u8, result.err, "fatal: signed commit rejected") != null); try std.testing.expect(std.mem.indexOf(u8, result.err, "UnexpectedEof") == null); try std.testing.expect(result.reaped); try std.testing.expect(result.child_gone); try expectNoPublication(std.testing.io, tmp, "signed-like-export"); } test "import-git verifies the frozen .zit and import map before the atomic rename" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "verified", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expect(script.publication.store_reopened_for_verify); try std.testing.expect(script.publication.map_reopened_for_verify); try std.testing.expect(script.publication.verify_before_publish); } test "import-git competing destination at commit point never clobbers it and removes its temporary sibling" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export, .competing_destination_at_publish = true, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "raced", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(script.competitor_destination_intact); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try std.testing.expect(!script.temporary_sibling_left); } test "import-git never publishes or deletes a replaced staging sibling" { const replacements = [_]import_cli.CommandScript.StagingReplacement{ .directory, .symlink }; for (replacements) |replacement| { for (0..8) |_| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDir(std.testing.io, "outside", .default_dir); try writeFixtureFile(std.testing.io, tmp.dir, "outside/replacement-sentinel", "survives\n"); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export, .replace_staging_at_publish = replacement, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "raced-staging", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(std.mem.indexOf(u8, result.err, "StagingEntryReplaced") != null); try std.testing.expect(script.staging_replacement_observed); try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try std.testing.expect(!directoryExists(std.testing.io, tmp.dir, "raced-staging")); try std.testing.expect(!exists(std.testing.io, tmp.dir, ".zit-import-map.jsonl")); const staging_name = script.stagingReplacementName(); try std.testing.expect(staging_name.len != 0); var outside = try tmp.dir.openDir(std.testing.io, "outside", .{ .follow_symlinks = false }); defer outside.close(std.testing.io); try std.testing.expect(exists(std.testing.io, outside, "replacement-sentinel")); switch (replacement) { .directory => { var preserved = try tmp.dir.openDir(std.testing.io, staging_name, .{ .follow_symlinks = false }); defer preserved.close(std.testing.io); try std.testing.expect(exists(std.testing.io, preserved, "replacement-sentinel")); }, .symlink => { var target: [32]u8 = undefined; const len = try tmp.dir.readLink(std.testing.io, staging_name, &target); try std.testing.expectEqualStrings("outside", target[0..len]); }, } } } } test "import-git default ceilings clear the retired 200k per-commit file wall" { const limits: import_cli.ImportLimits = .{}; // The import leaves the arena budget at its default, so that default is // what an operator actually meets. const defaults: fast_export.Limits = .{ .max_line = 0, .max_data = 0, .max_path = 0, .max_events = std.math.maxInt(u64) }; const ceiling = fast_export.fileCommandCeiling(defaults); // The retired ceiling was 200,000 because that is the benchmark corpus // width. The derived one must clear it by orders of magnitude, so no // ordinary monorepo root commit can reach it. try std.testing.expect(ceiling > 100 * 200_000); try std.testing.expectEqual( defaults.max_command_arena_bytes / fast_export.min_retained_bytes_per_file_command, ceiling, ); // The cumulative payload budget must clear a real repository: sqlite's // export stream is 12.2 GiB, and a default that refuses it is a default // that refuses ordinary work. It must also no longer move the ceiling. try std.testing.expect(limits.max_export_payload_bytes > 16 * @as(u64, 1024 * 1024 * 1024)); var raised = defaults; raised.max_total_data = limits.max_export_payload_bytes; try std.testing.expectEqual(ceiling, fast_export.fileCommandCeiling(raised)); try std.testing.expect(limits.max_retained_bytes >= 64 * 1024 * 1024); } test "import-git reader limits reject before event and data parsing allocations" { const event_limited = "feature done\nblob\nmark :1\ndata 1\nx\n"; var event_reader = fast_export.Reader.init(std.testing.allocator, .fixed(event_limited), .{ .max_line = 1024, .max_data = 1024, .max_path = 1024, .max_events = 0, .max_total_data = 1024, }); defer event_reader.deinit(); try std.testing.expectError(error.EventLimitExceeded, event_reader.next()); const data_limited = "feature done\nblob\nmark :1\ndata 1\nx\n"; var data_reader = fast_export.Reader.init(std.testing.allocator, .fixed(data_limited), .{ .max_line = 1024, .max_data = 1024, .max_path = 1024, .max_events = 1, .max_total_data = 0, }); defer data_reader.deinit(); try std.testing.expectError(error.DataLimitExceeded, data_reader.next()); } test "import-git enforces total import ceilings before publication" { // The two budgets are separate now, so the accepted value only has to // clear the stream's nine payload bytes. const one_command_budget = fast_export.min_retained_bytes_per_file_command; const accepted_limits: import_cli.ImportLimits = .{ .max_export_payload_bytes = one_command_budget, .max_retained_bytes = 1024, }; const RejectedLimit = struct { limits: import_cli.ImportLimits, error_name: []const u8, }; const rejected_limits = [_]RejectedLimit{ .{ // One byte below the stream's own payload. The blob's five bytes // admit; the commit message's four push the total past it, before // any file command is parsed. .limits = .{ .max_export_payload_bytes = 8, }, .error_name = "ExportPayloadLimitExceeded", }, .{ .limits = .{ .max_export_payload_bytes = accepted_limits.max_export_payload_bytes, .max_retained_bytes = 0, }, .error_name = "ImporterRetainedByteLimitExceeded", }, }; { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export_with_oids, .limits = accepted_limits, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "at-limit", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); } for (rejected_limits, 0..) |rejected, index| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export_with_oids, .limits = rejected.limits, }); var destination: [32]u8 = undefined; const destination_name = try std.fmt.bufPrint(&destination, "limit-{d}", .{index}); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, destination_name, false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(std.mem.indexOf(u8, result.err, rejected.error_name) != null); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, destination_name); } { // One byte below one command's minimum retention derives a ceiling of // zero, so the stream's single `M` command is refused. The failure must // name the observed count, the ceiling, and the derivation. It must // NOT name a flag: this ceiling is structural, and the payload knob // that once raised it no longer moves it at all. var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export_with_oids, .limits = .{ .max_command_arena_bytes = one_command_budget - 1, .max_retained_bytes = accepted_limits.max_retained_bytes, }, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "command-ceiling", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); var expected: [512]u8 = undefined; const diagnostic = try std.fmt.bufPrint( &expected, "import-git: import failed: ExportFileCommandLimitExceeded\n" ++ "import-git: a commit exceeded the per-commit file-command ceiling: " ++ "observed at least 1, ceiling 0, derived from the {d}-byte per-commit " ++ "arena budget at {d} minimum retained bytes per command. This is a " ++ "structural bound on one commit, not a total: no option raises it, " ++ "because a commit at this width is beyond what the importer can hold whole\n", .{ one_command_budget - 1, one_command_budget }, ); try std.testing.expect(std.mem.indexOf(u8, result.err, diagnostic) != null); try std.testing.expect(std.mem.indexOf(u8, result.err, "--max-export-payload-bytes") == null); try expectNoPublication(std.testing.io, tmp, "command-ceiling"); } { // The cumulative-payload refusal must name its numbers for the same // reason the file-command one does. `ExportPayloadLimitExceeded` alone // sent a real 32k-commit import 17 minutes downstream to learn a single // integer, and the integer it needs is not even the one it was refused // at: parsing stops at the overrun, so the observed total is a lower // bound on the stream's payload and the budget must clear the whole of // it. Saying "above 9" would name a value that fails again. var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export_with_oids, .limits = .{ .max_export_payload_bytes = 8, .max_retained_bytes = accepted_limits.max_retained_bytes, }, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "payload-ceiling", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(std.mem.indexOf( u8, result.err, "import-git: import failed: ExportPayloadLimitExceeded\n" ++ "import-git: the export stream's cumulative payload exceeded its budget: " ++ "observed at least 9 bytes against a budget of 8 bytes; parsing stopped there, " ++ "so the stream's full payload is larger than the observed total; re-run with " ++ "--max-export-payload-bytes above the stream's full payload\n", ) != null); try expectNoPublication(std.testing.io, tmp, "payload-ceiling"); } } const pre_rename_failure_points = [_]FailurePoint{ .before_flush_store, .after_flush_store, .before_sync_store, .after_sync_store, .before_close_store, .after_close_store, .before_flush_map, .after_flush_map, .before_sync_map, .after_sync_map, .before_close_map, .after_close_map, .capture_drop_set, .create_packed_store, .before_pack, .after_pack, .before_sync_packed_store, .after_sync_packed_store, .before_close_packed_store, .after_close_packed_store, .verify_raw_store, .verify_store, .verify_map, .before_discard_raw_generation, .after_discard_raw_generation, .before_sync_temporary_directory, .after_sync_temporary_directory, .before_rename_no_replace, }; test "import-git aborts every pre-rename writable-boundary failure without a partial destination" { for (pre_rename_failure_points) |failure| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "boundary-failed", false, .{ .fail_at = failure }, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); // All three clauses, per case. A pre-rename abort that only proves DEST // is absent is compatible with a retained staging sibling holding the // whole raw generation -- on a real corpus that is tens of megabytes of // garbage per failed import, and it is invisible to a destination-only // assertion. // // No child process. try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); // No destination. try expectNoPublication(std.testing.io, tmp, "boundary-failed"); // No temporary sibling -- asked a second time, fail-CLOSED, from // inside the harness. `expectNoPublication` scans `tmp.dir` from the // outside; this one is taken by `runScripted` the instant `run` // returns, against the parent the transaction was actually handed, and // it reports a LEAK whenever it cannot look -- no parent, no `io`, an // iterate error. A negative assertion answered "clean" on a question // that could not be asked is exactly how this clause would go vacuous. try std.testing.expect(!script.temporary_sibling_left); } } // The gate on the four fault sweeps above: every boundary the transaction // declares is driven by one of them, and each is driven by exactly one. // // This is what makes "failures at EVERY new boundary" checkable instead of // asserted. Without it, adding a `FailurePoint` -- which is what every new // writable step in this transaction does -- silently ships an untested fault // arm, and the suite stays green because the arm is never selected. Both // directions are asserted for the same reason `ci_test.zig` asserts its step // set both ways: a class list that names a point twice is a bookkeeping error // that would otherwise let a real point go missing while the count still // matched. test "import-git drives every declared publication failure point in exactly one fault class" { var covered = std.EnumSet(FailurePoint).initEmpty(); const singletons = [_]FailurePoint{ create_failure_point, post_rename_failure_point, parent_sync_failure_point, }; for (&[_][]const FailurePoint{ &singletons, &legacy_failure_points, &pre_rename_failure_points, }) |class| { for (class) |point| { if (covered.contains(point)) { std.debug.print("failure point classified twice: {t}\n", .{point}); return error.DuplicateFailurePointClass; } covered.insert(point); } } inline for (@typeInfo(FailurePoint).@"enum".fields) |field| { const point: FailurePoint = @enumFromInt(field.value); if (!covered.contains(point)) { std.debug.print("failure point never driven by any test: {s}\n", .{field.name}); return error.UndrivenFailurePoint; } } try std.testing.expectEqual(@typeInfo(FailurePoint).@"enum".fields.len, covered.count()); } // The rename is the commit point, and `after_rename_no_replace` is the first // boundary strictly past it. Every other publication fault is answered with // "DEST is absent"; this one must be answered with the opposite, because by the // time it fires the verified generation is already the destination and undoing // it would destroy a complete repository to report a failure about a step that // happened afterwards. // // The staging sibling is gone here for the same reason -- it WAS the // destination -- so `expectNoPublication`'s sibling clause is asserted directly // rather than through a helper whose first clause would be inverted. test "import-git keeps the published destination when a boundary strictly after the rename fails" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "post-rename", false, .{ .fail_at = post_rename_failure_point }, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try std.testing.expect(!script.temporary_sibling_left); var destination = try tmp.dir.openDir(std.testing.io, "post-rename", .{}); defer destination.close(std.testing.io); // The whole manifest and NOTHING BESIDE IT. Naming only the entries that // must be present would pass a destination that also carried a fifth -- // and the rename ships the staging directory whole, so anything extra in // it is published by accident into every imported repository. try expectPublishManifestExactly(std.testing.io, destination); // Generation 1 left staging before the rename, so it cannot have ridden // into the published destination on the way past this boundary. try std.testing.expect(!exists(std.testing.io, destination, ".zit-import-raw")); } // Cancellation and child failure WHILE GENERATION 1 EXISTS. // // Both faults land after `createRawGeneration` and before anything packs, which // is the one window in which the transaction owns a live, half-written store // that no later stage will ever look at. Neither seam had a test: `run` fails // out of `consumeExport`, and the only thing that then removes generation 1 is // the bundle's own abort path. test "import-git unwinds a cancelled export that already opened the raw generation" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); // `cancel_export_mid_stream` replaces the stream with one that parses a blob // and then hits a record the reader rejects -- a truncation, not a rejection // at record zero, so the raw generation is open when it happens. var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export, .cancel_export_mid_stream = true, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "cancelled", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); // The child is terminated and reaped exactly once: an abandoned export is // still a process, and leaking it would outlive the failed import. try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, "cancelled"); // Generation 1 was open and partly written when the cancel landed, so the // sibling holding it is exactly what must not survive. Fail-closed. try std.testing.expect(!script.temporary_sibling_left); } test "import-git unwinds a failed export child that already opened the raw generation" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); // A COMPLETE, well-formed stream whose child then exits non-zero. The // import has no parse error to fail on: the only thing wrong is the exit // status, and trusting a stream whose producer died is exactly how a // truncated history would publish as a whole one. var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export, .export_exit = 128, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "child-failed", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, "child-failed"); try std.testing.expect(!script.temporary_sibling_left); } // The premise both tests above are NAMED for, pinned from the export stream and // the boundary log instead of from a comment. // // Replayed through `runFixture` rather than `scripted` because the two faults // need no script -- a stream and an exit status are the whole fixture -- and the // pass-through harness is the smaller thing to be wrong about. (`scripted` would // also work now: `CommandScript.dependencies` chains a caller's observer behind // its own. It used to REPLACE it, which is how this pairing was first written // and how it went green while observing nothing.) test "import-git export faults that unwind generation 1 reach the raw store and nothing past it" { const Case = struct { name: []const u8, export_bytes: []const u8, exit_code: u8, rejected: bool }; const cases = [_]Case{ // The truncated stream `cancel_export_mid_stream` substitutes, BORROWED from // the seam rather than retyped: a hand copy would keep passing against // stale bytes the moment the production literal moved, and the claim // this case makes is a claim about that exact stream. .{ .name = "cancelled-observed", .export_bytes = import_cli.CommandScript.cancelled_export, .exit_code = 0, .rejected = true, }, // Complete and well-formed; only the child's status is wrong. .{ .name = "child-failed-observed", .export_bytes = valid_export, .exit_code = 128, .rejected = false }, }; for (cases) |case| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); // The premise, measured from the stream itself. `create_store` cannot // state it -- that boundary is checked before `createRawGeneration`, so // a stream rejected at record zero observes it exactly as a truncated // one does. Replaying through the same reader with the same limits says // which this is: at least one accepted record means the failure lands // with generation 1 open and already written. const reach = try exportReach(case.export_bytes); try std.testing.expect(reach.events >= 1); try std.testing.expectEqual(case.rejected, reach.rejected); var recorder: PointRecorder = .{}; var git: ScriptedGit = .{ .fast_export = case.export_bytes, .export_exit = case.exit_code }; const result = runFixture(tmp, "fixture.git", case.name, false, &git, recorder.hooks()); try std.testing.expectEqual(cli.Exit.failure, result.code); // The transaction entered `importInto` rather than failing in // preflight or argument handling -- which is all this boundary can say. try std.testing.expect(recorder.seen.contains(create_failure_point)); // And nothing downstream of it ran. A fault that unwound from further // along is a different window with a different abort path, and would // leave this pair claiming coverage it does not have. try std.testing.expect(!recorder.seen.contains(.capture_drop_set)); try std.testing.expect(!recorder.seen.contains(.create_packed_store)); try std.testing.expect(!recorder.seen.contains(.before_rename_no_replace)); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.launched); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, case.name); } } // The scripted harness passes a caller's observer THROUGH. // // `CommandScript.dependencies` must install its own observer -- that recorder is // how the whole two-generation ordering in `Publication` gets written -- so the // obvious implementation replaces the caller's, and that is what it used to do. // The failure mode is the worst kind available to a test suite: `scripted` takes // the observer, drops it without a diagnostic, and every assertion made through // it passes while observing nothing. // // Asserted here rather than left to the doc comment, because a constraint that // only a comment enforces is one the next caller re-discovers by shipping a // vacuous green. A successful import walks the whole boundary sequence, so a // dropped observer shows up as an EMPTY set against three points the run // certainly reached. test "import-git scripted runs report publication boundaries to a caller's observer" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var recorder: PointRecorder = .{}; var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "observed", false, recorder.hooks(), &result_cap); try std.testing.expectEqual(cli.Exit.ok, result.code); // One from each generation and one from publication, so a chain that only // survived part of the transaction cannot satisfy this. try std.testing.expect(recorder.seen.contains(.create_store)); try std.testing.expect(recorder.seen.contains(.create_packed_store)); try std.testing.expect(recorder.seen.contains(.before_rename_no_replace)); // The script's own recorder still ran: chaining must ADD a listener, not // trade one for the other. `raw_frozen_before_packing` is written by // `publicationEvent` and by nothing else. try std.testing.expect(script.publication.raw_frozen_before_packing); } test "import-git reports parent sync durability uncertainty after publish while retaining the verified destination" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "durability-uncertain", false, .{ .fail_at = parent_sync_failure_point }, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(result.published_durability_uncertain); var destination = try tmp.dir.openDir(std.testing.io, "durability-uncertain", .{}); defer destination.close(std.testing.io); try std.testing.expect(exists(std.testing.io, destination, ".zit")); try std.testing.expect(!exists(std.testing.io, destination, ".zit-import-map.jsonl")); } test "import-git start session failure after stderr thread closes pipes and reaps exactly once" { var session = import_cli.ProductionGit.initForTest( std.testing.allocator, std.testing.io, std.testing.environ, &.{ helper_options.git_import_pipe_helper, "hold" }, ); defer session.deinit(); session.test_startup_failure = .after_stderr_thread; const dependencies = session.dependencies(.real_filesystem); try std.testing.expectError( error.InjectedStartFailure, dependencies.git.vtable.start_export(dependencies.git.context, "fixture.git"), ); try std.testing.expect(session.child == null); try std.testing.expect(session.stderr_thread == null); try std.testing.expect(session.stderr_file == null); try std.testing.expect(session.reaped); try std.testing.expectEqual(@as(usize, 1), session.reap_calls); } test "import-git rejects a replacement destination after rename without deleting it" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export, .replace_destination_after_rename = true, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "replacement-after-rename", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(std.mem.indexOf(u8, result.err, "PublishedDestinationReplaced") != null); try std.testing.expect(script.competitor_destination_intact); var replacement = try tmp.dir.openDir(std.testing.io, "replacement-after-rename", .{ .follow_symlinks = false, }); defer replacement.close(std.testing.io); try std.testing.expect(exists(std.testing.io, replacement, "replacement-sentinel")); try std.testing.expect(!script.temporary_sibling_left); } test "import-git reaps after termination failure and reports the lifecycle error" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var git: ScriptedGit = .{ .fast_export = malformed_export, .lifecycle_fault = .terminate, }; const result = runFixture(tmp, "fixture.git", "termination-failure", false, &git, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(std.mem.indexOf(u8, result.err, "InjectedLifecycleFailure") != null); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, "termination-failure"); } test "import-git accounts the peeled target retained for marked tags at the exact byte boundary" { const exact_limit = try retainedBytesForExport(marked_tag_export); var exact_tmp = std.testing.tmpDir(.{}); defer exact_tmp.cleanup(); var exact_git: ScriptedGit = .{ .fast_export = marked_tag_export, .limits = .{ .max_retained_bytes = exact_limit }, }; const exact = runFixture(exact_tmp, "fixture.git", "marked-tag-exact", false, &exact_git, .real_filesystem); try std.testing.expectEqual(cli.Exit.ok, exact.code); var exceeded_tmp = std.testing.tmpDir(.{}); defer exceeded_tmp.cleanup(); var exceeded_git: ScriptedGit = .{ .fast_export = marked_tag_export, .limits = .{ .max_retained_bytes = exact_limit - 1 }, }; const exceeded = runFixture(exceeded_tmp, "fixture.git", "marked-tag-exceeded", false, &exceeded_git, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, exceeded.code); try std.testing.expect(std.mem.indexOf(u8, exceeded.err, "ImporterRetainedByteLimitExceeded") != null); try expectNoPublication(std.testing.io, exceeded_tmp, "marked-tag-exceeded"); } /// A fast-export whose single commit lands `count` distinct paths. Wide enough, /// at the k the tree-mode side of the comparison pins, that tree emission /// really stores SUBMANIFEST artifacts -- without that, "the flat import stored /// none" would be true of both modes and would prove nothing. fn wideExport(gpa: std.mem.Allocator, count: usize) ![]u8 { var out: std.ArrayList(u8) = .empty; errdefer out.deinit(gpa); try out.appendSlice( gpa, "feature done\n" ++ "blob\n" ++ "mark :1\n" ++ "data 5\n" ++ "hello\n" ++ "commit refs/heads/main\n" ++ "mark :2\n" ++ "author A <[email protected]> 0 +0000\n" ++ "committer A <[email protected]> 0 +0000\n" ++ "data 4\n" ++ "init\n", ); for (0..count) |index| { const line = try std.fmt.allocPrint(gpa, "M 100644 :1 file{d:0>6}.txt\n", .{index}); defer gpa.free(line); try out.appendSlice(gpa, line); } try out.appendSlice(gpa, "\n" ++ "done\n"); return out.toOwnedSlice(gpa); } const ImportedSubmanifestState = struct { enable_submanifests: bool, submanifest_k: u8, seen_submanifest: bool, /// Whether ANY artifact in the produced store is a SUBMANIFEST. Read off /// the store itself rather than off the config frame, so the two statements /// can disagree and be caught doing it. submanifests_stored: bool, checkin_paths: usize, }; /// Everything this feature claims about a produced repository, read back from /// the repository: the four persisted sub-manifest settings, whether the store /// holds a SUBMANIFEST artifact, and the flattened path count of the one /// check-in. The path count is what lets the caller assert that the two modes /// imported the SAME history. fn readImportedSubmanifestState(io: std.Io, destination: std.Io.Dir) !ImportedSubmanifestState { const gpa = std.testing.allocator; var file = try destination.openFile(io, ".zit", .{}); defer file.close(io); const stat = try file.stat(io); var file_storage = storage.FileStorage.init(io, file, stat.size); var log = try logstore.LogStore.openReadOnly(gpa, file_storage.storageFile()); defer log.deinit(gpa); var owned = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult; defer owned.deinit(gpa); try std.testing.expect(!owned.config.submanifest_auto_retune); const rids = try log.ridsInPersistedOrder(gpa); defer gpa.free(rids); var submanifests: usize = 0; var checkins: usize = 0; var checkin_paths: usize = 0; for (rids) |rid| { const bytes = (try log.content(gpa, rid)) orelse continue; defer gpa.free(bytes); // A blob is not parseable as an artifact; skipping is the whole // classification here, so it must not be an error. var manifest = artifact.parse(gpa, bytes) catch continue; defer manifest.deinit(); switch (manifest.kind) { .submanifest => submanifests += 1, .manifest => { checkins += 1; checkin_paths = if (manifest.s.len != 0) manifest.s.len else manifest.f.len; }, else => {}, } } try std.testing.expectEqual(@as(usize, 1), checkins); return .{ .enable_submanifests = owned.config.enable_submanifests, .submanifest_k = owned.config.submanifest_k, .seen_submanifest = owned.config.seen_submanifest, .submanifests_stored = submanifests != 0, .checkin_paths = checkin_paths, }; } /// Every check-in manifest in the store carries only F cards, and no artifact /// in it is a SUBMANIFEST. This is the store-side reading of §8.1's "MUST NOT /// emit an S card"; it consults no flag and no config field. fn expectNoSCardsAnywhere(io: std.Io, destination: std.Io.Dir) !void { const gpa = std.testing.allocator; var file = try destination.openFile(io, ".zit", .{}); defer file.close(io); const stat = try file.stat(io); var file_storage = storage.FileStorage.init(io, file, stat.size); var log = try logstore.LogStore.openReadOnly(gpa, file_storage.storageFile()); defer log.deinit(gpa); const rids = try log.ridsInPersistedOrder(gpa); defer gpa.free(rids); for (rids) |rid| { const bytes = (try log.content(gpa, rid)) orelse continue; defer gpa.free(bytes); var manifest = artifact.parse(gpa, bytes) catch continue; defer manifest.deinit(); try std.testing.expect(manifest.kind != .submanifest); try std.testing.expectEqual(@as(usize, 0), manifest.s.len); } } test "import-git --no-submanifests leaves the destination at the spec-default config with no S cards" { const gpa = std.testing.allocator; // 96 paths at k=1 is comfortably past the point the canonical partition // yields more than one group, which the tree-mode half below proves by // actually storing SUBMANIFEST artifacts from this exact stream. const export_stream = try wideExport(gpa, 96); defer gpa.free(export_stream); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var flat_git: ScriptedGit = .{ .fast_export = export_stream }; const flat = runFixtureShaped(tmp, "fixture.git", "flat", false, &flat_git, .real_filesystem, .flat); try std.testing.expectEqual(cli.Exit.ok, flat.code); // "Do the default thing" reports nothing about sub-manifests. try std.testing.expect(std.mem.indexOf(u8, flat.err, "sub-manifest") == null); var tree_git: ScriptedGit = .{ .fast_export = export_stream }; const tree = runFixtureShaped(tmp, "fixture.git", "tree", false, &tree_git, .real_filesystem, .{ .tree = 1 }); try std.testing.expectEqual(cli.Exit.ok, tree.code); // The other side of the same gate. Flat's silence above only means // something if an unpinned tree import does report its analyzed k, so pin // that here too -- otherwise deleting the report entirely would leave both // halves green. var analyzed_git: ScriptedGit = .{ .fast_export = export_stream, .analyzed_submanifest_k = 3 }; const analyzed = runFixtureShaped(tmp, "fixture.git", "analyzed", false, &analyzed_git, .real_filesystem, .{ .tree = null }); try std.testing.expectEqual(cli.Exit.ok, analyzed.code); try std.testing.expect(std.mem.indexOf(u8, analyzed.err, "analyzed sub-manifest k=3") != null); var flat_dir = try tmp.dir.openDir(std.testing.io, "flat", .{}); defer flat_dir.close(std.testing.io); var tree_dir = try tmp.dir.openDir(std.testing.io, "tree", .{}); defer tree_dir.close(std.testing.io); const flat_state = try readImportedSubmanifestState(std.testing.io, flat_dir); const tree_state = try readImportedSubmanifestState(std.testing.io, tree_dir); // The tree half is asserted first because it is what gives the flat half // its meaning: this stream at this k really does produce sub-manifests, and // today's tree-mode config frame is pinned here as the contrast. try std.testing.expect(tree_state.submanifests_stored); try std.testing.expect(tree_state.enable_submanifests); try std.testing.expectEqual(@as(u8, 1), tree_state.submanifest_k); try std.testing.expect(tree_state.seen_submanifest); // The flat half: identical to a fresh `zit init` as far as sub-manifest // behavior goes. `submanifest_k = 0` is "not pinned", not "k of zero". try std.testing.expect(!flat_state.submanifests_stored); try std.testing.expect(!flat_state.enable_submanifests); try std.testing.expectEqual(@as(u8, 0), flat_state.submanifest_k); try std.testing.expect(!flat_state.seen_submanifest); try expectNoSCardsAnywhere(std.testing.io, flat_dir); // ... and it imported the same history: every path landed, as F cards. try std.testing.expectEqual(@as(usize, 96), flat_state.checkin_paths); try std.testing.expect(tree_state.checkin_paths < flat_state.checkin_paths); } // --------------------------------------------------------------------------- // Second generation: every successful import publishes a PACKED store. // // The transaction runs `writing_raw -> raw_frozen -> packing -> packed_frozen // -> verified -> published`. Generation 1 is written under its own name and is // never the thing that ships: it is frozen, packed into generation 2 through // the classifier's exact candidate-only drop set, read once more by // verification, and then deleted from staging before the directory is synced // and renamed. `DEST/.zit` is generation 2 and nothing else. // --------------------------------------------------------------------------- // The two generations must be distinct FILES, not one file rewritten. A single // name would make "pack generation 1 into generation 2" a truncating in-place // rewrite with no recoverable source, which is the shape this transaction // exists to avoid. test "import-git second generation uses a file distinct from the raw generation" { try std.testing.expect(!std.mem.eql( u8, import_cli.raw_generation_name, import_cli.published_generation_name, )); try std.testing.expectEqualStrings(".zit", import_cli.published_generation_name); } test "import-git second generation advances its publication stages one way only" { const io = std.testing.io; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var bundle = try import_cli.PublicationBundle.create(std.testing.allocator, io, tmp.dir, "one-way"); defer bundle.abort(); // Skipping ahead is rejected... try std.testing.expectError(error.PublicationStageOutOfOrder, bundle.advance(.packing)); try std.testing.expectEqual(import_cli.PublicationBundle.Stage.writing_raw, bundle.stage); // ...one step at a time is accepted... try bundle.advance(.raw_frozen); try bundle.advance(.packing); // ...repeating a stage is rejected... try std.testing.expectError(error.PublicationStageOutOfOrder, bundle.advance(.packing)); // ...and so is going back. try std.testing.expectError(error.PublicationStageOutOfOrder, bundle.advance(.raw_frozen)); try std.testing.expectEqual(import_cli.PublicationBundle.Stage.packing, bundle.stage); } test "import-git second generation publish rejects every state earlier than verified" { const io = std.testing.io; const early = [_]import_cli.PublicationBundle.Stage{ .writing_raw, .raw_frozen, .packing, .packed_frozen, }; for (early) |stage| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var bundle = try import_cli.PublicationBundle.create(std.testing.allocator, io, tmp.dir, "too-early"); defer bundle.abort(); bundle.stage = stage; try std.testing.expectError( error.PublicationNotVerified, bundle.publish("too-early", .real_filesystem), ); try std.testing.expect(!directoryExists(io, tmp.dir, "too-early")); } } test "import-git second generation publishes the packed generation and no raw generation" { const io = std.testing.io; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "second-generation", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.ok, result.code); var destination = try tmp.dir.openDir(io, "second-generation", .{}); defer destination.close(io); try std.testing.expect(exists(io, destination, import_cli.published_generation_name)); // The rename ships the whole staging directory, so a surviving generation 1 // would land in DEST. It must be gone before the commit point. try std.testing.expect(!exists(io, destination, import_cli.raw_generation_name)); // The default publication is the repository and its checkout record -- // instrumentation leaves staging with the raw generation unless // --provenance kept it. try std.testing.expect(!exists(io, destination, ".zit-import-map.jsonl")); try std.testing.expect(!exists(io, destination, "refs")); // Stage ordering, observed rather than asserted from the source. try std.testing.expect(script.publication.raw_frozen_before_packing); try std.testing.expect(script.publication.packed_generation_created); try std.testing.expect(script.publication.packed_before_verify); try std.testing.expect(script.publication.both_generations_present_before_discard); try std.testing.expect(script.publication.raw_generation_discarded); try std.testing.expect(script.publication.discard_before_rename); try std.testing.expect(script.publication.verify_before_publish); // The published store is a complete log in its own right. var store_file = try destination.openFile(io, import_cli.published_generation_name, .{}); defer store_file.close(io); const stat = try store_file.stat(io); var file_storage = storage.FileStorage.init(io, store_file, stat.size); var log = try logstore.LogStore.openReadOnly(std.testing.allocator, file_storage.storageFile()); defer log.deinit(std.testing.allocator); try std.testing.expect(log.entryCount() != 0); } // The pack report is the only place an operator learns that publication is // packed at all, and the only externally visible statement of how many mirror // artifacts the classifier authorized dropping. A stream with no Fossil mirror // pair must report zero -- fail-closed, stated out loud. test "import-git second generation reports its pack census and drops nothing without a mirror pair" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var git: ScriptedGit = .{ .fast_export = valid_export }; const result = runFixture(tmp, "fixture.git", "census", false, &git, .real_filesystem); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expect(std.mem.indexOf(u8, result.err, "import-git: packed ") != null); try std.testing.expect(std.mem.indexOf(u8, result.err, "dropped 0 mirror artifacts") != null); } // The eleven packing-stage boundaries used to be swept again here, from a // SIXTH `FailurePoint` array local to this test. Deleted rather than kept: all // eleven are in `pre_rename_failure_points`, and that sweep asserts a strict // superset of what this one did (`Exit.failure`, `launched == 1`, `reaped == 1`, // `expectNoPublication`, and `!temporary_sibling_left`, against this test's // failure/reaped/no-publication). A duplicate list is not extra coverage; it is // a second registration site the partition gate above cannot see, which is the // exact drift that gate exists to stop. // A competitor that appears at the commit point must survive untouched even // though the transaction now has two generations to unwind, and the staging // sibling holding BOTH of them must still be removed. test "import-git second generation never clobbers a competing destination and leaves no sibling" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export, .competing_destination_at_publish = true, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "packed-raced", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(script.competitor_destination_intact); try std.testing.expect(!script.temporary_sibling_left); // The competitor is a bare directory the script created: it must still be // bare, with neither generation written into it. var competitor = try tmp.dir.openDir(std.testing.io, "packed-raced", .{}); defer competitor.close(std.testing.io); try std.testing.expect(!exists(std.testing.io, competitor, import_cli.published_generation_name)); try std.testing.expect(!exists(std.testing.io, competitor, import_cli.raw_generation_name)); } // The manifest check is what makes "only generation 2 is published" a property // of the DIRECTORY at the instant it is renamed, rather than a property of the // statement order that got it there. Stand in for a future edit that writes an // intermediate file and forgets to remove it. test "import-git second generation refuses to publish staging holding anything unmanifested" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export, .stray_staging_file_after_discard = true, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "stray", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.failure, result.code); // The stray arrives AFTER the discard, so the transaction was otherwise // complete: only the manifest check stands between it and the rename. try std.testing.expect(script.publication.raw_generation_discarded); try expectNoPublication(std.testing.io, tmp, "stray"); } // `.verified` is both the pre-discard and the post-discard stage, so the stage // alone cannot say whether generation 1 left staging. The commit point checks // the fact directly. test "import-git second generation publish rejects a bundle still holding the raw generation" { const io = std.testing.io; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var bundle = try import_cli.PublicationBundle.create(std.testing.allocator, io, tmp.dir, "raw-held"); defer bundle.abort(); bundle.stage = .verified; bundle.raw_present = true; try std.testing.expectError( error.PublicationRawGenerationPresent, bundle.publish("raw-held", .real_filesystem), ); try std.testing.expect(!directoryExists(io, tmp.dir, "raw-held")); // The gate is the raw generation and nothing else: once it is gone, the // same bundle at the same stage publishes. bundle.raw_present = false; try bundle.publish("raw-held", .real_filesystem); try std.testing.expect(directoryExists(io, tmp.dir, "raw-held")); } // =========================================================================== // Task 6 Step 1 -- the eleven corruption fixtures. // // RED BY CONSTRUCTION. The ordered verification pipeline of Step 2 does not // exist: `verifyFrozen` today reopens both generations headers-only, reads the // checkout, and opens the map without reading a byte of it. So every fixture // below injects a real divergence between the frozen generations and what the // import decided, and then asserts the ONE diagnostic that names its clause. // // Two assertions per fixture, and both are load-bearing: // // 1. `script.verifier_fault_injected` -- the corruption actually happened. // Without it a fixture whose injection silently no-ops would go green the // moment the pipeline lands, which is the failure mode this suite exists // to prevent. A fixture that passes for the wrong reason is worse than a // missing fixture. // 2. the clause's own diagnostic in stderr -- not merely `.failure`. A // verifier that refuses generically is nearly as bad as none: the // operator cannot tell a dropped artifact from a tampered provenance // line, and the whole point of Task 6 is that Tasks 3-5 DELETE artifacts // from a repository and publish the result. // // The diagnostic tokens below are the contract Step 2 implements. They are // deliberately clause names, not error names: `verifier:` plus the invariant. // =========================================================================== const verifier_clause_prefix = "import-git: verifier: "; /// Two files, distinct author and committer, a two-parent merge, a lightweight /// tag and an annotated tag. One stream so that ten of the eleven fixtures /// differ only in what they corrupt, never in what they imported. const verifier_export = "feature done\n" ++ "blob\n" ++ "mark :1\n" ++ "data 5\n" ++ "alpha\n" ++ "blob\n" ++ "mark :2\n" ++ "data 4\n" ++ "beta\n" ++ "commit refs/heads/main\n" ++ "mark :3\n" ++ "original-oid 1111111111111111111111111111111111111111\n" ++ "author Author One <[email protected]> 1700000000 +0000\n" ++ "committer Committer Two <[email protected]> 1700000100 +0000\n" ++ "data 4\n" ++ "root\n" ++ "M 100644 :1 a.txt\n" ++ "M 100644 :2 b.txt\n" ++ "\n" ++ "blob\n" ++ "mark :4\n" ++ "data 5\n" ++ "gamma\n" ++ "commit refs/heads/side\n" ++ "mark :5\n" ++ "original-oid 2222222222222222222222222222222222222222\n" ++ "author Author One <[email protected]> 1700000200 +0000\n" ++ "committer Committer Two <[email protected]> 1700000300 +0000\n" ++ "data 4\n" ++ "side\n" ++ "from :3\n" ++ "M 100644 :4 b.txt\n" ++ "\n" ++ "commit refs/heads/main\n" ++ "mark :6\n" ++ "original-oid 3333333333333333333333333333333333333333\n" ++ "author Author One <[email protected]> 1700000400 +0000\n" ++ "committer Committer Two <[email protected]> 1700000500 +0000\n" ++ "data 5\n" ++ "merge\n" ++ "from :3\n" ++ "merge :5\n" ++ "\n" ++ "reset refs/tags/light\n" ++ "from :3\n" ++ "\n" ++ "tag v1\n" ++ "mark :7\n" ++ "from :6\n" ++ "tagger Author One <[email protected]> 1700000600 +0000\n" ++ "data 0\n" ++ "\n" ++ "done\n"; fn appendFmt(out: *std.ArrayList(u8), gpa: std.mem.Allocator, comptime fmt: []const u8, args: anytype) !void { const rendered = try std.fmt.allocPrint(gpa, fmt, args); defer gpa.free(rendered); try out.appendSlice(gpa, rendered); } /// A stream carrying a VALID Fossil mirror pair, so that the drop set is not /// empty and `retained_omitted_artifact` has a referent. /// /// Built at runtime because two of the four blobs are hashes of the other two: /// `manifest.uuid` must name the sterilized `manifest` bytes, and those bytes /// must carry an F card naming the tree file. A string literal cannot close /// that loop. Gates 1-8 of `classifyMirror` are what this satisfies -- one /// regular-mode candidate set, a canonical uuid over the sterilized manifest, /// a flat check-in whose file set is the candidate-free tree, no parents on a /// root commit, a D card at the committer instant, and a well-formed tags file. fn buildMirrorExport(gpa: std.mem.Allocator) ![]u8 { const sterilizer = "# Remove this line to create a well-formed Fossil manifest.\n"; const tree_content = "alpha\n"; const tags_content = "branch trunk\ntag trunk\n"; // 1700000000 as a Fossil D card; the export's committer instant must be // the same second or gate 7 retains the pair. const instant = "2023-11-14T22:13:20"; var name_buf: [64]u8 = undefined; const tree_hash = artifact.artifactName(.sha1, tree_content, &name_buf); var manifest: artifact.Manifest = .{ .arena_state = std.heap.ArenaAllocator.init(gpa), .kind = .manifest, .a = null, .b = null, .c = .{ .comment = "mirror" }, .d = .{ .timestamp = instant }, .e = null, .g = null, .h = null, .i = null, .k = null, .l = null, .n = null, .r = null, .u = .{ .login = "mirror" }, .z = null, .w = null, .f = &.{}, .j = &.{}, .m = &.{}, .p = &.{}, .q = &.{}, .s = &.{}, .t = &.{}, }; defer manifest.deinit(); var cards = [_]artifact.FCard{.{ .filename = "a.txt", .hash = try manifest.arena_state.allocator().dupe(u8, tree_hash), .perm = null, .old_name = null, .mode = .regular, }}; manifest.f = &cards; const manifest_bytes = try artifact.create(&manifest, gpa); defer gpa.free(manifest_bytes); var uuid_buf: [64]u8 = undefined; const uuid = artifact.artifactName(.sha1, manifest_bytes, &uuid_buf); var out: std.ArrayList(u8) = .empty; errdefer out.deinit(gpa); try appendFmt(&out, gpa, "feature done\nblob\nmark :1\ndata {d}\n{s}\n", .{ tree_content.len, tree_content }); try appendFmt(&out, gpa, "blob\nmark :2\ndata {d}\n{s}{s}\n", .{ manifest_bytes.len + sterilizer.len, manifest_bytes, sterilizer }); try appendFmt(&out, gpa, "blob\nmark :3\ndata {d}\n{s}\n\n", .{ uuid.len + 1, uuid }); try appendFmt(&out, gpa, "blob\nmark :4\ndata {d}\n{s}\n", .{ tags_content.len, tags_content }); try out.appendSlice( gpa, "commit refs/heads/main\n" ++ "mark :5\n" ++ "original-oid 4444444444444444444444444444444444444444\n" ++ "author Mirror <[email protected]> 1700000000 +0000\n" ++ "committer Mirror <[email protected]> 1700000000 +0000\n" ++ "data 6\nmirror\n" ++ "M 100644 :1 a.txt\n" ++ "M 100644 :2 manifest\n" ++ "M 100644 :3 manifest.uuid\n" ++ "M 100644 :4 manifest.tags\n" ++ "\ndone\n", ); return out.toOwnedSlice(gpa); } /// One corruption fixture. Runs the whole transaction with `fault` injected at /// `.verify_raw_store`, then requires that the named clause -- and only the /// named clause -- is what refused the rename. fn expectVerifierClause( destination: []const u8, source_export: []const u8, fault: import_cli.CommandScript.VerifierFault, clause: []const u8, ) !void { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = source_export, .verifier_fault = fault, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, destination, false, .real_filesystem, &result_cap); // The corruption happened. Asserted first and separately, so a fixture // whose injection no-ops reports THAT and never masquerades as a verifier // result. if (!script.verifier_fault_injected) { std.debug.print( "\n [{s}] INJECTION DID NOT HAPPEN -- the fixture corrupted nothing, so it says nothing about `{s}`.\n", .{ destination, clause }, ); return error.VerifierFaultNotInjected; } // It refused for this reason. Checked before the exit code, because // "refused generically" and "refused for the right reason" are different // results and only the second one proves a clause exists. if (std.mem.indexOf(u8, result.err, clause) == null) { std.debug.print( "\n [{s}] MISSING CLAUSE `{s}`\n exit: {s}\n published: {}\n stderr: {s}\n", .{ destination, clause, @tagName(result.code), directoryExists(std.testing.io, tmp.dir, destination), result.err }, ); return error.VerifierClauseMissing; } // And ONLY that clause. The pipeline returns at its first refusal, so a // second `verifier:` line would mean the fixture tripped something else on // the way and this assertion is what keeps "the clause appears" from // drifting into "the clause appears somewhere among several". var scan: usize = 0; var lines: usize = 0; while (std.mem.indexOfPos(u8, result.err, scan, verifier_clause_prefix)) |at| { lines += 1; scan = at + verifier_clause_prefix.len; } if (lines != 1) { std.debug.print( "\n [{s}] EXPECTED EXACTLY ONE VERIFIER CLAUSE, SAW {d}\n stderr: {s}\n", .{ destination, lines, result.err }, ); return error.VerifierClauseAmbiguous; } // The verifier refused, and DEST was never created. try std.testing.expectEqual(cli.Exit.failure, result.code); try expectNoPublication(std.testing.io, tmp, destination); } test "import-git exhaustive verifier rejects an omitted artifact retained in the packed generation" { const mirror_export = try buildMirrorExport(std.testing.allocator); defer std.testing.allocator.free(mirror_export); // Guard the fixture's own premise before asserting anything about the // verifier: with an empty drop set there is nothing to retain, and the // corruption would be unexpressible rather than undetected. { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = mirror_export }); var census_cap: import_cli.ScriptCapture = .{}; const census = scripted(tmp, &script, "mirror-census", false, .real_filesystem, &census_cap); try std.testing.expectEqual(cli.Exit.ok, census.code); try std.testing.expect(std.mem.indexOf(u8, census.err, "dropped 0 mirror artifacts") == null); } try expectVerifierClause( "verifier-retained-omitted", mirror_export, .retained_omitted_artifact, verifier_clause_prefix ++ "retained-omitted-artifact", ); } test "import-git exhaustive verifier rejects a required artifact dropped from the packed generation" { try expectVerifierClause( "verifier-dropped-required", verifier_export, .dropped_required_artifact, verifier_clause_prefix ++ "dropped-required-artifact", ); } test "import-git exhaustive verifier rejects an artifact added to the packed generation" { try expectVerifierClause( "verifier-added", verifier_export, .added_artifact, verifier_clause_prefix ++ "added-artifact", ); } test "import-git exhaustive verifier rejects changed artifact bytes under an unchanged name" { try expectVerifierClause( "verifier-changed-bytes", verifier_export, .changed_artifact_bytes, verifier_clause_prefix ++ "artifact-rehash-mismatch", ); } test "import-git exhaustive verifier rejects a delta frame recorded against the wrong base" { try expectVerifierClause( "verifier-wrong-delta-base", verifier_export, .wrong_delta_base, verifier_clause_prefix ++ "delta-base-mismatch", ); } test "import-git exhaustive verifier rejects an artifact-set-preserving tree change" { try expectVerifierClause( "verifier-tree-change", verifier_export, .tree_change_preserving_artifacts, verifier_clause_prefix ++ "normalized-tree-digest-mismatch", ); } test "import-git exhaustive verifier rejects reordered check-in parents" { try expectVerifierClause( "verifier-parent-reorder", verifier_export, .parent_reorder, verifier_clause_prefix ++ "parent-order-mismatch", ); } test "import-git exhaustive verifier rejects a retargeted branch ref" { try expectVerifierClause( "verifier-ref-retarget", verifier_export, .ref_retarget, verifier_clause_prefix ++ "ref-target-mismatch", ); } test "import-git exhaustive verifier rejects a changed tag target or kind" { try expectVerifierClause( "verifier-tag-change", verifier_export, .tag_change, verifier_clause_prefix ++ "tag-mismatch", ); } test "import-git exhaustive verifier rejects changed attribution in the finalized map" { try expectVerifierClause( "verifier-attribution", verifier_export, .attribution_change, verifier_clause_prefix ++ "attribution-mismatch", ); } test "import-git exhaustive verifier rejects a tampered provenance line" { try expectVerifierClause( "verifier-provenance", verifier_export, .provenance_tamper, verifier_clause_prefix ++ "provenance-digest-mismatch", ); } // The negative control. Same stream, no injection: the transaction must reach // `verified` and publish, so no fixture above can be passing merely because // this export is unimportable. test "import-git exhaustive verifier admits the uncorrupted fixture" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = verifier_export, }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "verifier-clean", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expect(!script.verifier_fault_injected); try std.testing.expect(script.publication.verify_before_publish); try std.testing.expect(std.mem.indexOf(u8, result.err, "import-git: verifier: ") == null); try std.testing.expect(directoryExists(std.testing.io, tmp.dir, "verifier-clean")); } // The fixpoint, proved OUTSIDE the transaction and deliberately so. // // Design item 6 makes the B-to-C proof a qualification layer, not a runtime // one: the production import creates generation 1 and generation 2 and stops, // because a third generation is an extra full pack of the whole repository on // every import to re-establish a property that does not change between runs. // This is where that property is established instead -- on a real published // import, over a stream carrying a validated mirror pair, so the pack whose // determinism is under test is one that actually dropped something. test "import-git pack is idempotent: repacking a published generation is byte-identical" { const io = std.testing.io; const gpa = std.testing.allocator; const mirror_export = try buildMirrorExport(gpa); defer gpa.free(mirror_export); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = mirror_export }); var result_cap: import_cli.ScriptCapture = .{}; const result = scripted(tmp, &script, "fixpoint", false, .real_filesystem, &result_cap); try std.testing.expectEqual(cli.Exit.ok, result.code); // The premise: generation 2 is a pack that removed something. A fixpoint // over a pack that dropped nothing would prove far less. try std.testing.expect(std.mem.indexOf(u8, result.err, "dropped 0 mirror artifacts") == null); var published = try tmp.dir.openDir(io, "fixpoint", .{}); defer published.close(io); var b_file = try published.openFile(io, import_cli.published_generation_name, .{ .mode = .read_only }); defer b_file.close(io); var b_storage = storage.FileStorage.init(io, b_file, (try b_file.stat(io)).size); var b = try logstore.LogStore.openReadOnly(gpa, b_storage.storageFile()); defer b.deinit(gpa); // C, disposable, in the test's own directory: nothing about this generation // is ever published, and the import that produced B never created it. var c_file = try tmp.dir.createFile(io, "generation-c", .{ .read = true, .exclusive = true }); defer c_file.close(io); var c_storage = storage.FileStorage.init(io, c_file, 0); var c = try logstore.LogStore.open(gpa, c_storage.storageFile(), b.hash_kind); defer c.deinit(gpa); // No filter and no extra shuns: everything B holds is content the import // already proved it should keep, so C differs from B only if packing is // not deterministic over its own output. const stats = try pack.packLog(gpa, &b, &c, pack.Filter.keep_all, &.{}); try std.testing.expectEqual(@as(usize, 0), stats.dropped); const b_bytes = try published.readFileAlloc(io, import_cli.published_generation_name, gpa, .limited(1 << 24)); defer gpa.free(b_bytes); const c_bytes = try tmp.dir.readFileAlloc(io, "generation-c", gpa, .limited(1 << 24)); defer gpa.free(c_bytes); try std.testing.expectEqualSlices(u8, b_bytes, c_bytes); }