Artifact 05cd075eecd88079500fc360cce2db2143eb2d1e2a298612bc18afaf10d1af39:
- File
src/git_import_cli_test.zig
— part of check-in
[d189b0da1d]
at
2026-08-03 03:08:23
on branch sub-manifest
— import-git: classify Fossil mirror metadata as droppable
A Fossil->Git mirror carries root manifest/manifest.uuid/manifest.tags that are not project content: wapp has 252 distinct manifest blobs across 252 commits while its native history tracks no such file. The classifier proves per commit that those bytes are reconstructible from what the import already holds, so packing can drop them.
Fail-closed and cryptographic: stripped-manifest hash equals the uuid, F cards equal the tree, P cards equal validated parents, D card equals the committer instant. A real top-level file named manifest cannot satisfy that by chance, and any inconclusive signal retains every present candidate -- never a strict subset.
Three corrections to the design, all established against the corpus: presence is per-repository (fossil's manifest setting emits each of the three independently), gate 6 must exclude every present candidate since F cards list none of them, and gate 3 must strip the sterilizer line the export appends or the uuid never matches. update.zig's manifest_paths and sterilizer are now public rather than duplicated -- it already had all three names and the exact trailer.
Measured on the pinned wapp corpus: 252/252 commits validate, drop set 512 = 252 manifest + 252 manifest.uuid + 8 distinct manifest.tags.
test 3461/3461, gate, e2e 716/716, git-import-e2e 11/11, dst 6x1000. (user: dmestas, size: 70243) [annotate] [blame] [check-ins using]
//! RED coverage for Task 10's transactional `zit import-git` command. //! //! This file deliberately names the smallest testable production seam that does //! not execute a shell command or contact a remote: //! //! `git_import_cli.run(gpa, io, request, streams, dependencies) -> cli.Exit` //! //! `dependencies` supplies a `ChildFactory`/`ChildSession` and filesystem hooks. //! A `ChildSession` must expose its stdout reader plus `terminate`, `drainStderr`, //! and `reap`; each lifecycle operation is observable in the supplied fake. The //! real CLI dispatch will construct OS-backed dependencies. The filesystem hook //! is only needed to deterministically fail publication flush/close/sync/rename //! boundaries; ordinary scenarios exercise a real `std.testing.tmpDir`. //! //! The opt-in suite intentionally remains RED until Task 10 provides command //! preflight, real-pipe lifecycle, and publication-boundary fault seams. It is //! not part of `zig build test`. const std = @import("std"); const builtin = @import("builtin"); const cli = @import("cli.zig"); const fast_export = @import("zit").git_fast_export; const import_cli = @import("git_import_cli.zig"); const checkout = @import("zit").checkout; const artifact = @import("zit").artifact; const git_import = @import("zit").git_import; const logstore = @import("zit").logstore; const storage = @import("zit").storage; const 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 { const Capture = struct { var out_storage: [4096]u8 = undefined; var err_storage: [4096]u8 = undefined; }; var out = std.Io.Writer.fixed(&Capture.out_storage); var err = std.Io.Writer.fixed(&Capture.err_storage); const streams: cli.Streams = .{ .out = &out, .err = &err }; const code = import_cli.run( std.testing.allocator, std.testing.io, .{ .source = source, .destination_parent = tmp.dir, .destination_name = destination, .submanifest = submanifest, .attribution = .committer, .default_branch = null, .quiet = quiet, }, 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, }); 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; } 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; } fn expectNoPublication(io: std.Io, tmp: std.testing.TmpDir, destination: []const u8) !void { try std.testing.expect(!directoryExists(io, tmp.dir, destination)); try std.testing.expect(!exists(io, tmp.dir, ".zit-import-map.jsonl")); var entries = tmp.dir.iterate(); while (try entries.next(io)) |entry| { try std.testing.expect(!std.mem.startsWith(u8, entry.name, ".zit-import-tmp-")); } } test "import-git 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 = runFixture( tmp, "fixture.git", "imported", false, &git, .real_filesystem, ); 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); try std.testing.expectEqualStrings("main", 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 = runFixture(tmp, "fixture.git", "with-remote", false, &git, .real_filesystem); 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 809fa79c883c54adff3a115e235f83f60327e4d2\n" ++ "sym-refs/remotes/origin/main 809fa79c883c54adff3a115e235f83f60327e4d2\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"); } 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_store }, ); 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"); } test "import-git removes all temporary output when publication flush or rename fails" { const failures = [_]import_cli.PublicationHooks.FailurePoint{ .flush_store, .sync_map, .rename_no_replace }; for (failures) |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); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, "publication-failed"); } } test "import-git writes only a machine-readable destination to stdout and progress to stderr" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var git: ScriptedGit = .{ .fast_export = valid_export, .stderr = "git progress\n" }; const result = runFixture(tmp, "fixture.git", "machine", false, &git, .real_filesystem); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expectEqualStrings("machine\n", result.out); try std.testing.expect(std.mem.indexOf(u8, result.err, "git progress") != null); try std.testing.expect(std.mem.indexOf(u8, result.out, "progress") == null); } test "import-git --quiet suppresses progress but not diagnostics" { var quiet_tmp = std.testing.tmpDir(.{}); defer quiet_tmp.cleanup(); var quiet_git: ScriptedGit = .{ .fast_export = valid_export, .stderr = "git progress\n" }; const quiet = runFixture(quiet_tmp, "fixture.git", "quiet", true, &quiet_git, .real_filesystem); try std.testing.expectEqual(cli.Exit.ok, quiet.code); try std.testing.expectEqualStrings("quiet\n", quiet.out); try std.testing.expectEqualStrings("", quiet.err); var error_tmp = std.testing.tmpDir(.{}); defer error_tmp.cleanup(); var error_git: ScriptedGit = .{ .fast_export = malformed_export, .stderr = "fatal fixture diagnostic\n" }; const failed = runFixture(error_tmp, "fixture.git", "quiet-error", true, &error_git, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, failed.code); try std.testing.expectEqualStrings("", failed.out); try std.testing.expect(std.mem.indexOf(u8, failed.err, "fatal fixture diagnostic") != null); } // The remaining tests specify the Task 10 seam not yet present in // `git_import_cli`: CommandScript records every preflight/export invocation, // supplies bounded stdout/stderr streams, and exposes deterministic // cancellation and publication-boundary faults. Production dispatch constructs // the OS adapter; tests use no shell or network. fn scripted( tmp: std.testing.TmpDir, script: *import_cli.CommandScript, destination: []const u8, quiet: bool, fault: import_cli.PublicationHooks, ) import_cli.ScriptResult { return import_cli.runScripted( std.testing.allocator, std.testing.io, .{ .source = "fixture.git", .destination_parent = tmp.dir, .destination_name = destination, .submanifest = .{ .tree = 5 }, .attribution = .committer, .default_branch = null, .quiet = quiet, }, script, fault, ); } const ProcessResult = struct { code: cli.Exit, out: []const u8, err: []const u8, exit_code: u8, reaped: bool, reap_calls: usize, child_gone: bool, stderr_bytes_drained: usize, retained_diagnostic_len: usize, }; /// Drives the real ProductionGit session against the build-wired helper. The /// only substitution is argv; spawning, pipe adapters, concurrent diagnostic /// drain, terminate, join, and wait are the production implementation. fn runProductionPipe( tmp: std.testing.TmpDir, destination: []const u8, mode: []const u8, ) !ProcessResult { var threaded = std.Io.Threaded.init(std.testing.allocator, .{}); defer threaded.deinit(); const io = threaded.io(); var session = import_cli.ProductionGit.initForTest( std.testing.allocator, io, &.{ helper_options.git_import_pipe_helper, mode }, ); const Capture = struct { var out_storage: [4096]u8 = undefined; var err_storage: [4096]u8 = undefined; }; var out = std.Io.Writer.fixed(&Capture.out_storage); var err = std.Io.Writer.fixed(&Capture.err_storage); const code = import_cli.run( std.testing.allocator, io, .{ .source = "fixture.git", .destination_parent = tmp.dir, .destination_name = destination, }, .{ .out = &out, .err = &err }, session.dependencies(.real_filesystem), ); return .{ .code = code, .out = out.buffered(), .err = err.buffered(), .exit_code = session.exit_code, .reaped = session.reaped, .child_gone = session.child == null, .stderr_bytes_drained = session.stderr_bytes_drained, .reap_calls = session.reap_calls, .retained_diagnostic_len = session.stderr_ring_len, }; } fn runGit(io: std.Io, argv: []const []const u8) !void { const result = try std.process.run(std.testing.allocator, io, .{ .argv = argv }); defer std.testing.allocator.free(result.stdout); defer std.testing.allocator.free(result.stderr); switch (result.term) { .exited => |code| if (code == 0) return, else => {}, } return error.GitSetupFailed; } fn writeFixtureFile(io: std.Io, dir: std.Io.Dir, path: []const u8, bytes: []const u8) !void { var file = try dir.createFile(io, path, .{ .read = true, .truncate = true }); defer file.close(io); try file.writeStreamingAll(io, bytes); } fn runProductionPreflight( tmp: std.testing.TmpDir, source: []const u8, destination: []const u8, ) cli.Exit { const Capture = struct { var out_storage: [1024]u8 = undefined; var err_storage: [1024]u8 = undefined; }; var out = std.Io.Writer.fixed(&Capture.out_storage); var err = std.Io.Writer.fixed(&Capture.err_storage); var session = import_cli.ProductionGit.init(std.testing.allocator, std.testing.io); defer session.deinit(); return import_cli.run( std.testing.allocator, std.testing.io, .{ .source = source, .destination_parent = tmp.dir, .destination_name = destination, }, .{ .out = &out, .err = &err }, session.dependencies(.real_filesystem), ); } test "import-git parser leaves k unset for analysis and is a real dispatched verb" { // Absent means "analyze", not "5": the parser must not manufacture a pin // the runner would then be unable to distinguish from an operator's. const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" }); try std.testing.expectEqual(@as(?u4, null), defaulted.submanifest_k); try std.testing.expectEqual(cli.Verb.import_git, cli.verbOf("import-git")); try std.testing.expect(!cli.isPlanned("import-git")); const parsed = try cli.parseImportGitArgs(&.{ "--submanifest-k", "7", "--attribution", "author", "--default-branch", "main", "--quiet", "fixture.git", "dest", }); try std.testing.expectEqual(@as(?u4, 7), parsed.submanifest_k); try std.testing.expectEqual(.author, parsed.attribution); try std.testing.expectEqualStrings("main", parsed.default_branch.?); try std.testing.expect(parsed.quiet); const min_k = try cli.parseImportGitArgs(&.{ "--submanifest-k", "1", "fixture.git", "dest" }); try std.testing.expectEqual(@as(?u4, 1), min_k.submanifest_k); const max_k = try cli.parseImportGitArgs(&.{ "--submanifest-k", "12", "fixture.git", "dest" }); try std.testing.expectEqual(@as(?u4, 12), max_k.submanifest_k); for ([_][]const u8{ "0", "13", "14", "15" }) |value| { try std.testing.expectError( cli.ImportGitParseError.InvalidSubmanifestK, cli.parseImportGitArgs(&.{ "--submanifest-k", value, "fixture.git", "dest" }), ); } } test "import-git parser accepts --no-submanifests and refuses it paired with a pinned k" { const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" }); try std.testing.expect(!defaulted.no_submanifests); // The flag declines the mechanism; it does not manufacture a k, and it must // not be confusable with one -- `submanifest_k` stays absent. const flat = try cli.parseImportGitArgs(&.{ "--no-submanifests", "fixture.git", "dest" }); try std.testing.expect(flat.no_submanifests); try std.testing.expectEqual(@as(?u4, null), flat.submanifest_k); // BOTH orders. An in-arm check would only fire for whichever token happened // to be seen second, which is exactly the bug this pairing invites. try std.testing.expectError( cli.ImportGitParseError.ConflictingSubmanifestOptions, cli.parseImportGitArgs(&.{ "--no-submanifests", "--submanifest-k", "5", "fixture.git", "dest" }), ); try std.testing.expectError( cli.ImportGitParseError.ConflictingSubmanifestOptions, cli.parseImportGitArgs(&.{ "--submanifest-k", "5", "--no-submanifests", "fixture.git", "dest" }), ); // A malformed token still reports its own cause: the conflict is ranked // below `first_err`, so an invalid k is not masked by the pairing. try std.testing.expectError( cli.ImportGitParseError.InvalidSubmanifestK, cli.parseImportGitArgs(&.{ "--submanifest-k", "0", "--no-submanifests", "fixture.git", "dest" }), ); // Same shape as every other boolean flag on this verb. try std.testing.expectError( cli.ImportGitParseError.RepeatedOption, cli.parseImportGitArgs(&.{ "--no-submanifests", "--no-submanifests", "fixture.git", "dest" }), ); // Help wins over the conflict, so `--help` stays informative no matter what // else is on the line. const helped = try cli.parseImportGitArgs(&.{ "--no-submanifests", "--submanifest-k", "5", "--help" }); try std.testing.expect(helped.help); // It composes with every other behavioral flag; none of them interact. const composed = try cli.parseImportGitArgs(&.{ "--no-submanifests", "--refuse-signed", "--attribution", "author", "--default-branch", "trunk", "--quiet", "fixture.git", "dest", }); try std.testing.expect(composed.no_submanifests); try std.testing.expect(composed.refuse_signed); try std.testing.expectEqual(.author, composed.attribution); try std.testing.expectEqualStrings("trunk", composed.default_branch.?); try std.testing.expect(composed.quiet); } test "import-git parser exposes the export payload budget as an operator flag" { // Every ceiling an honest source can reach must be raisable without a // recompile. This one budget carries both the cumulative payload limit and // the derived per-commit file-command ceiling, so it is the only knob the // verb needs. const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" }); try std.testing.expectEqual(@as(?u64, null), defaulted.max_export_payload_bytes); const raised = try cli.parseImportGitArgs(&.{ "--max-export-payload-bytes", "68719476736", "fixture.git", "dest", }); try std.testing.expectEqual(@as(?u64, 68719476736), raised.max_export_payload_bytes); // Zero derives a per-commit ceiling of zero: a budget no import can ever // satisfy is a typo, not a policy. try std.testing.expectError( cli.ImportGitParseError.InvalidExportPayloadBytes, cli.parseImportGitArgs(&.{ "--max-export-payload-bytes", "0", "fixture.git", "dest" }), ); try std.testing.expectError( cli.ImportGitParseError.InvalidExportPayloadBytes, cli.parseImportGitArgs(&.{ "--max-export-payload-bytes", "many", "fixture.git", "dest" }), ); try std.testing.expectError( cli.ImportGitParseError.MissingOptionValue, cli.parseImportGitArgs(&.{ "fixture.git", "dest", "--max-export-payload-bytes" }), ); try std.testing.expectError( cli.ImportGitParseError.RepeatedOption, cli.parseImportGitArgs(&.{ "--max-export-payload-bytes", "1024", "--max-export-payload-bytes", "2048", "fixture.git", "dest", }), ); } /// Whether `token` appears anywhere in `argv`. Positional indices would encode /// the argv's shape, which is not what this asserts. fn argvHas(argv: []const []const u8, token: []const u8) bool { for (argv) |candidate| { if (std.mem.eql(u8, candidate, token)) return true; } return false; } test "import-git strips signatures by default and aborts only under --refuse-signed" { // Fossil has no signature card, so refusing by default withheld the import // without saving anything. The default must therefore strip. const defaulted = try cli.parseImportGitArgs(&.{ "fixture.git", "dest" }); try std.testing.expect(!defaulted.refuse_signed); const stripping = import_cli.exportArgv("fixture.git", defaulted.refuse_signed); try std.testing.expect(argvHas(&stripping, "--signed-commits=strip")); try std.testing.expect(argvHas(&stripping, "--signed-tags=strip")); try std.testing.expect(!argvHas(&stripping, "--signed-commits=abort")); try std.testing.expect(!argvHas(&stripping, "--signed-tags=abort")); // The opt-out restores the refusal, and nothing else about the argv moves. const opted_out = try cli.parseImportGitArgs(&.{ "--refuse-signed", "fixture.git", "dest" }); try std.testing.expect(opted_out.refuse_signed); const aborting = import_cli.exportArgv("fixture.git", opted_out.refuse_signed); try std.testing.expect(argvHas(&aborting, "--signed-commits=abort")); try std.testing.expect(argvHas(&aborting, "--signed-tags=abort")); try std.testing.expectEqual(stripping.len, aborting.len); for (stripping, aborting) |strip_token, abort_token| { if (std.mem.startsWith(u8, strip_token, "--signed-")) continue; try std.testing.expectEqualStrings(strip_token, abort_token); } // Same error shape as every other boolean flag: repeated is a usage error, // and the retired spelling is simply unknown. try std.testing.expectError( cli.ImportGitParseError.RepeatedOption, cli.parseImportGitArgs(&.{ "--refuse-signed", "--refuse-signed", "fixture.git", "dest" }), ); try std.testing.expectError( cli.ImportGitParseError.UnknownOption, cli.parseImportGitArgs(&.{ "--drop-signatures", "fixture.git", "dest" }), ); } test "import-git help exits zero and malformed argument forms exit usage without process launch" { const help = import_cli.dispatchForTest(&.{"--help"}); try std.testing.expectEqual(cli.Exit.ok, help.code); try std.testing.expectEqualStrings("", help.err); try std.testing.expect(std.mem.startsWith(u8, help.out, "usage: zit import-git SOURCE DEST")); // Substring checks are what let the previous hand transcription rot two // options behind the real help, so pin every option the usage line owns. for ([_][]const u8{ "--submanifest-k K | --no-submanifests", "--attribution", "--default-branch", "--refuse-signed", "--max-export-payload-bytes", "--quiet", }) |option| { try std.testing.expect(std.mem.indexOf(u8, help.out, option) != null); } const accepted_flat = import_cli.dispatchForTest(&.{ "--no-submanifests", "fixture.git", "dest" }); try std.testing.expectEqual(cli.Exit.failure, accepted_flat.code); try std.testing.expectEqual(@as(usize, 0), accepted_flat.lifecycle.launched); try std.testing.expectEqualStrings("test dispatch requires scripted dependencies\n", accepted_flat.err); const accepted_max = import_cli.dispatchForTest(&.{ "--submanifest-k", "12", "fixture.git", "dest" }); try std.testing.expectEqual(cli.Exit.failure, accepted_max.code); try std.testing.expectEqual(@as(usize, 0), accepted_max.lifecycle.launched); try std.testing.expectEqualStrings("test dispatch requires scripted dependencies\n", accepted_max.err); for ([_][]const []const u8{ &.{ "--submanifest-k", "0", "fixture.git", "dest" }, &.{ "--submanifest-k", "13", "fixture.git", "dest" }, &.{ "--submanifest-k", "14", "fixture.git", "dest" }, &.{ "--submanifest-k", "15", "fixture.git", "dest" }, &.{ "--attribution", "committerish", "fixture.git", "dest" }, // The conflicting pair exits `usage` in both orders and launches // nothing -- no process is spawned to discover an argument mistake. &.{ "--no-submanifests", "--submanifest-k", "5", "fixture.git", "dest" }, &.{ "--submanifest-k", "5", "--no-submanifests", "fixture.git", "dest" }, &.{ "--no-submanifests", "--no-submanifests", "fixture.git", "dest" }, &.{"fixture.git"}, &.{ "fixture.git", "dest", "extra" }, }) |args| { const usage = import_cli.dispatchForTest(args); try std.testing.expectEqual(cli.Exit.usage, usage.code); try std.testing.expectEqual(@as(usize, 0), usage.lifecycle.launched); try std.testing.expectEqualStrings("", usage.out); try std.testing.expect(usage.err.len != 0); } } test "import-git rejects missing Git non-repository shallow and partial sources before export" { const preflight_failures = [_]import_cli.CommandScript.Preflight{ .git_missing, .not_repository, .missing_objects, .shallow, .partial_promisor, }; for (preflight_failures) |preflight| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = preflight }); const result = scripted(tmp, &script, "rejected", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqual(@as(usize, 0), script.export_starts); try std.testing.expect(script.preflight_starts != 0); try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, "rejected"); } } test "import-git production preflight rejects absent non-repository missing-object shallow and partial sources" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var root_buf: [128]u8 = undefined; const root = try std.fmt.bufPrint(&root_buf, ".zig-cache/tmp/{s}", .{tmp.sub_path}); var source_buf: [160]u8 = undefined; const source = try std.fmt.bufPrint(&source_buf, "{s}/source", .{root}); // A non-existent -C path and an existing non-repository both execute real // ProductionGit query children and fail before an export can launch. try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "missing-git")); try tmp.dir.createDir(std.testing.io, "not-repository", .default_dir); var nonrepo_buf: [160]u8 = undefined; const nonrepo = try std.fmt.bufPrint(&nonrepo_buf, "{s}/not-repository", .{root}); try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, nonrepo, "not-repository-dest")); try expectNoPublication(std.testing.io, tmp, "missing-git"); try expectNoPublication(std.testing.io, tmp, "not-repository-dest"); try runGit(std.testing.io, &.{ "git", "init", source }); try runGit(std.testing.io, &.{ "git", "-C", source, "config", "user.name", "Zit Test" }); try runGit(std.testing.io, &.{ "git", "-C", source, "config", "user.email", "[email protected]" }); try runGit(std.testing.io, &.{ "git", "-C", source, "commit", "--allow-empty", "-m", "init" }); try runGit(std.testing.io, &.{ "git", "-C", source, "config", "extensions.partialClone", "origin" }); try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "partial")); try runGit(std.testing.io, &.{ "git", "-C", source, "config", "--unset", "extensions.partialClone" }); try writeFixtureFile(std.testing.io, tmp.dir, "source/.git/shallow", "0000000000000000000000000000000000000000\n"); try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "shallow")); try tmp.dir.deleteFile(std.testing.io, "source/.git/shallow"); try tmp.dir.deleteTree(std.testing.io, "source/.git/objects"); try std.testing.expectEqual(cli.Exit.failure, runProductionPreflight(tmp, source, "missing-objects")); try expectNoPublication(std.testing.io, tmp, "partial"); try expectNoPublication(std.testing.io, tmp, "shallow"); try expectNoPublication(std.testing.io, tmp, "missing-objects"); } test "import-git records detached HEAD and refuses it unless default branch is explicit" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .detached_head, .fast_export = valid_export }); const rejected = scripted(tmp, &script, "detached", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, rejected.code); try std.testing.expectEqual(@as(usize, 0), script.export_starts); try expectNoPublication(std.testing.io, tmp, "detached"); script.default_branch = "main"; const accepted = scripted(tmp, &script, "detached-with-branch", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.ok, accepted.code); try std.testing.expect(script.detached_head_recorded); } test "import-git starts fast-export with the exact safe argv and disabled prompt and lazy fetch" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export }); const result = scripted(tmp, &script, "argv", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expectEqualStrings( "git -C fixture.git fast-export --all --show-original-ids --mark-tags --signed-commits=strip --signed-tags=strip --use-done-feature", script.exportCommandLine(), ); try std.testing.expectEqualStrings("0", script.environment.get("GIT_TERMINAL_PROMPT").?); try std.testing.expectEqualStrings("1", script.environment.get("GIT_NO_LAZY_FETCH").?); } test "import-git drains real OS-pipe backpressure concurrently while stdout remains active" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const result = try runProductionPipe(tmp, "flood", "success"); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expectEqual(@as(usize, 8 * 1024 * 1024), result.stderr_bytes_drained); try std.testing.expectEqual(@as(usize, 4096), result.retained_diagnostic_len); try std.testing.expect(result.err.len < 4096); try std.testing.expect(std.mem.indexOf(u8, result.err, "imported 'fixture.git'") != null); try std.testing.expectEqual(@as(u8, 0), result.exit_code); try std.testing.expect(result.reaped); try std.testing.expect(result.child_gone); } test "import-git cancellation terminates a real child then joins and reaps its actual status once" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const result = try runProductionPipe(tmp, "cancelled", "hold"); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(result.exit_code != 0); try std.testing.expect(result.err.len < 4096); try std.testing.expect(std.mem.indexOf(u8, result.err, "Malformed") != null); try std.testing.expect(std.mem.indexOf(u8, result.err, "GitExportFailed") == null); try std.testing.expect(result.reaped); try std.testing.expect(result.child_gone); try expectNoPublication(std.testing.io, tmp, "cancelled"); } test "import-git non-Darwin/Linux fallback drains stable stderr ownership then reaps once" { if (builtin.os.tag == .macos or builtin.os.tag == .linux) return; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const result = try runProductionPipe(tmp, "fallback-cancelled", "hold"); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(result.stderr_bytes_drained != 0); try std.testing.expect(result.reaped); try std.testing.expectEqual(@as(usize, 1), result.reap_calls); try std.testing.expect(result.child_gone); try expectNoPublication(std.testing.io, tmp, "fallback-cancelled"); } test "import-git rejects a nonzero completed export after draining and reaping it once" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const result = try runProductionPipe(tmp, "nonzero-export", "nonzero"); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqual(@as(u8, 23), result.exit_code); try std.testing.expect(result.reaped); try std.testing.expect(result.child_gone); try expectNoPublication(std.testing.io, tmp, "nonzero-export"); } test "import-git prefers a nonzero truncated export status over its parser EOF" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const result = try runProductionPipe(tmp, "signed-like-export", "truncated-nonzero"); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqual(@as(u8, 23), result.exit_code); try std.testing.expect(std.mem.indexOf(u8, result.err, "GitExportFailed") != null); try std.testing.expect(std.mem.indexOf(u8, result.err, "fatal: signed commit rejected") != null); try std.testing.expect(std.mem.indexOf(u8, result.err, "UnexpectedEof") == null); try std.testing.expect(result.reaped); try std.testing.expect(result.child_gone); try expectNoPublication(std.testing.io, tmp, "signed-like-export"); } test "import-git verifies the frozen .zit and import map before the atomic rename" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export }); const result = scripted(tmp, &script, "verified", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expect(script.publication.store_reopened_for_verify); try std.testing.expect(script.publication.map_reopened_for_verify); try std.testing.expect(script.publication.verify_before_publish); } test "import-git competing destination at commit point never clobbers it and removes its temporary sibling" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export, .competing_destination_at_publish = true, }); const result = scripted(tmp, &script, "raced", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(script.competitor_destination_intact); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try std.testing.expect(!script.temporary_sibling_left); } test "import-git never publishes or deletes a replaced staging sibling" { const replacements = [_]import_cli.CommandScript.StagingReplacement{ .directory, .symlink }; for (replacements) |replacement| { for (0..8) |_| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDir(std.testing.io, "outside", .default_dir); try writeFixtureFile(std.testing.io, tmp.dir, "outside/replacement-sentinel", "survives\n"); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export, .replace_staging_at_publish = replacement, }); const result = scripted(tmp, &script, "raced-staging", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(std.mem.indexOf(u8, result.err, "StagingEntryReplaced") != null); try std.testing.expect(script.staging_replacement_observed); try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try std.testing.expect(!directoryExists(std.testing.io, tmp.dir, "raced-staging")); try std.testing.expect(!exists(std.testing.io, tmp.dir, ".zit-import-map.jsonl")); const staging_name = script.stagingReplacementName(); try std.testing.expect(staging_name.len != 0); var outside = try tmp.dir.openDir(std.testing.io, "outside", .{ .follow_symlinks = false }); defer outside.close(std.testing.io); try std.testing.expect(exists(std.testing.io, outside, "replacement-sentinel")); switch (replacement) { .directory => { var preserved = try tmp.dir.openDir(std.testing.io, staging_name, .{ .follow_symlinks = false }); defer preserved.close(std.testing.io); try std.testing.expect(exists(std.testing.io, preserved, "replacement-sentinel")); }, .symlink => { var target: [32]u8 = undefined; const len = try tmp.dir.readLink(std.testing.io, staging_name, &target); try std.testing.expectEqualStrings("outside", target[0..len]); }, } } } } test "import-git default ceilings clear the retired 200k per-commit file wall" { const limits: import_cli.ImportLimits = .{}; // The import leaves the arena budget at its default, so that default is // what an operator actually meets. const defaults: fast_export.Limits = .{ .max_line = 0, .max_data = 0, .max_path = 0 }; 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_export_events >= 400_000); try std.testing.expect(limits.max_retained_entries >= 6 * 400_000); 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_events = 3, .max_export_payload_bytes = one_command_budget, // Bumped from 7 by the mirror classifier's tables. A blob now also // retains its content facts, and a commit event is authorized for the // reference, candidate and tags-grammar entries its own file commands // can add -- a per-event bound, so the headroom is the worst case for // this stream's single command rather than its actual growth of two. .max_retained_entries = 16, .max_retained_bytes = 1024, }; const RejectedLimit = struct { limits: import_cli.ImportLimits, error_name: []const u8, }; const rejected_limits = [_]RejectedLimit{ .{ .limits = .{ .max_export_events = accepted_limits.max_export_events - 1, .max_export_payload_bytes = accepted_limits.max_export_payload_bytes, .max_retained_entries = accepted_limits.max_retained_entries, }, .error_name = "ExportEventLimitExceeded", }, .{ // 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_events = accepted_limits.max_export_events, .max_export_payload_bytes = 8, .max_retained_entries = accepted_limits.max_retained_entries, }, .error_name = "ExportPayloadLimitExceeded", }, .{ .limits = .{ .max_export_events = accepted_limits.max_export_events, .max_export_payload_bytes = accepted_limits.max_export_payload_bytes, .max_retained_entries = accepted_limits.max_retained_entries - 1, }, .error_name = "ImporterRetainedEntryLimitExceeded", }, .{ .limits = .{ .max_export_events = accepted_limits.max_export_events, .max_export_payload_bytes = accepted_limits.max_export_payload_bytes, .max_retained_entries = accepted_limits.max_retained_entries, .max_retained_bytes = 0, }, .error_name = "ImporterRetainedByteLimitExceeded", }, }; { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export_with_oids, .limits = accepted_limits, }); const result = scripted(tmp, &script, "at-limit", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.ok, result.code); try std.testing.expectEqual(@as(usize, 0), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); } for (rejected_limits, 0..) |rejected, index| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export_with_oids, .limits = rejected.limits, }); var destination: [32]u8 = undefined; const destination_name = try std.fmt.bufPrint(&destination, "limit-{d}", .{index}); const result = scripted(tmp, &script, destination_name, false, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(std.mem.indexOf(u8, result.err, rejected.error_name) != null); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, destination_name); } { // One byte below one command's minimum retention derives a ceiling of // zero, so the stream's single `M` command is refused. The failure must // name the observed count, the ceiling, and the derivation. It must // NOT name a flag: this ceiling is structural, and the payload knob // that once raised it no longer moves it at all. var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export_with_oids, .limits = .{ .max_export_events = accepted_limits.max_export_events, .max_command_arena_bytes = one_command_budget - 1, .max_retained_entries = accepted_limits.max_retained_entries, .max_retained_bytes = accepted_limits.max_retained_bytes, }, }); const result = scripted(tmp, &script, "command-ceiling", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); var expected: [512]u8 = undefined; const diagnostic = try std.fmt.bufPrint( &expected, "import-git: import failed: ExportFileCommandLimitExceeded\n" ++ "import-git: a commit exceeded the per-commit file-command ceiling: " ++ "observed at least 1, ceiling 0, derived from the {d}-byte per-commit " ++ "arena budget at {d} minimum retained bytes per command. This is a " ++ "structural bound on one commit, not a total: no option raises it, " ++ "because a commit at this width is beyond what the importer can hold whole\n", .{ one_command_budget - 1, one_command_budget }, ); try std.testing.expect(std.mem.indexOf(u8, result.err, diagnostic) != null); try std.testing.expect(std.mem.indexOf(u8, result.err, "--max-export-payload-bytes") == null); try expectNoPublication(std.testing.io, tmp, "command-ceiling"); } { // The cumulative-payload refusal must name its numbers for the same // reason the file-command one does. `ExportPayloadLimitExceeded` alone // sent a real 32k-commit import 17 minutes downstream to learn a single // integer, and the integer it needs is not even the one it was refused // at: parsing stops at the overrun, so the observed total is a lower // bound on the stream's payload and the budget must clear the whole of // it. Saying "above 9" would name a value that fails again. var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export_with_oids, .limits = .{ .max_export_events = accepted_limits.max_export_events, .max_export_payload_bytes = 8, .max_retained_entries = accepted_limits.max_retained_entries, .max_retained_bytes = accepted_limits.max_retained_bytes, }, }); const result = scripted(tmp, &script, "payload-ceiling", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(std.mem.indexOf( u8, result.err, "import-git: import failed: ExportPayloadLimitExceeded\n" ++ "import-git: the export stream's cumulative payload exceeded its budget: " ++ "observed at least 9 bytes against a budget of 8 bytes; parsing stopped there, " ++ "so the stream's full payload is larger than the observed total; re-run with " ++ "--max-export-payload-bytes above the stream's full payload\n", ) != null); try expectNoPublication(std.testing.io, tmp, "payload-ceiling"); } { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = "feature done\nprogress first\nprogress second\n", .limits = .{ .max_export_events = 1 }, }); const result = scripted(tmp, &script, "progress-limit", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(std.mem.indexOf(u8, result.err, "ExportEventLimitExceeded") != 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, "progress-limit"); } } test "import-git aborts every pre-rename writable-boundary failure without a partial destination" { const failures = [_]import_cli.PublicationHooks.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, .before_sync_temporary_directory, .after_sync_temporary_directory, .before_rename_no_replace, }; for (failures) |failure| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export }); const result = scripted(tmp, &script, "boundary-failed", false, .{ .fail_at = failure }); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, "boundary-failed"); } } test "import-git reports parent sync durability uncertainty after publish while retaining the verified destination" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export }); const result = scripted(tmp, &script, "durability-uncertain", false, .{ .fail_at = .sync_parent_after_publish }); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(result.published_durability_uncertain); var destination = try tmp.dir.openDir(std.testing.io, "durability-uncertain", .{}); defer destination.close(std.testing.io); try std.testing.expect(exists(std.testing.io, destination, ".zit")); try std.testing.expect(exists(std.testing.io, destination, ".zit-import-map.jsonl")); } test "import-git start session failure after stderr thread closes pipes and reaps exactly once" { var session = import_cli.ProductionGit.initForTest( std.testing.allocator, std.testing.io, &.{ helper_options.git_import_pipe_helper, "hold" }, ); defer session.deinit(); session.test_startup_failure = .after_stderr_thread; const dependencies = session.dependencies(.real_filesystem); try std.testing.expectError( error.InjectedStartFailure, dependencies.git.vtable.start_export(dependencies.git.context, "fixture.git"), ); try std.testing.expect(session.child == null); try std.testing.expect(session.stderr_thread == null); try std.testing.expect(session.stderr_file == null); try std.testing.expect(session.reaped); try std.testing.expectEqual(@as(usize, 1), session.reap_calls); } test "import-git rejects a replacement destination after rename without deleting it" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var script = import_cli.CommandScript.init(.{ .preflight = .branch_head, .fast_export = valid_export, .replace_destination_after_rename = true, }); const result = scripted(tmp, &script, "replacement-after-rename", false, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(std.mem.indexOf(u8, result.err, "PublishedDestinationReplaced") != null); try std.testing.expect(script.competitor_destination_intact); var replacement = try tmp.dir.openDir(std.testing.io, "replacement-after-rename", .{ .follow_symlinks = false, }); defer replacement.close(std.testing.io); try std.testing.expect(exists(std.testing.io, replacement, "replacement-sentinel")); try std.testing.expect(!script.temporary_sibling_left); } test "import-git reaps after termination failure and reports the lifecycle error" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var git: ScriptedGit = .{ .fast_export = malformed_export, .lifecycle_fault = .terminate, }; const result = runFixture(tmp, "fixture.git", "termination-failure", false, &git, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, result.code); try std.testing.expect(std.mem.indexOf(u8, result.err, "InjectedLifecycleFailure") != null); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.terminated); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.stderr_drained); try std.testing.expectEqual(@as(usize, 1), result.lifecycle.reaped); try expectNoPublication(std.testing.io, tmp, "termination-failure"); } test "import-git accounts the peeled target retained for marked tags at the exact byte boundary" { const exact_limit = try retainedBytesForExport(marked_tag_export); var exact_tmp = std.testing.tmpDir(.{}); defer exact_tmp.cleanup(); var exact_git: ScriptedGit = .{ .fast_export = marked_tag_export, .limits = .{ .max_retained_bytes = exact_limit }, }; const exact = runFixture(exact_tmp, "fixture.git", "marked-tag-exact", false, &exact_git, .real_filesystem); try std.testing.expectEqual(cli.Exit.ok, exact.code); var exceeded_tmp = std.testing.tmpDir(.{}); defer exceeded_tmp.cleanup(); var exceeded_git: ScriptedGit = .{ .fast_export = marked_tag_export, .limits = .{ .max_retained_bytes = exact_limit - 1 }, }; const exceeded = runFixture(exceeded_tmp, "fixture.git", "marked-tag-exceeded", false, &exceeded_git, .real_filesystem); try std.testing.expectEqual(cli.Exit.failure, exceeded.code); try std.testing.expect(std.mem.indexOf(u8, exceeded.err, "ImporterRetainedByteLimitExceeded") != null); try expectNoPublication(std.testing.io, exceeded_tmp, "marked-tag-exceeded"); } /// A fast-export whose single commit lands `count` distinct paths. Wide enough, /// at the k the tree-mode side of the comparison pins, that tree emission /// really stores SUBMANIFEST artifacts -- without that, "the flat import stored /// none" would be true of both modes and would prove nothing. fn wideExport(gpa: std.mem.Allocator, count: usize) ![]u8 { var out: std.ArrayList(u8) = .empty; errdefer out.deinit(gpa); try out.appendSlice( gpa, "feature done\n" ++ "blob\n" ++ "mark :1\n" ++ "data 5\n" ++ "hello\n" ++ "commit refs/heads/main\n" ++ "mark :2\n" ++ "author A <[email protected]> 0 +0000\n" ++ "committer A <[email protected]> 0 +0000\n" ++ "data 4\n" ++ "init\n", ); for (0..count) |index| { const line = try std.fmt.allocPrint(gpa, "M 100644 :1 file{d:0>6}.txt\n", .{index}); defer gpa.free(line); try out.appendSlice(gpa, line); } try out.appendSlice(gpa, "\n" ++ "done\n"); return out.toOwnedSlice(gpa); } const ImportedSubmanifestState = struct { enable_submanifests: bool, submanifest_k: u8, seen_submanifest: bool, /// Whether ANY artifact in the produced store is a SUBMANIFEST. Read off /// the store itself rather than off the config frame, so the two statements /// can disagree and be caught doing it. submanifests_stored: bool, checkin_paths: usize, }; /// Everything this feature claims about a produced repository, read back from /// the repository: the four persisted sub-manifest settings, whether the store /// holds a SUBMANIFEST artifact, and the flattened path count of the one /// check-in. The path count is what lets the caller assert that the two modes /// imported the SAME history. fn readImportedSubmanifestState(io: std.Io, destination: std.Io.Dir) !ImportedSubmanifestState { const gpa = std.testing.allocator; var file = try destination.openFile(io, ".zit", .{}); defer file.close(io); const stat = try file.stat(io); var file_storage = storage.FileStorage.init(io, file, stat.size); var log = try logstore.LogStore.openReadOnly(gpa, file_storage.storageFile()); defer log.deinit(gpa); var owned = (try log.readConfig(gpa)) orelse return error.TestUnexpectedResult; defer owned.deinit(gpa); try std.testing.expect(!owned.config.submanifest_auto_retune); const rids = try log.ridsInPersistedOrder(gpa); defer gpa.free(rids); var submanifests: usize = 0; var checkins: usize = 0; var checkin_paths: usize = 0; for (rids) |rid| { const bytes = (try log.content(gpa, rid)) orelse continue; defer gpa.free(bytes); // A blob is not parseable as an artifact; skipping is the whole // classification here, so it must not be an error. var manifest = artifact.parse(gpa, bytes) catch continue; defer manifest.deinit(); switch (manifest.kind) { .submanifest => submanifests += 1, .manifest => { checkins += 1; checkin_paths = if (manifest.s.len != 0) manifest.s.len else manifest.f.len; }, else => {}, } } try std.testing.expectEqual(@as(usize, 1), checkins); return .{ .enable_submanifests = owned.config.enable_submanifests, .submanifest_k = owned.config.submanifest_k, .seen_submanifest = owned.config.seen_submanifest, .submanifests_stored = submanifests != 0, .checkin_paths = checkin_paths, }; } /// Every check-in manifest in the store carries only F cards, and no artifact /// in it is a SUBMANIFEST. This is the store-side reading of §8.1's "MUST NOT /// emit an S card"; it consults no flag and no config field. fn expectNoSCardsAnywhere(io: std.Io, destination: std.Io.Dir) !void { const gpa = std.testing.allocator; var file = try destination.openFile(io, ".zit", .{}); defer file.close(io); const stat = try file.stat(io); var file_storage = storage.FileStorage.init(io, file, stat.size); var log = try logstore.LogStore.openReadOnly(gpa, file_storage.storageFile()); defer log.deinit(gpa); const rids = try log.ridsInPersistedOrder(gpa); defer gpa.free(rids); for (rids) |rid| { const bytes = (try log.content(gpa, rid)) orelse continue; defer gpa.free(bytes); var manifest = artifact.parse(gpa, bytes) catch continue; defer manifest.deinit(); try std.testing.expect(manifest.kind != .submanifest); try std.testing.expectEqual(@as(usize, 0), manifest.s.len); } } test "import-git --no-submanifests leaves the destination at the spec-default config with no S cards" { const gpa = std.testing.allocator; // 96 paths at k=1 is comfortably past the point the canonical partition // yields more than one group, which the tree-mode half below proves by // actually storing SUBMANIFEST artifacts from this exact stream. const export_stream = try wideExport(gpa, 96); defer gpa.free(export_stream); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var flat_git: ScriptedGit = .{ .fast_export = export_stream }; const flat = runFixtureShaped(tmp, "fixture.git", "flat", false, &flat_git, .real_filesystem, .flat); try std.testing.expectEqual(cli.Exit.ok, flat.code); // "Do the default thing" reports nothing about sub-manifests. try std.testing.expect(std.mem.indexOf(u8, flat.err, "sub-manifest") == null); var tree_git: ScriptedGit = .{ .fast_export = export_stream }; const tree = runFixtureShaped(tmp, "fixture.git", "tree", false, &tree_git, .real_filesystem, .{ .tree = 1 }); try std.testing.expectEqual(cli.Exit.ok, tree.code); // The other side of the same gate. Flat's silence above only means // something if an unpinned tree import does report its analyzed k, so pin // that here too -- otherwise deleting the report entirely would leave both // halves green. var analyzed_git: ScriptedGit = .{ .fast_export = export_stream, .analyzed_submanifest_k = 3 }; const analyzed = runFixtureShaped(tmp, "fixture.git", "analyzed", false, &analyzed_git, .real_filesystem, .{ .tree = null }); try std.testing.expectEqual(cli.Exit.ok, analyzed.code); try std.testing.expect(std.mem.indexOf(u8, analyzed.err, "analyzed sub-manifest k=3") != null); var flat_dir = try tmp.dir.openDir(std.testing.io, "flat", .{}); defer flat_dir.close(std.testing.io); var tree_dir = try tmp.dir.openDir(std.testing.io, "tree", .{}); defer tree_dir.close(std.testing.io); const flat_state = try readImportedSubmanifestState(std.testing.io, flat_dir); const tree_state = try readImportedSubmanifestState(std.testing.io, tree_dir); // The tree half is asserted first because it is what gives the flat half // its meaning: this stream at this k really does produce sub-manifests, and // today's tree-mode config frame is pinned here as the contrast. try std.testing.expect(tree_state.submanifests_stored); try std.testing.expect(tree_state.enable_submanifests); try std.testing.expectEqual(@as(u8, 1), tree_state.submanifest_k); try std.testing.expect(tree_state.seen_submanifest); // The flat half: identical to a fresh `zit init` as far as sub-manifest // behavior goes. `submanifest_k = 0` is "not pinned", not "k of zero". try std.testing.expect(!flat_state.submanifests_stored); try std.testing.expect(!flat_state.enable_submanifests); try std.testing.expectEqual(@as(u8, 0), flat_state.submanifest_k); try std.testing.expect(!flat_state.seen_submanifest); try expectNoSCardsAnywhere(std.testing.io, flat_dir); // ... and it imported the same history: every path landed, as F cards. try std.testing.expectEqual(@as(usize, 96), flat_state.checkin_paths); try std.testing.expect(tree_state.checkin_paths < flat_state.checkin_paths); }