Artifact c734311ed0726df09f68b6dc3108d5ff41f72dfa963c237ebf1168b53ff42baf:
- File
src/update_test.zig
— part of check-in
[8797f53546]
at
2026-08-10 13:41:20
on branch sub-manifest
— update: cost tracks the diff, not the width
Three fixes, each found by measuring the previous one's residue.
markUnchanged: a path byte-identical between baseline and target -- same digest, same mode, with requireClean having already proven the on-disk file matches -- is skipped by every mutation phase: staging both directions, the undo journal, deletion and rewrite. An update used to delete and rewrite the entire tree regardless of how little changed.
.zit-vstat: the stat sidecar written at materialization is now the BASELINE PLAN -- version-bound, CRC'd, carrying mode and plan order -- so requireClean rebuilds the baseline with zero tree flattens when it describes the current check-in, and proves most files clean by inode/size/mtime instead of reconstructing their content out of the log. Advisory throughout: any mismatch, staleness or corruption falls back to the flatten and the content compare, which stay authoritative.
ManagedSet.fromSortedPaths: building the 66k-entry managed set through insert was quadratic -- an O(n) sorted-position memmove plus the LIVE sortedUnique assert per call -- and measured at 23.9 SECONDS of a pkgsrc update, larger than every real phase combined. The plan's own order is already sorted and unique, so the set is now built in one pass, order enforced per adjacent pair, invariant asserted once.
Measured, same store and targets as #52's gate: sqlite (2.2k files): 117.8s flat -> 0.6s adjacent / 1.2s distant pkgsrc (66k files): 42.2s flat -> 6.8s adjacent / 8.9s distant pkgsrc user CPU: ~25s -> 1.3s Residue is stat I/O (two ~66k-file sweeps), not computation.
The lockstep flatten prune #52 proposed was never built: the flatten measured 178ms at 66k files once the real costs fell.
Tests: dirty AND delete of a skipped-diff file both refuse with the tree untouched; sidecar intact/absent/rebound-to-wrong-check-in all produce identical trees; an unchanged file provably keeps its inode; an updated tree equals a from-scratch materialization. Closes #52. (user: dmestas, size: 43486) [annotate] [blame] [check-ins using]
//! Private `update.applyCheckin` contract tests. //! //! Tests that require update.zig file-private helpers or fault-injection seams //! remain inline with that implementation. const std = @import("std"); const artifact = @import("artifact.zig"); const checkout = @import("checkout.zig"); const logstore = @import("logstore.zig"); const managed = @import("managed.zig"); const storage = @import("storage.zig"); const update = @import("update.zig"); const ApplyError = update.ApplyError; const applyCheckin = update.applyCheckin; fn decodeValidatedName(name: []const u8, out: *[logstore.max_hash_len]u8) ApplyError![]const u8 { if (!artifact.validateName(name)) return error.InvalidCheckinId; const raw = out[0 .. name.len / 2]; _ = std.fmt.hexToBytes(raw, name) catch return error.InvalidCheckinId; return raw; } fn completeTestArtifact(gpa: std.mem.Allocator, body: []const u8) ![]u8 { var checksum_buf: [32]u8 = undefined; const checksum = artifact.selfChecksum(body, &checksum_buf); return std.fmt.allocPrint(gpa, "{s}Z {s}\n", .{ body, checksum }); } fn putTestArtifact(log: *logstore.LogStore, name: []const u8, bytes: []const u8) !void { var digest: [logstore.max_hash_len]u8 = undefined; const raw = try decodeValidatedName(name, &digest); _ = try log.put(raw, bytes); } fn putCompleteTestArtifact( gpa: std.mem.Allocator, log: *logstore.LogStore, body: []const u8, id_buf: *[64]u8, ) ![]const u8 { const bytes = try completeTestArtifact(gpa, body); defer gpa.free(bytes); const id = artifact.artifactName(.sha1, bytes, id_buf); try putTestArtifact(log, id, bytes); return id; } fn putTestPayload( log: *logstore.LogStore, bytes: []const u8, id_buf: *[64]u8, ) ![]const u8 { const id = artifact.artifactName(.sha1, bytes, id_buf); try putTestArtifact(log, id, bytes); return id; } fn putDeltaTestTarget( gpa: std.mem.Allocator, log: *logstore.LogStore, baseline_id: []const u8, id_buf: *[64]u8, ) ![]const u8 { const body = try std.fmt.allocPrint(gpa, "B {s}\nD 2007-07-21T14:10:00\nF next {s}\n", .{ baseline_id, "a" ** 40, }); defer gpa.free(body); return putCompleteTestArtifact(gpa, log, body, id_buf); } fn writeFixturePayload( io: std.Io, root: std.Io.Dir, path: []const u8, bytes: []const u8, mode: artifact.FileMode, ) !void { var file = try root.createFile(io, path, .{ .read = false, .exclusive = true, .permissions = switch (mode) { .regular => .default_file, .executable => .executable_file, .symlink => unreachable, }, .resolve_beneath = true, }); defer file.close(io); try file.writePositionalAll(io, bytes, 0); try file.sync(io); } fn modeMatches(stat: std.Io.File.Stat, mode: artifact.FileMode) bool { if (comptime std.Io.File.Permissions.has_executable_bit) { const executable = stat.permissions.toMode() & 0o111 != 0; return switch (mode) { .regular => !executable, .executable => executable, .symlink => false, }; } return mode != .symlink; } fn expectFileState( gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir, path: []const u8, expected_bytes: []const u8, expected_mode: artifact.FileMode, ) !void { const stat = try worktree.statFile(io, path, .{ .follow_symlinks = false }); try std.testing.expectEqual(.file, stat.kind); try std.testing.expect(modeMatches(stat, expected_mode)); var file = try worktree.openFile(io, path, .{ .allow_directory = false, .follow_symlinks = false, .resolve_beneath = true, }); defer file.close(io); const bytes = try gpa.alloc(u8, stat.size); defer gpa.free(bytes); const n = try file.readPositionalAll(io, bytes, 0); try std.testing.expectEqual(expected_bytes.len, n); try std.testing.expectEqualStrings(expected_bytes, bytes); } fn expectSymlinkTarget(io: std.Io, worktree: std.Io.Dir, path: []const u8, expected_target: []const u8) !void { const stat = try worktree.statFile(io, path, .{ .follow_symlinks = false }); try std.testing.expectEqual(.sym_link, stat.kind); var target: [1024]u8 = undefined; const n = try worktree.readLink(io, path, &target); try std.testing.expectEqualStrings(expected_target, target[0..n]); } const RootEntrySet = struct { names: std.ArrayList([]u8) = .empty, fn deinit(self: *RootEntrySet, gpa: std.mem.Allocator) void { for (self.names.items) |name| gpa.free(name); self.names.deinit(gpa); self.* = undefined; } fn contains(self: *const RootEntrySet, name: []const u8) bool { for (self.names.items) |candidate| { if (std.mem.eql(u8, candidate, name)) return true; } return false; } }; fn snapshotRootEntries(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir) !RootEntrySet { var entries = RootEntrySet{}; errdefer entries.deinit(gpa); var root = try worktree.openDir(io, ".", .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); defer root.close(io); var iterator = root.iterate(); while (try iterator.next(io)) |entry| { const name = try gpa.dupe(u8, entry.name); entries.names.append(gpa, name) catch |err| { gpa.free(name); return err; }; } return entries; } fn expectSameRootEntries(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir, expected: *const RootEntrySet) !void { var actual = try snapshotRootEntries(gpa, io, worktree); defer actual.deinit(gpa); try std.testing.expectEqual(expected.names.items.len, actual.names.items.len); for (expected.names.items) |name| try std.testing.expect(actual.contains(name)); } fn seedUntouchedWorktree(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir) !void { try checkout.write(io, worktree, .{ .version = "old-checkout", .branch = "old-branch", .merge_parent = "", .special_op = "", .manifest_flags = "" }); var old_managed = managed.ManagedSet.init(gpa); defer old_managed.deinit(); _ = try old_managed.insert("old-file"); try old_managed.write(io, worktree); try writeFixturePayload(io, worktree, "old-file", "old executable bytes", .executable); try writeFixturePayload(io, worktree, "witness", "unmanaged witness bytes", .regular); } fn expectUntouchedWorktree(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir) !void { var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, worktree, &checkout_buf)).?; try std.testing.expectEqualStrings("old-checkout", state.version); try std.testing.expectEqualStrings("old-branch", state.branch); var current_managed = try managed.ManagedSet.read(gpa, io, worktree); defer current_managed.deinit(); try std.testing.expectEqual(@as(usize, 1), current_managed.iterate().len); try std.testing.expect(current_managed.contains("old-file")); try expectFileState(gpa, io, worktree, "old-file", "old executable bytes", .executable); try expectFileState(gpa, io, worktree, "witness", "unmanaged witness bytes", .regular); } fn setupRollbackFailureFixture( gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir, log: *logstore.LogStore, target_id_buf: *[64]u8, old_id_buf: *[64]u8, ) ![]const u8 { var payload_id_buf: [64]u8 = undefined; const payload_id = artifact.artifactName(.sha1, "", &payload_id_buf); try putTestArtifact(log, payload_id, ""); const old_body = try std.fmt.allocPrint(gpa, "D 2007-07-21T14:09:59\nF old {s}\n", .{payload_id}); defer gpa.free(old_body); const old_bytes = try completeTestArtifact(gpa, old_body); defer gpa.free(old_bytes); const old_id = artifact.artifactName(.sha1, old_bytes, old_id_buf); try putTestArtifact(log, old_id, old_bytes); const target_body = try std.fmt.allocPrint(gpa, "D 2007-07-21T14:10:00\nF new {s}\n", .{payload_id}); defer gpa.free(target_body); const target_bytes = try completeTestArtifact(gpa, target_body); defer gpa.free(target_bytes); const target_id = artifact.artifactName(.sha1, target_bytes, target_id_buf); try putTestArtifact(log, target_id, target_bytes); try checkout.write(io, worktree, .{ .version = old_id, .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" }); var old_managed = managed.ManagedSet.init(gpa); defer old_managed.deinit(); _ = try old_managed.insert("old"); try old_managed.write(io, worktree); try writeFixturePayload(io, worktree, "old", "", .regular); return target_id; } fn retainedTransactionContainsPrevious(io: std.Io, worktree: std.Io.Dir) !bool { var root = try worktree.openDir(io, ".", .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); defer root.close(io); var iterator = root.iterate(); while (try iterator.next(io)) |entry| { if (entry.kind != .directory or !std.mem.startsWith(u8, entry.name, ".zit-update-txn-")) continue; var transaction = try root.openDir(io, entry.name, .{ .access_sub_paths = true, .follow_symlinks = false }); defer transaction.close(io); var previous = transaction.openDir(io, "previous", .{ .access_sub_paths = true, .follow_symlinks = false }) catch continue; defer previous.close(io); const old = previous.statFile(io, "old", .{ .follow_symlinks = false }) catch continue; if (old.kind == .file) return true; } return false; } test "update strict target and baseline failures leave the checkout untouched" { const t = std.testing; const Case = struct { kind: enum { target_missing, target_phantom, target_misnamed, target_non_manifest, baseline_missing, baseline_phantom, baseline_misnamed, baseline_non_manifest, baseline_delta, }, expected: ApplyError, }; const cases = [_]Case{ .{ .kind = .target_missing, .expected = error.CheckinNotFound }, .{ .kind = .target_phantom, .expected = error.CheckinContentMissing }, .{ .kind = .target_misnamed, .expected = error.CheckinNameMismatch }, .{ .kind = .target_non_manifest, .expected = error.TargetNotManifest }, .{ .kind = .baseline_missing, .expected = error.BaselineNotFound }, .{ .kind = .baseline_phantom, .expected = error.BaselineContentMissing }, .{ .kind = .baseline_misnamed, .expected = error.BaselineNameMismatch }, .{ .kind = .baseline_non_manifest, .expected = error.BaselineNotManifest }, .{ .kind = .baseline_delta, .expected = error.BaselineIsDelta }, }; for (cases) |case| { var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); try seedUntouchedWorktree(t.allocator, io, tmp.dir); var root_entries = try snapshotRootEntries(t.allocator, io, tmp.dir); defer root_entries.deinit(t.allocator); var target_id_buf: [64]u8 = undefined; var target_id: []const u8 = undefined; switch (case.kind) { .target_missing => target_id = "a" ** 40, .target_phantom => { target_id = "b" ** 40; var digest: [logstore.max_hash_len]u8 = undefined; _ = try log.intern(try decodeValidatedName(target_id, &digest)); }, .target_misnamed => { const expected_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:09:59\nF expected aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n"); defer t.allocator.free(expected_bytes); target_id = artifact.artifactName(.sha1, expected_bytes, &target_id_buf); const actual_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:10:00\nF actual bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n"); defer t.allocator.free(actual_bytes); try putTestArtifact(&log, target_id, actual_bytes); }, .target_non_manifest => { target_id = try putCompleteTestArtifact(t.allocator, &log, "D 2007-07-21T14:09:59\nT +branch aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa trunk\nU alice\n", &target_id_buf); }, .baseline_missing => { target_id = try putDeltaTestTarget(t.allocator, &log, "c" ** 40, &target_id_buf); }, .baseline_phantom => { const baseline_id = "d" ** 40; var digest: [logstore.max_hash_len]u8 = undefined; _ = try log.intern(try decodeValidatedName(baseline_id, &digest)); target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf); }, .baseline_misnamed => { const expected_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:09:59\nF expected aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n"); defer t.allocator.free(expected_bytes); var baseline_id_buf: [64]u8 = undefined; const baseline_id = artifact.artifactName(.sha1, expected_bytes, &baseline_id_buf); const actual_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:10:00\nF actual bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n"); defer t.allocator.free(actual_bytes); try putTestArtifact(&log, baseline_id, actual_bytes); target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf); }, .baseline_non_manifest => { var baseline_id_buf: [64]u8 = undefined; const baseline_id = try putCompleteTestArtifact(t.allocator, &log, "D 2007-07-21T14:09:59\nT +branch aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa trunk\nU alice\n", &baseline_id_buf); target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf); }, .baseline_delta => { var baseline_id_buf: [64]u8 = undefined; const baseline_body = try std.fmt.allocPrint(t.allocator, "B {s}\nD 2007-07-21T14:09:59\nF inherited aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", .{"e" ** 40}); defer t.allocator.free(baseline_body); const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf); target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf); }, } try t.expectError(case.expected, applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit)); try expectUntouchedWorktree(t.allocator, io, tmp.dir); try expectSameRootEntries(t.allocator, io, tmp.dir, &root_entries); } } test "update delta applies inherited replacements and deletions across multiple entries" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var inherited_id_buf: [64]u8 = undefined; const inherited_id = try putTestPayload(&log, "inherited bytes", &inherited_id_buf); var original_id_buf: [64]u8 = undefined; const original_id = try putTestPayload(&log, "original bytes", &original_id_buf); var removed_id_buf: [64]u8 = undefined; const removed_id = try putTestPayload(&log, "removed bytes", &removed_id_buf); var replacement_id_buf: [64]u8 = undefined; const replacement_id = try putTestPayload(&log, "replacement bytes", &replacement_id_buf); const baseline_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF inherited {s}\nF removed {s}\nF replaced {s}\n", .{ inherited_id, removed_id, original_id, }); defer t.allocator.free(baseline_body); var baseline_id_buf: [64]u8 = undefined; const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf); const target_body = try std.fmt.allocPrint(t.allocator, "B {s}\nD 2007-07-21T14:10:00\nF removed\nF replaced {s}\nF unmatched\n", .{ baseline_id, replacement_id, }); defer t.allocator.free(target_body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit); try expectFileState(t.allocator, io, tmp.dir, "inherited", "inherited bytes", .regular); try expectFileState(t.allocator, io, tmp.dir, "replaced", "replacement bytes", .regular); try t.expectError(error.FileNotFound, tmp.dir.statFile(io, "removed", .{ .follow_symlinks = false })); try t.expectError(error.FileNotFound, tmp.dir.statFile(io, "unmatched", .{ .follow_symlinks = false })); var current_managed = try managed.ManagedSet.read(t.allocator, io, tmp.dir); defer current_managed.deinit(); try t.expectEqual(@as(usize, 2), current_managed.iterate().len); try t.expect(current_managed.contains("inherited")); try t.expect(current_managed.contains("replaced")); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(target_id, state.version); } test "update touches only the diff: an unchanged file is not rewritten, and the tree still equals a fresh materialization" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var stable_id_buf: [64]u8 = undefined; const stable_id = try putTestPayload(&log, "stable bytes", &stable_id_buf); var v1_id_buf: [64]u8 = undefined; const v1_id = try putTestPayload(&log, "moving v1", &v1_id_buf); var v2_id_buf: [64]u8 = undefined; const v2_id = try putTestPayload(&log, "moving v2", &v2_id_buf); const baseline_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF moving {s}\nF stable {s}\n", .{ v1_id, stable_id }); defer t.allocator.free(baseline_body); var baseline_id_buf: [64]u8 = undefined; const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf); const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF moving {s}\nF stable {s}\n", .{ v2_id, stable_id }); defer t.allocator.free(target_body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, baseline_id, .inherit); const before = try tmp.dir.statFile(io, "stable", .{ .follow_symlinks = false }); try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit); // The canary that the skip actually fired: materialization writes a fresh // file and moves it into place, so a rewritten `stable` would carry a new // inode. The SAME inode proves the unchanged path was never staged, // deleted, or rewritten -- the point of the diff. const after = try tmp.dir.statFile(io, "stable", .{ .follow_symlinks = false }); try t.expectEqual(before.inode, after.inode); // And the oracle that skipping never changes the result: the updated tree // is byte-identical to what a from-scratch materialization of the target // produces. A wrong diff -- an entry skipped that should have moved -- // fails here as a content mismatch, not silently. try expectFileState(t.allocator, io, tmp.dir, "stable", "stable bytes", .regular); try expectFileState(t.allocator, io, tmp.dir, "moving", "moving v2", .regular); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(target_id, state.version); } test "update refuses a dirtied or deleted file that the diff would have skipped" { const t = std.testing; const io = t.io; // The invariant #52 requires pinned BEFORE any skip optimization: a file // that is byte-identical between baseline and target -- exactly the one // markUnchanged marks and the sidecar plan carries -- must still be // repaired-or-refused when the working tree disagrees with the baseline. // Both mutations must die in `requireClean`, before any mark is computed. for ([_]enum { dirty, delete }{ .dirty, .delete }) |mutation| { var tmp = t.tmpDir(.{}); defer tmp.cleanup(); var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var stable_id_buf: [64]u8 = undefined; const stable_id = try putTestPayload(&log, "stable bytes", &stable_id_buf); var v1_id_buf: [64]u8 = undefined; const v1_id = try putTestPayload(&log, "moving v1", &v1_id_buf); var v2_id_buf: [64]u8 = undefined; const v2_id = try putTestPayload(&log, "moving v2", &v2_id_buf); const baseline_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF moving {s}\nF stable {s}\n", .{ v1_id, stable_id }); defer t.allocator.free(baseline_body); var baseline_id_buf: [64]u8 = undefined; const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf); const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF moving {s}\nF stable {s}\n", .{ v2_id, stable_id }); defer t.allocator.free(target_body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, baseline_id, .inherit); switch (mutation) { .dirty => { try tmp.dir.deleteFile(io, "stable"); try writeFixturePayload(io, tmp.dir, "stable", "tampered", .regular); }, .delete => try tmp.dir.deleteFile(io, "stable"), } try t.expectError(error.WorktreeNotClean, applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit)); // Refused means REFUSED: the moving file is still at v1, the record // still names the baseline. try expectFileState(t.allocator, io, tmp.dir, "moving", "moving v1", .regular); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(baseline_id, state.version); } } test "update behaves identically with the sidecar plan, without it, and with a stale one" { const t = std.testing; const io = t.io; // Three second-updates over identical repositories: sidecar intact (the // no-flatten fast path), sidecar deleted (the flatten fallback), and // sidecar rebound to a WRONG check-in id (must be ignored, not trusted). // All three must land the same tree and the same record -- the sidecar is // an accelerator, never an input. for ([_]enum { intact, absent, stale }{ .intact, .absent, .stale }) |variant| { var tmp = t.tmpDir(.{}); defer tmp.cleanup(); var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var stable_id_buf: [64]u8 = undefined; const stable_id = try putTestPayload(&log, "stable bytes", &stable_id_buf); var v1_id_buf: [64]u8 = undefined; const v1_id = try putTestPayload(&log, "moving v1", &v1_id_buf); var v2_id_buf: [64]u8 = undefined; const v2_id = try putTestPayload(&log, "moving v2", &v2_id_buf); const baseline_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF moving {s}\nF stable {s}\n", .{ v1_id, stable_id }); defer t.allocator.free(baseline_body); var baseline_id_buf: [64]u8 = undefined; const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf); const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF moving {s}\nF stable {s}\n", .{ v2_id, stable_id }); defer t.allocator.free(target_body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, baseline_id, .inherit); switch (variant) { .intact => {}, .absent => try tmp.dir.deleteFile(io, ".zit-vstat"), .stale => { // Rebind the sidecar to a different id by rewriting the header // byte-for-byte EXCEPT the checkin field, keeping the CRC // honest -- a syntactically perfect sidecar for the wrong // check-in, the exact shape trust must refuse. var f = try tmp.dir.openFile(io, ".zit-vstat", .{}); defer f.close(io); const st = try f.stat(io); const buf = try t.allocator.alloc(u8, @intCast(st.size)); defer t.allocator.free(buf); _ = try f.readPositionalAll(io, buf, 0); // magic(8) + version(2) + checkin_len(2) then the id bytes. buf[12] ^= 0x01; // flip one id byte const body = buf[0 .. buf.len - 4]; var crc = @import("crc32.zig").Crc32.init(); crc.update(body); std.mem.writeInt(u32, buf[buf.len - 4 ..][0..4], crc.final(), .little); var rw = try tmp.dir.createFile(io, ".zit-vstat", .{ .truncate = true }); defer rw.close(io); try rw.writePositionalAll(io, buf, 0); }, } try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit); try expectFileState(t.allocator, io, tmp.dir, "stable", "stable bytes", .regular); try expectFileState(t.allocator, io, tmp.dir, "moving", "moving v2", .regular); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(target_id, state.version); } } test "update stages the full tree when the baseline is unborn, unchanged marks notwithstanding" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var one_id_buf: [64]u8 = undefined; const one_id = try putTestPayload(&log, "only bytes", &one_id_buf); const body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF only {s}\n", .{one_id}); defer t.allocator.free(body); var id_buf: [64]u8 = undefined; const id = try putCompleteTestArtifact(t.allocator, &log, body, &id_buf); // An unborn baseline has an EMPTY plan, so nothing can be marked unchanged // and open-style materialization is exactly what it always was. try applyCheckin(t.allocator, io, tmp.dir, &log, id, .inherit); try expectFileState(t.allocator, io, tmp.dir, "only", "only bytes", .regular); } test "update non-ancestor control tags use persisted frame order over RID order" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var target_payload_id_buf: [64]u8 = undefined; const target_payload_id = try putTestPayload(&log, "target bytes", &target_payload_id_buf); const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF target {s}\n", .{target_payload_id}); defer t.allocator.free(target_body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf); const RID_first_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nT +branch {s} rid-first\nU alice\n", .{target_id}); defer t.allocator.free(RID_first_body); const RID_first_bytes = try completeTestArtifact(t.allocator, RID_first_body); defer t.allocator.free(RID_first_bytes); var RID_first_id_buf: [64]u8 = undefined; const RID_first_id = artifact.artifactName(.sha1, RID_first_bytes, &RID_first_id_buf); var RID_first_digest: [logstore.max_hash_len]u8 = undefined; const RID_first = try log.intern(try decodeValidatedName(RID_first_id, &RID_first_digest)); const persisted_first_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nT +branch {s} persisted-first\nU alice\n", .{target_id}); defer t.allocator.free(persisted_first_body); const persisted_first_bytes = try completeTestArtifact(t.allocator, persisted_first_body); defer t.allocator.free(persisted_first_bytes); var persisted_first_id_buf: [64]u8 = undefined; const persisted_first_id = artifact.artifactName(.sha1, persisted_first_bytes, &persisted_first_id_buf); try putTestArtifact(&log, persisted_first_id, persisted_first_bytes); var persisted_first_digest: [logstore.max_hash_len]u8 = undefined; const persisted_first = log.lookup(try decodeValidatedName(persisted_first_id, &persisted_first_digest)).?; try t.expect(RID_first < persisted_first); try putTestArtifact(&log, RID_first_id, RID_first_bytes); try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings("persisted-first", state.branch); } test "update applyCheckin rejects a non-manifest before checkout mutation" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); const payload = "ordinary blob, not a structural artifact"; var id_buf: [64]u8 = undefined; const target_id = artifact.artifactName(.sha1, payload, &id_buf); var digest: [logstore.max_hash_len]u8 = undefined; const raw_digest = try decodeValidatedName(target_id, &digest); _ = try log.put(raw_digest, payload); try checkout.write(io, tmp.dir, .{ .version = "old", .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" }); try t.expectError(error.TargetNotManifest, applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit)); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings("old", state.version); try t.expectEqualStrings("trunk", state.branch); } test "update retains the staged rollback tree when inverse rollback fails" { const t = std.testing; const io = t.io; var probe_tmp = t.tmpDir(.{}); defer probe_tmp.cleanup(); var probe_raw_file = try probe_tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer probe_raw_file.close(io); var probe_storage = storage.FileStorage.init(io, probe_raw_file, 0); var probe_log = try logstore.LogStore.open(t.allocator, probe_storage.storageFile(), 0); defer probe_log.deinit(t.allocator); var probe_target_id_buf: [64]u8 = undefined; var probe_old_id_buf: [64]u8 = undefined; const probe_target_id = try setupRollbackFailureFixture(t.allocator, io, probe_tmp.dir, &probe_log, &probe_target_id_buf, &probe_old_id_buf); var probe_allocator = std.testing.FailingAllocator.init(t.allocator, .{}); try applyCheckin(probe_allocator.allocator(), io, probe_tmp.dir, &probe_log, probe_target_id, .inherit); const allocation_count = probe_allocator.alloc_index; try t.expect(allocation_count > 0); // The rollback-arming allocation is no longer the run's LAST: the stat // sidecar writes (and allocates, swallowed) after the commit point, so // "fail the final allocation" now lands harmlessly inside it and the // update simply succeeds. Walk the fail index downward from the probe // count until the injected failure genuinely reaches the rollback path -- // a successful attempt is discarded with its tree, a fresh fixture built, // and the walk is bounded by the probe count itself. var fail_index: usize = allocation_count; const found = while (fail_index > 0) : (fail_index -= 1) { var tmp = t.tmpDir(.{}); defer tmp.cleanup(); var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var target_id_buf: [64]u8 = undefined; var old_id_buf: [64]u8 = undefined; const target_id = try setupRollbackFailureFixture(t.allocator, io, tmp.dir, &log, &target_id_buf, &old_id_buf); var failing_allocator = std.testing.FailingAllocator.init(t.allocator, .{ .fail_index = fail_index - 1 }); const result = applyCheckin(failing_allocator.allocator(), io, tmp.dir, &log, target_id, .inherit); if (result) |_| continue else |err| { if (err != error.RollbackFailed) continue; try t.expect(failing_allocator.has_induced_failure); try t.expect(try retainedTransactionContainsPrevious(io, tmp.dir)); try t.expect((try tmp.dir.statFile(io, "old", .{ .follow_symlinks = false })).kind == .file); try t.expectError(error.FileNotFound, tmp.dir.statFile(io, "new", .{ .follow_symlinks = false })); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(old_id_buf[0..40], state.version); var restored_managed = try managed.ManagedSet.read(t.allocator, io, tmp.dir); defer restored_managed.deinit(); try t.expectEqual(@as(usize, 1), restored_managed.iterate().len); try t.expect(restored_managed.contains("old")); break true; } } else false; try t.expect(found); } test "update applyCheckin treats a present-but-unborn checkout record as a clean tree" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); const payload = "unborn-tree payload\n"; var payload_id_buf: [64]u8 = undefined; const payload_id = try putTestPayload(&log, payload, &payload_id_buf); const body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF only.txt {s}\n", .{payload_id}); defer t.allocator.free(body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, body, &target_id_buf); // The exact bootstrap `init` writes and `clone` inherits: a checkout record // that is PRESENT but names no check-in, with no managed set beside it. The // e2e proves this state as clone actually produces it; this pins the // semantics at the layer that owns them, so a regression fails fast and // locally instead of only through a networked clone. try checkout.write(io, tmp.dir, .{ .version = "", .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" }); try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit); try expectFileState(t.allocator, io, tmp.dir, "only.txt", payload, .regular); var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined; const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?; try t.expectEqualStrings(target_id, state.version); } test "update applyCheckin still refuses an unborn record that has a managed set" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); const payload = "unborn-tree payload\n"; var payload_id_buf: [64]u8 = undefined; const payload_id = try putTestPayload(&log, payload, &payload_id_buf); const body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF only.txt {s}\n", .{payload_id}); defer t.allocator.free(body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, body, &target_id_buf); // An unborn record is only "clean" when nothing is managed. A record that // names no check-in but claims managed paths is incoherent state, not a // fresh clone — the tolerance added for clone must NOT swallow it. try checkout.write(io, tmp.dir, .{ .version = "", .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" }); var set = managed.ManagedSet.init(t.allocator); defer set.deinit(); _ = try set.insert("stray.txt"); try set.write(io, tmp.dir); try t.expectError(error.WorktreeNotClean, applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit)); } test "update applyCheckin refuses an unborn record that claims a pending operation" { const t = std.testing; const io = t.io; // The other half of the unborn coherence rule: a record naming no check-in // cannot also carry a merge parent or a special operation, because both // describe work in progress against a check-in the record does not name. // An empty managed set alone does not make such a record a fresh clone. const cases = [_]checkout.CheckoutState{ .{ .version = "", .branch = "trunk", .merge_parent = "b" ** 64, .special_op = "", .manifest_flags = "" }, .{ .version = "", .branch = "trunk", .merge_parent = "", .special_op = "merge", .manifest_flags = "" }, }; for (cases) |state| { var tmp = t.tmpDir(.{}); defer tmp.cleanup(); var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); const payload = "pending-op payload\n"; var payload_id_buf: [64]u8 = undefined; const payload_id = try putTestPayload(&log, payload, &payload_id_buf); const body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF only.txt {s}\n", .{payload_id}); defer t.allocator.free(body); var target_id_buf: [64]u8 = undefined; const target_id = try putCompleteTestArtifact(t.allocator, &log, body, &target_id_buf); try checkout.write(io, tmp.dir, state); try t.expectError( error.CurrentStateInvalid, applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit), ); } } test "update materializes symlink targets and transitions them with regular files" { const t = std.testing; var tmp = t.tmpDir(.{}); defer tmp.cleanup(); const io = t.io; var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true }); defer raw_file.close(io); var file_storage = storage.FileStorage.init(io, raw_file, 0); var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0); defer log.deinit(t.allocator); var link_target_id_buf: [64]u8 = undefined; const link_target_id = try putTestPayload(&log, "../actual-target", &link_target_id_buf); var regular_id_buf: [64]u8 = undefined; const regular_id = try putTestPayload(&log, "regular replacement\n", ®ular_id_buf); const link_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF entry {s} l\n", .{link_target_id}); defer t.allocator.free(link_body); var link_id_buf: [64]u8 = undefined; const link_id = try putCompleteTestArtifact(t.allocator, &log, link_body, &link_id_buf); const regular_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF entry {s}\n", .{regular_id}); defer t.allocator.free(regular_body); var regular_checkin_id_buf: [64]u8 = undefined; const regular_checkin_id = try putCompleteTestArtifact(t.allocator, &log, regular_body, ®ular_checkin_id_buf); try applyCheckin(t.allocator, io, tmp.dir, &log, link_id, .inherit); try expectSymlinkTarget(io, tmp.dir, "entry", "../actual-target"); try applyCheckin(t.allocator, io, tmp.dir, &log, regular_checkin_id, .inherit); try expectFileState(t.allocator, io, tmp.dir, "entry", "regular replacement\n", .regular); try applyCheckin(t.allocator, io, tmp.dir, &log, link_id, .inherit); try expectSymlinkTarget(io, tmp.dir, "entry", "../actual-target"); }