Skip to content
zit Artifact [d4dbe7a61a]
Login

Artifact [d4dbe7a61a]

Artifact d4dbe7a61a3ab2c0d4d923921c86ac9c78eb5e5aa629f6c1513221d8b040747d:


//! 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 repeat = @import("repeat.zig").repeat;
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,
        repeat("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-tmp-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,
        },
        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 },
    };

    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 = repeat("a", 40),
            .target_phantom => {
                target_id = repeat("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, repeat("c", 40), &target_id_buf);
            },
            .baseline_phantom => {
                const baseline_id = repeat("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);
            },
        }

        try t.expectError(case.expected, applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .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, .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 over a delta whose baseline is a delta merges one level, as Fossil does" {
    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 v1_id_buf: [64]u8 = undefined;
    const v1_id = try putTestPayload(&log, "v1 bytes", &v1_id_buf);
    var v2_id_buf: [64]u8 = undefined;
    const v2_id = try putTestPayload(&log, "v2 bytes", &v2_id_buf);

    // The erratum's Fossil 2.28 experiment: base(a.txt, b.txt, c.txt) <- d1
    // (changes a.txt) <- d2 (changes b.txt, B card names d1). `fossil ls -r
    // d2` lists a.txt and b.txt; c.txt, inherited by d1 from base, is absent.
    const base_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF a.txt {s}\nF b.txt {s}\nF c.txt {s}\n", .{ v1_id, v1_id, v1_id });
    defer t.allocator.free(base_body);
    var base_id_buf: [64]u8 = undefined;
    const base_id = try putCompleteTestArtifact(t.allocator, &log, base_body, &base_id_buf);
    const d1_body = try std.fmt.allocPrint(t.allocator, "B {s}\nD 2007-07-21T14:10:00\nF a.txt {s}\n", .{ base_id, v2_id });
    defer t.allocator.free(d1_body);
    var d1_id_buf: [64]u8 = undefined;
    const d1_id = try putCompleteTestArtifact(t.allocator, &log, d1_body, &d1_id_buf);
    const d2_body = try std.fmt.allocPrint(t.allocator, "B {s}\nD 2007-07-21T14:11:00\nF b.txt {s}\n", .{ d1_id, v2_id });
    defer t.allocator.free(d2_body);
    var d2_id_buf: [64]u8 = undefined;
    const d2_id = try putCompleteTestArtifact(t.allocator, &log, d2_body, &d2_id_buf);

    try applyCheckin(t.allocator, io, tmp.dir, &log, d2_id, .inherit, .inherit);
    try expectFileState(t.allocator, io, tmp.dir, "a.txt", "v2 bytes", .regular);
    try expectFileState(t.allocator, io, tmp.dir, "b.txt", "v2 bytes", .regular);
    try t.expectError(error.FileNotFound, tmp.dir.statFile(io, "c.txt", .{ .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("a.txt"));
    try t.expect(current_managed.contains("b.txt"));
}

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, .inherit);
    const before = try tmp.dir.statFile(io, "stable", .{ .follow_symlinks = false });

    try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .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 the skip optimization must preserve: 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, .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, .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, .inherit);
        switch (variant) {
            .intact => {},
            .absent => try tmp.dir.deleteFile(io, ".zit-cache/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-cache/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-cache/vstat", .{ .truncate = true });
                defer rw.close(io);
                try rw.writePositionalAll(io, buf, 0);
            },
        }

        try applyCheckin(t.allocator, io, tmp.dir, &log, target_id, .inherit, .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, .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, .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, .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, .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, .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, .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, .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 = repeat("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, .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", &regular_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, &regular_checkin_id_buf);

    try applyCheckin(t.allocator, io, tmp.dir, &log, link_id, .inherit, .inherit);
    try expectSymlinkTarget(io, tmp.dir, "entry", "../actual-target");

    try applyCheckin(t.allocator, io, tmp.dir, &log, regular_checkin_id, .inherit, .inherit);
    try expectFileState(t.allocator, io, tmp.dir, "entry", "regular replacement\n", .regular);

    try applyCheckin(t.allocator, io, tmp.dir, &log, link_id, .inherit, .inherit);
    try expectSymlinkTarget(io, tmp.dir, "entry", "../actual-target");
}

/// A `CarryResolver` that records what the scan handed it and answers with a
/// fixed carry set. Lets a test observe the divergence list on the real path
/// the CLI uses, rather than through a second entry point that exists only for
/// tests to call.
const RecordingResolver = struct {
    gpa: std.mem.Allocator,
    seen: std.ArrayList(update.Divergence) = .empty,
    answer: []const update.Carry = &.{},
    refuse: bool = false,

    fn deinit(self: *RecordingResolver) void {
        for (self.seen.items) |d| self.gpa.free(d.path);
        self.seen.deinit(self.gpa);
    }

    fn resolver(self: *RecordingResolver) update.CarryResolver {
        return .{ .ctx = self, .resolve = resolve };
    }

    fn resolve(
        ctx: *anyopaque,
        divergences: []const update.Divergence,
    ) error{ OutOfMemory, Refused }![]const update.Carry {
        const self: *RecordingResolver = @ptrCast(@alignCast(ctx));
        for (divergences) |d| {
            try self.seen.append(self.gpa, .{
                .path = try self.gpa.dupe(u8, d.path),
                .kind = d.kind,
            });
        }
        if (self.refuse) return error.Refused;
        return self.answer;
    }
};

test "a carrying update sees every kind of divergence instead of stopping at the first" {
    const t = std.testing;
    const io = t.io;
    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 a_buf: [64]u8 = undefined;
    const a_id = try putTestPayload(&log, "alpha", &a_buf);
    var b_buf: [64]u8 = undefined;
    const b_id = try putTestPayload(&log, "bravo", &b_buf);
    var c_buf: [64]u8 = undefined;
    const c_id = try putTestPayload(&log, "charlie", &c_buf);
    const body = try std.fmt.allocPrint(
        t.allocator,
        "D 2007-07-21T14:09:59\nF edited {s}\nF gone {s}\nF untouched {s}\n",
        .{ a_id, b_id, c_id },
    );
    defer t.allocator.free(body);
    var checkin_buf: [64]u8 = undefined;
    const checkin = try putCompleteTestArtifact(t.allocator, &log, body, &checkin_buf);
    try applyCheckin(t.allocator, io, tmp.dir, &log, checkin, .inherit, .inherit);

    // One of each: content drift, a deleted managed file, and a path the
    // checkout manages that the check-in has never heard of. The refusing scan
    // stops at whichever it reaches first; this one must report all three.
    try tmp.dir.deleteFile(io, "edited");
    try writeFixturePayload(io, tmp.dir, "edited", "alpha, revised", .regular);
    try tmp.dir.deleteFile(io, "gone");
    try writeFixturePayload(io, tmp.dir, "extra", "not in the check-in", .regular);
    {
        var set = try managed.ManagedSet.read(t.allocator, io, tmp.dir);
        defer set.deinit();
        _ = try set.insert("extra");
        try set.write(io, tmp.dir);
    }

    var rec = RecordingResolver{ .gpa = t.allocator, .refuse = true };
    defer rec.deinit();
    // Refusing keeps the tree untouched, so the assertions below are about the
    // scan alone.
    try t.expectError(error.WorktreeNotClean, update.applyCheckinWith(t.allocator, io, tmp.dir, &log, checkin, .inherit, .inherit, null, rec.resolver()));

    try t.expectEqual(@as(usize, 3), rec.seen.items.len);
    // Ascending by path, so a caller that reports the list needs no sort.
    try t.expectEqualStrings("edited", rec.seen.items[0].path);
    try t.expectEqual(update.DivergenceKind.modified, rec.seen.items[0].kind);
    try t.expectEqualStrings("extra", rec.seen.items[1].path);
    try t.expectEqual(update.DivergenceKind.added, rec.seen.items[1].kind);
    try t.expectEqualStrings("gone", rec.seen.items[2].path);
    try t.expectEqual(update.DivergenceKind.missing, rec.seen.items[2].kind);

    // "untouched" is clean and therefore absent: the list is divergences, not
    // an inventory.
    for (rec.seen.items) |d| try t.expect(!std.mem.eql(u8, d.path, "untouched"));
}

test "a clean tree never reaches the resolver, so carrying costs it nothing" {
    const t = std.testing;
    const io = t.io;
    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 v1_buf: [64]u8 = undefined;
    const v1 = try putTestPayload(&log, "version one", &v1_buf);
    var v2_buf: [64]u8 = undefined;
    const v2 = try putTestPayload(&log, "version two", &v2_buf);
    const base_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF moving {s}\n", .{v1});
    defer t.allocator.free(base_body);
    var base_buf: [64]u8 = undefined;
    const base = try putCompleteTestArtifact(t.allocator, &log, base_body, &base_buf);
    const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF moving {s}\n", .{v2});
    defer t.allocator.free(target_body);
    var target_buf: [64]u8 = undefined;
    const target = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_buf);

    try applyCheckin(t.allocator, io, tmp.dir, &log, base, .inherit, .inherit);

    // The resolver is a policy for divergence. A clean tree has none, so it is
    // never consulted -- the property that keeps the one extra scan from
    // costing a clean update anything.
    var rec = RecordingResolver{ .gpa = t.allocator, .refuse = true };
    defer rec.deinit();
    try update.applyCheckinWith(t.allocator, io, tmp.dir, &log, target, .inherit, .inherit, null, rec.resolver());
    try t.expectEqual(@as(usize, 0), rec.seen.items.len);
    try expectFileState(t.allocator, io, tmp.dir, "moving", "version two", .regular);
}

test "a carried update materializes the carried bytes and still moves the checkout" {
    const t = std.testing;
    const io = t.io;
    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 v1_buf: [64]u8 = undefined;
    const v1 = try putTestPayload(&log, "version one", &v1_buf);
    var v2_buf: [64]u8 = undefined;
    const v2 = try putTestPayload(&log, "version two", &v2_buf);
    const base_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF moving {s}\n", .{v1});
    defer t.allocator.free(base_body);
    var base_buf: [64]u8 = undefined;
    const base = try putCompleteTestArtifact(t.allocator, &log, base_body, &base_buf);
    const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nF moving {s}\n", .{v2});
    defer t.allocator.free(target_body);
    var target_buf: [64]u8 = undefined;
    const target = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_buf);

    try applyCheckin(t.allocator, io, tmp.dir, &log, base, .inherit, .inherit);
    try tmp.dir.deleteFile(io, "moving");
    try writeFixturePayload(io, tmp.dir, "moving", "locally edited", .regular);

    // Without a resolver this is exactly the refusal the clean path has always
    // made — the guard is not gone, it is opted out of per call.
    try t.expectError(
        error.WorktreeNotClean,
        applyCheckin(t.allocator, io, tmp.dir, &log, target, .inherit, .inherit),
    );

    const carry = [_]update.Carry{.{
        .path = "moving",
        .after = .{ .bytes = "merged result" },
        .before = .{ .bytes = "locally edited" },
    }};
    var rec = RecordingResolver{ .gpa = t.allocator, .answer = &carry };
    defer rec.deinit();
    try update.applyCheckinWith(t.allocator, io, tmp.dir, &log, target, .inherit, .inherit, null, rec.resolver());

    // The carry wins over the target's own content for that path...
    try expectFileState(t.allocator, io, tmp.dir, "moving", "merged result", .regular);
    // ...and the checkout still moved, which is the difference between this and
    // a merge: no merge parent, just a new version.
    var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined;
    const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?;
    try t.expectEqualStrings(target, state.version);
    try t.expectEqualStrings("", state.merge_parent);

    // The stat sidecar must not claim the carried file matches the target's
    // digest, or the next scan would call a locally-edited file clean.
    var rec2 = RecordingResolver{ .gpa = t.allocator, .refuse = true };
    defer rec2.deinit();
    try t.expectError(error.WorktreeNotClean, update.applyCheckinWith(t.allocator, io, tmp.dir, &log, target, .inherit, .inherit, null, rec2.resolver()));
    try t.expectEqual(@as(usize, 1), rec2.seen.items.len);
    try t.expectEqualStrings("moving", rec2.seen.items[0].path);
    try t.expectEqual(update.DivergenceKind.modified, rec2.seen.items[0].kind);
}