Artifact 79f3f9ee2a2c625abdcace58f18068a07829a86bb09158c41175bd7e7b802ea3:
- File
src/repostate_test.zig
— part of check-in
[c8c7c2696e]
at
2026-08-02 14:29:47
on branch sub-manifest
— repostate: branchValueOf reads the configured main branch, not "trunk"
repo-state §6.1 requires reading the configured value and falling back to trunk only when main-branch is unset -- "MUST NOT assume a fixed string in the presence of an explicit setting". branchValueOf, which backs the §7.2 same-branch leaf test, used the literal.
It hid because all three uses are comparisons: when both sides fall back identically the result is unchanged. It bites when one check-in is tagged and another is not, which is the imported-repository shape -- displayed as one branch by branchOf, leaf-tested as another by branchValueOf.
The name is now a field on RepoState, set through one RepoView setter so the two readers cannot drift, with branchOf asserting its argument agrees. That assert immediately found a second source: update.zig re-read the config itself and passed a value the state never saw. It also treats an empty stored name as unset, as the view already did. (user: dmestas, size: 80757) [annotate] [blame] [check-ins using]
const std = @import("std"); const repostate = @import("repostate.zig"); const delta = @import("delta.zig"); const Rid = repostate.Rid; const Mtime = repostate.Mtime; const TagType = repostate.TagType; const MemStore = repostate.MemStore; const RepoState = repostate.RepoState; const parseMtime = repostate.parseMtime; const t = std.testing; const h_a = "a" ** 64; const h_b = "b" ** 64; const h_c = "c" ** 64; const h_d = "d" ** 64; const h_e = "e" ** 64; const h_g = "0" ** 64; fn countLeaves(rs: *const RepoState) usize { var it = rs.leaves(); var n: usize = 0; while (it.next()) |_| n += 1; return n; } test "repostate §2: intern assigns one stable rid per hash" { var ms = MemStore.init(t.allocator); defer ms.deinit(); const s = ms.store(); const ra = try s.intern(h_a); const rb = try s.intern(h_b); try t.expect(ra != rb); try t.expectEqual(ra, try s.intern(h_a)); try t.expectEqualStrings(h_a, s.hashOf(ra)); try t.expectEqualStrings(h_b, s.hashOf(rb)); } test "repostate §3.2: count enumerates dense rids so a peer can classify each entry as phantom or held" { var ms = MemStore.init(t.allocator); defer ms.deinit(); const s = ms.store(); try t.expectEqual(@as(usize, 0), s.count()); _ = try s.intern(h_a); // phantom _ = try s.put(h_b, "held"); // content present _ = try s.intern(h_c); // phantom try t.expectEqual(@as(usize, 3), s.count()); // Walk 1..=count and classify via content(rid): two phantoms, one held. var phantoms: usize = 0; var held: usize = 0; var rid: Rid = 1; while (rid <= s.count()) : (rid += 1) { if (s.content(rid) == null) phantoms += 1 else held += 1; } try t.expectEqual(@as(usize, 2), phantoms); try t.expectEqual(@as(usize, 1), held); } test "repostate §4.2: an interned but absent artifact is a phantom with no content" { var ms = MemStore.init(t.allocator); defer ms.deinit(); const s = ms.store(); const rid = try s.intern(h_a); try t.expectEqual(@as(?[]const u8, null), s.content(rid)); // The referenced artifact arrives: the phantom upgrades in place. try t.expectEqual(rid, try s.put(h_a, "payload")); try t.expectEqualStrings("payload", s.content(rid).?); // A repeat put of the same hash is a no-op. try t.expectEqual(rid, try s.put(h_a, "payload")); try t.expectEqualStrings("payload", s.content(rid).?); } test "repostate §5.1: tag types encode cancel=0, singleton=1, propagate=2" { try t.expectEqual(0, @intFromEnum(TagType.cancel)); try t.expectEqual(1, @intFromEnum(TagType.singleton)); try t.expectEqual(2, @intFromEnum(TagType.propagate)); } test "repostate §3.1: a repeated parent reference collapses to one linkage row" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{ h_a, h_a } }); try t.expectEqual(@as(usize, 1), rs.parentCount(rb)); try t.expectEqual(@as(?Rid, ra), rs.primaryParentOf(rb)); try t.expect(!rs.isLeaf(ra)); try t.expect(rs.isLeaf(rb)); } test "repostate §3.2: the first listed parent is the primary parent" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); _ = try rs.crosslink(.{ .hash = h_b, .mtime = 2 }); const rc = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{ h_a, h_b } }); try t.expectEqual(@as(usize, 2), rs.parentCount(rc)); try t.expectEqual(@as(?Rid, ra), rs.primaryParentOf(rc)); } test "repostate §3.3: parentsOf yields the primary and every merge parent once" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2 }); // A merge child of two independent roots: primary ra, merge rb (§3.2). const rc = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{ h_a, h_b, h_a } }); // §3.1: the repeated parent collapses, so both parents are yielded once. var seen_a: usize = 0; var seen_b: usize = 0; var n: usize = 0; var it = rs.parentsOf(rc); while (it.next()) |p| { n += 1; if (p == ra) seen_a += 1; if (p == rb) seen_b += 1; } try t.expectEqual(@as(usize, 2), n); try t.expectEqual(@as(usize, 1), seen_a); try t.expectEqual(@as(usize, 1), seen_b); try t.expectEqual(rs.parentCount(rc), n); // §3.3: same set parentCount counts // A root yields nothing. var root_it = rs.parentsOf(ra); try t.expectEqual(@as(?Rid, null), root_it.next()); } test "repostate §3.2: a sole parent is the primary parent" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); try t.expectEqual(@as(?Rid, ra), rs.primaryParentOf(rb)); } test "repostate §4.2: re-crosslinking a linked artifact is a linkage no-op" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); const rb_again = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); try t.expectEqual(rb, rb_again); try t.expectEqual(@as(usize, 1), rs.parentCount(rb)); try t.expectEqual(@as(usize, 1), countLeaves(&rs)); try t.expect(rs.isLeaf(rb)); } test "repostate §4.2: an absent parent hash yields a phantom, resolved when it arrives" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // The child arrives first; its parent hash resolves to a phantom // placeholder rather than a fatal error. const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); const ra = rs.primaryParentOf(rb).?; try t.expectEqual(@as(?Mtime, null), rs.mtimeOf(ra)); // A phantom is not a check-in: it never enters the leaf table. try t.expect(!rs.isLeaf(ra)); try t.expect(rs.isLeaf(rb)); try t.expectEqual(@as(usize, 1), countLeaves(&rs)); // The referenced artifact arrives and is crosslinked: the recorded // linkage becomes visible in the derived views. try t.expectEqual(ra, try rs.crosslink(.{ .hash = h_a, .mtime = 1 })); try t.expectEqual(@as(?Mtime, 1), rs.mtimeOf(ra)); try t.expect(!rs.isLeaf(ra)); try t.expect(rs.isLeaf(rb)); try t.expectEqual(@as(usize, 1), countLeaves(&rs)); } test "repostate §7.2: a child reached only via a merge edge disqualifies the leaf" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); const rc = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a} }); const rd = try rs.crosslink(.{ .hash = h_d, .mtime = 4, .parents = &.{ h_b, h_c } }); // rc's only child rd is reached via a non-primary (merge) edge; the // canonical test applies no primary/merge filter. try t.expect(!rs.isLeaf(rb)); try t.expect(!rs.isLeaf(rc)); try t.expect(rs.isLeaf(rd)); try t.expectEqual(@as(usize, 1), countLeaves(&rs)); } test "repostate §7.3: a root check-in with no children is a leaf" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); try t.expect(rs.isLeaf(ra)); var it = rs.leaves(); try t.expectEqual(@as(?Rid, ra), it.next()); try t.expectEqual(@as(?Rid, null), it.next()); } test "repostate §7.3: a new child retroactively removes its parent's leaf status" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); try t.expect(rs.isLeaf(ra)); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); try t.expect(!rs.isLeaf(ra)); try t.expect(rs.isLeaf(rb)); } test "repostate §7.3: full rebuild reproduces the incrementally maintained table" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); const rc = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a} }); _ = try rs.crosslink(.{ .hash = h_d, .mtime = 4, .parents = &.{ h_b, h_c } }); // An independent root with no children: a candidate the rebuild MUST // include even though it appears in no parent-link. const re = try rs.crosslink(.{ .hash = h_e, .mtime = 5 }); var before: [8]Rid = undefined; const n_before = collectLeaves(&rs, &before); try rs.rebuildLeaves(); var after: [8]Rid = undefined; const n_after = collectLeaves(&rs, &after); try t.expectEqual(n_before, n_after); std.mem.sort(Rid, before[0..n_before], {}, std.sort.asc(Rid)); std.mem.sort(Rid, after[0..n_after], {}, std.sort.asc(Rid)); try t.expectEqualSlices(Rid, before[0..n_before], after[0..n_after]); try t.expect(rs.isLeaf(re)); try t.expect(!rs.isLeaf(rb)); try t.expect(!rs.isLeaf(rc)); } test "repostate §7.2/§6.1: a fork onto a new branch leaves the parent a leaf of its own branch" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a}, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "feature" }, } }); try t.expect(rs.isLeaf(ra)); try t.expect(rs.isLeaf(rb)); } test "repostate §7.6: directly applying a branch tag re-evaluates leaf status" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); _ = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); try t.expect(!rs.isLeaf(ra)); // same branch, has a child _ = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = h_b, .value = "feature" }, } }); try t.expect(rs.isLeaf(ra)); } test "repostate §7.6: branch propagation re-evaluates a descendant's leaf status" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); // C carries its own direct branch tag, so it blocks any further // propagation past it (Section 5.5: a nonzero-srcid row is never // overwritten by an inherited one). const rc = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_b}, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, } }); try t.expect(!rs.isLeaf(ra)); try t.expect(!rs.isLeaf(rb)); // same branch (trunk) as its child C try t.expect(rs.isLeaf(rc)); // A, a pre-existing check-in with a child already in place, now gets a // branch tag. The walk reaches B — B carries no direct branch row of // its own, so it inherits "feature" — while C's direct "trunk" row // blocks the walk from reaching any further. B did not itself get // crosslinked by this call, so nothing but the propagation step re- // evaluates it. Derivation Section 7.6 second bullet: "when a branch // tag propagates to descendants, each descendant that gains or loses // the propagating tag MUST have its leaf status re-evaluated." _ = try rs.crosslink(.{ .hash = h_a, .mtime = 10, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "feature" }, } }); try t.expect(!rs.isLeaf(ra)); // A still shares a branch with child B try t.expect(rs.isLeaf(rb)); // B's only child C stayed on trunk: B gained leaf status try t.expect(rs.isLeaf(rc)); } test "repostate §7.4: a leaf carrying an active closed marker is not an open leaf" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // §6.1 is one repository-wide fact; `branchOf`'s argument and the // state's own field are two routes to it, and the assert in `branchOf` // requires they agree. Production sets both through `RepoView`. rs.main_branch = "main"; const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .singleton, .name = "closed", .target = null, .value = null }, } }); try t.expect(rs.isLeaf(ra)); try t.expect(!rs.isOpenLeaf(ra)); } test "repostate §7.4: cancelling the closed marker reopens the leaf" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // §6.1 is one repository-wide fact; `branchOf`'s argument and the // state's own field are two routes to it, and the assert in `branchOf` // requires they agree. Production sets both through `RepoView`. rs.main_branch = "main"; const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 5, .tags = &.{ .{ .type = .singleton, .name = "closed", .target = null, .value = null }, } }); try t.expect(!rs.isOpenLeaf(ra)); _ = try rs.crosslink(.{ .hash = h_b, .mtime = 6, .tags = &.{ .{ .type = .cancel, .name = "closed", .target = h_a, .value = null }, } }); try t.expect(rs.isOpenLeaf(ra)); } test "repostate §6.1: branchOf returns the active branch-tag value" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // §6.1 is one repository-wide fact; `branchOf`'s argument and the // state's own field are two routes to it, and the assert in `branchOf` // requires they agree. Production sets both through `RepoView`. rs.main_branch = "main"; const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, } }); try t.expectEqualStrings("trunk", rs.branchOf(ra, "main")); } test "repostate §6.1/§6.4: a check-in with no branch tag falls back to the configured main-branch" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // §6.1 is one repository-wide fact; `branchOf`'s argument and the // state's own field are two routes to it, and the assert in `branchOf` // requires they agree. Production sets both through `RepoView`. rs.main_branch = "main"; const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); // "main", not the literal "trunk" — proves the configured value is // read, not a hardcoded default (§6.1 "MUST NOT assume a fixed string"). try t.expectEqualStrings("main", rs.branchOf(ra, "main")); } test "repostate §6.4: a cancelled branch tag is not active and falls back" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // §6.1 is one repository-wide fact; `branchOf`'s argument and the // state's own field are two routes to it, and the assert in `branchOf` // requires they agree. Production sets both through `RepoView`. rs.main_branch = "main"; const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .cancel, .name = "branch", .target = null, .value = null }, } }); try t.expectEqualStrings("main", rs.branchOf(ra, "main")); } test "repostate §8.2: resolves to the active sym-name carrier with the greatest event.mtime" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" }, } }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 5, .parents = &.{h_a} }); // B inherits the propagating sym-trunk tag; resolution keys on each // carrier's own event.mtime (Section 8.2), not the tag row's stored // mtime, so B's later commit time outranks the tag's origin A. try t.expectEqual(rb, try rs.resolveName("trunk")); } test "repostate §8.2: closed-ness does not filter resolution" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" }, } }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 5, .parents = &.{h_a}, .tags = &.{ .{ .type = .singleton, .name = "closed", .target = null, .value = null }, } }); try t.expect(rs.isLeaf(rb)); try t.expect(!rs.isOpenLeaf(rb)); try t.expectEqual(rb, try rs.resolveName("trunk")); } test "repostate §8.2: leaf-ness does not filter resolution" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // Singleton keeps the tag off B, isolating "a non-leaf still resolves" // from Section 5.4 inheritance mechanics. const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 10, .tags = &.{ .{ .type = .singleton, .name = "sym-trunk", .target = null, .value = "trunk" }, } }); _ = try rs.crosslink(.{ .hash = h_b, .mtime = 5, .parents = &.{h_a} }); try t.expect(!rs.isLeaf(ra)); try t.expectEqual(ra, try rs.resolveName("trunk")); } test "repostate §8.2: an unknown symbolic name is rejected as NotFound" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); try t.expectError(error.NotFound, rs.resolveName("nope")); } test "repostate §8.2: a name whose only sym-tag row is cancelled is NotFound" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .cancel, .name = "sym-trunk", .target = null, .value = null }, } }); try t.expectError(error.NotFound, rs.resolveName("trunk")); } test "repostate §9/§2: current-trunk resolves the configured default branch name" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" }, } }); try t.expectEqual(ra, try rs.currentTrunk("trunk")); try t.expectEqual(try rs.resolveName("trunk"), try rs.currentTrunk("trunk")); } test "repostate §9: current-trunk of an unknown default branch is NotFound" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); try t.expectError(error.NotFound, rs.currentTrunk("nope")); } test "repostate §9: the open leaf carries an active branch tag, is a leaf, and is not closed" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, } }); try t.expectEqual(@as(?Rid, ra), rs.openLeafOf("trunk")); } test "repostate §9/§6.1: a tagless leaf is not the open leaf of the default branch" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); // Attributed to "trunk" only through the default-branch fallback: not // the open leaf of trunk (§6.1's fallback caveat). try t.expectEqual(@as(?Rid, null), rs.openLeafOf("trunk")); } test "repostate §9: a non-leaf on the branch is not an open leaf" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, } }); // B, a same-branch child, disqualifies A under condition (b) even // though A meets (a) and (c); B — the actual leaf — wins instead. const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); try t.expect(!rs.isLeaf(ra)); try t.expectEqual(@as(?Rid, rb), rs.openLeafOf("trunk")); } test "repostate §9: a closed leaf is not an open leaf" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, .{ .type = .singleton, .name = "closed", .target = null, .value = null }, } }); try t.expect(rs.isLeaf(ra)); try t.expectEqual(@as(?Rid, null), rs.openLeafOf("trunk")); } test "repostate §9: with several open leaves the greatest mtime is selected" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, } }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); const rc = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a} }); try t.expect(rs.isLeaf(rb)); try t.expect(rs.isLeaf(rc)); try t.expectEqual(@as(?Rid, rc), rs.openLeafOf("trunk")); } test "repostate §9: a branch with no qualifying check-in has no open leaf" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, .{ .type = .singleton, .name = "closed", .target = null, .value = null }, } }); try t.expectEqual(@as(?Rid, null), rs.openLeafOf("trunk")); } test "repostate §10.1: fork at trunk with one closed leaf — resolution and open-leaf agree" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" }, } }); const open_leaf = try rs.crosslink(.{ .hash = h_b, .mtime = 3, .parents = &.{h_a} }); const closed_leaf = try rs.crosslink(.{ .hash = h_c, .mtime = 2, .parents = &.{h_a}, .tags = &.{ .{ .type = .singleton, .name = "closed", .target = null, .value = null }, } }); _ = try rs.crosslink(.{ .hash = h_d, .mtime = 4, .parents = &.{h_a}, .tags = &.{ .{ .type = .cancel, .name = "sym-trunk", .target = null, .value = null }, .{ .type = .propagate, .name = "branch", .target = null, .value = "feature" }, .{ .type = .propagate, .name = "sym-feature", .target = null, .value = "feature" }, } }); try t.expect(rs.isLeaf(open_leaf)); try t.expect(rs.isOpenLeaf(open_leaf)); try t.expect(rs.isLeaf(closed_leaf)); try t.expect(!rs.isOpenLeaf(closed_leaf)); // §10.1: leaves lists the open trunk leaf while omitting the closed // sibling; whatis trunk agrees with that same open leaf here. try t.expectEqual(open_leaf, try rs.resolveName("trunk")); try t.expectEqual(@as(?Rid, open_leaf), rs.openLeafOf("trunk")); } test "repostate §10.2: a closed leaf newer than the open leaf — resolution and open-leaf diverge" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" }, } }); const open_leaf = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); const closed_leaf = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a}, .tags = &.{ .{ .type = .singleton, .name = "closed", .target = null, .value = null }, } }); try t.expect(rs.isOpenLeaf(open_leaf)); try t.expect(rs.isLeaf(closed_leaf)); try t.expect(!rs.isOpenLeaf(closed_leaf)); // §10.2: whatis trunk reports the closed, newer check-in even though // the default open-leaf view omits it — resolution and open-leaf // enumeration diverge on this graph. try t.expectEqual(closed_leaf, try rs.resolveName("trunk")); try t.expectEqual(@as(?Rid, open_leaf), rs.openLeafOf("trunk")); try t.expect((try rs.resolveName("trunk")) != (try rs.openLeafOf("trunk")).?); } fn collectLeaves(rs: *const RepoState, out: []Rid) usize { var it = rs.leaves(); var n: usize = 0; while (it.next()) |rid| : (n += 1) out[n] = rid; return n; } fn crosslinkNoLeak(gpa: std.mem.Allocator) !void { var ms = MemStore.init(gpa); defer ms.deinit(); var rs = RepoState.init(gpa, ms.store()); defer rs.deinit(); // Self-branch root plus a tag on a not-yet-stored (phantom) target, so // internTag/applyDirect/propagate all allocate. _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "feature" }, .{ .type = .propagate, .name = "t1", .target = h_g, .value = "v" }, } }); _ = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); // Phantom parent plus a merge, so every allocation site is exercised. _ = try rs.crosslink(.{ .hash = h_d, .mtime = 4, .parents = &.{ h_b, h_c } }); // A cancel and a singleton (+closed), exercising the value-clear and // downgrade-at-repropagation paths. _ = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a}, .tags = &.{ .{ .type = .cancel, .name = "t1", .target = null, .value = null }, .{ .type = .singleton, .name = "closed", .target = null, .value = null }, } }); _ = try rs.crosslink(.{ .hash = h_g, .mtime = 5 }); // the phantom target arrives try rs.rebuildLeaves(); } test "repostate §4.2: crosslink survives allocation failure without leaking" { try t.checkAllAllocationFailures(t.allocator, crosslinkNoLeak, .{}); } /// Fork topology reused across the root:/start:/merge-in:/branch-type /// tests: A (self-tagged trunk) <- B (plain, inherits trunk) <- C /// (self-tagged feature, diverging from trunk at B) <- D (plain, inherits /// feature). const TopoT = struct { a: Rid, b: Rid, c: Rid, d: Rid }; fn buildTopoT(rs: *RepoState) !TopoT { const a = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" }, .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, } }); const b = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); const c = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_b}, .tags = &.{ .{ .type = .propagate, .name = "sym-feature", .target = null, .value = "feature" }, .{ .type = .propagate, .name = "branch", .target = null, .value = "feature" }, } }); const d = try rs.crosslink(.{ .hash = h_d, .mtime = 4, .parents = &.{h_c} }); return .{ .a = a, .b = b, .c = c, .d = d }; } test "repostate §8.3: rid: resolves a numeric RID to a known check-in" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const topo = try buildTopoT(&rs); var buf: [32]u8 = undefined; const name = try std.fmt.bufPrint(&buf, "rid:{d}", .{topo.b}); try t.expectEqual(topo.b, try rs.resolve(name, .checkin)); } test "repostate §8.3: rid: an unknown or non-numeric RID is NotFound" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const cases = [_][]const u8{ "rid:999", "rid:xyz" }; for (cases) |name| { try t.expectError(error.NotFound, rs.resolve(name, .checkin)); } } test "repostate §8.3: tag: forces literal tag resolution over hash interpretation" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const h_hash_match = "dead" ++ "2" ** 60; const rx = try rs.crosslink(.{ .hash = h_hash_match, .mtime = 1 }); const ry = try rs.crosslink(.{ .hash = h_c, .mtime = 2, .tags = &.{ .{ .type = .propagate, .name = "sym-dead", .target = null, .value = "dead" }, } }); try t.expectEqual(rx, try rs.resolve("dead", .checkin)); try t.expectEqual(ry, try rs.resolve("tag:dead", .checkin)); try t.expect((try rs.resolve("dead", .checkin)) != (try rs.resolve("tag:dead", .checkin))); } test "repostate §8.3: tip resolves the global maximum event.mtime check-in" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const topo = try buildTopoT(&rs); try t.expectEqual(topo.d, try rs.resolve("tip", .checkin)); } test "repostate §8.3/§8.4: root: rewrites to the first ancestor off the branch" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const topo = try buildTopoT(&rs); // Walk D -> C[feature] -> B[trunk]: B is the first ancestor off feature. try t.expectEqual(topo.b, try rs.resolve("root:feature", .checkin)); } test "repostate §8.3/§8.4: start: rewrites to the earliest check-in on the branch" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const topo = try buildTopoT(&rs); try t.expectEqual(topo.c, try rs.resolve("start:feature", .checkin)); } test "repostate §8.3: merge-in: rewrites to the youngest parent-branch ancestor" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const topo = try buildTopoT(&rs); // Section 8.4's two-variant walk gives merge-in: the same node as // root: here: the youngest ancestor on the parent branch is exactly // the first ancestor found off the diverging branch. try t.expectEqual(topo.b, try rs.resolve("merge-in:feature", .checkin)); } test "repostate §8.3: branch-type request post-processes to the branch root" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const topo = try buildTopoT(&rs); try t.expectEqual(topo.c, try rs.resolve("feature", .branch)); } test "repostate §8.3: an ambiguous hash prefix is rejected" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const h1 = "abcd" ++ "1" ** 60; const h2 = "abcd" ++ "2" ** 60; _ = try rs.crosslink(.{ .hash = h1, .mtime = 1 }); _ = try rs.crosslink(.{ .hash = h2, .mtime = 2 }); try t.expectError(error.Ambiguous, rs.resolve("abcd", .checkin)); } test "repostate §8.3: a unique hash prefix resolves to its single check-in" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); try t.expectEqual(ra, try rs.resolve("aaaa", .checkin)); } test "repostate §8.3: a trailing punctuation character is stripped and retried" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" }, } }); try t.expectEqual(try rs.resolve("trunk", .checkin), try rs.resolve("trunk.", .checkin)); } test "repostate §8.3: a name with no matching form is NotFound" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); // "zzz": no form matches and no punctuation to strip. try t.expectError(error.NotFound, rs.resolve("zzz", .checkin)); // "..": the punct-retry exhausts down to an empty slice. try t.expectError(error.NotFound, rs.resolve("..", .checkin)); } test "repostate §8.3: plain name falls through to the §8.2 Rule" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" }, } }); try t.expectEqual(try rs.resolveName("trunk"), try rs.resolve("trunk", .checkin)); } test "repostate §8.3: a sym-prefixed selector preserves a user sym- tag" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const user_tag = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .singleton, .name = "sym-sym-foo", .target = null, .value = null }, } }); try t.expectEqual(user_tag, try rs.resolve("sym-foo", .checkin)); } test "repostate §8.3: a sym-prefixed selector falls back to imported canonical spelling" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const imported = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .singleton, .name = "sym-heads/main", .target = null, .value = null }, } }); try t.expectEqual(imported, try rs.resolve("sym-heads/main", .checkin)); } test "repostate §8.3: a user sym- tag takes precedence over imported canonical spelling" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const user_tag = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .singleton, .name = "sym-sym-foo", .target = null, .value = null }, } }); _ = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .tags = &.{ .{ .type = .singleton, .name = "sym-foo", .target = null, .value = null }, } }); try t.expectEqual(user_tag, try rs.resolve("sym-foo", .checkin)); } test "repostate §8.3: a missing sym-prefixed selector is NotFound" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); try t.expectError(error.NotFound, rs.resolve("sym-missing", .checkin)); } test "repostate §8.3: a sym-prefixed selector propagates allocation failure" { try t.checkAllAllocationFailures(t.allocator, resolveSymPrefixedOom, .{}); } fn resolveSymPrefixedOom(gpa: std.mem.Allocator) !void { var ms = MemStore.init(gpa); defer ms.deinit(); var rs = RepoState.init(gpa, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .singleton, .name = "sym-sym-foo", .target = null, .value = null }, } }); const user_tag = try rs.crosslink(.{ .hash = h_b, .mtime = 1, .parents = &.{h_a}, .tags = &.{ .{ .type = .singleton, .name = "sym-sym-foo", .target = null, .value = null }, } }); _ = try rs.crosslink(.{ .hash = h_c, .mtime = 2, .tags = &.{ .{ .type = .singleton, .name = "sym-foo", .target = null, .value = null }, } }); // The equal-mtime ordinary selector allocates to break its ancestry tie. // checkAllAllocationFailures requires that error.OutOfMemory escape rather // than incorrectly falling back to the imported canonical tag. try t.expectEqual(user_tag, try rs.resolve("sym-foo", .checkin)); } test "repostate §8.4: branch-walk is bounded by the primary-parent chain" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" }, .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, } }); // A root-only branch: root:trunk never finds an ancestor off-branch, so // the walk falls back to the branch root itself. try t.expectEqual(ra, try rs.resolve("root:trunk", .checkin)); } test "repostate §6.3: enumerates one entry per distinct active branch value" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try buildTopoT(&rs); const entries = try rs.branches(t.allocator); defer t.allocator.free(entries); try t.expectEqual(@as(usize, 2), entries.len); try t.expectEqualStrings("feature", entries[0].name); try t.expectEqualStrings("trunk", entries[1].name); } test "repostate §6.3: a branch entry's representative is its greatest-mtime member" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const topo = try buildTopoT(&rs); const entries = try rs.branches(t.allocator); defer t.allocator.free(entries); var trunk: ?RepoState.BranchEntry = null; for (entries) |e| { if (std.mem.eql(u8, e.name, "trunk")) trunk = e; } try t.expectEqual(topo.b, trunk.?.tip); try t.expectEqual(@as(usize, 2), trunk.?.count); try t.expectEqual(@as(Mtime, 2), trunk.?.mtime); } test "repostate §6.3: listing-closed reflects the representative row, diverging from §9 open-leaf" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, } }); const older_leaf = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); const newer_leaf = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a} }); _ = try rs.crosslink(.{ .hash = h_d, .mtime = 5, .tags = &.{ .{ .type = .singleton, .name = "closed", .target = h_c, .value = null }, } }); const entries = try rs.branches(t.allocator); defer t.allocator.free(entries); try t.expectEqual(@as(usize, 1), entries.len); try t.expectEqual(newer_leaf, entries[0].tip); try t.expect(entries[0].closed); try t.expectEqual(@as(?Rid, older_leaf), rs.openLeafOf("trunk")); } test "repostate §6.3: branches survives allocation failure without leaking" { try t.checkAllAllocationFailures(t.allocator, branchesNoLeak, .{}); } fn branchesNoLeak(gpa: std.mem.Allocator) !void { var ms = MemStore.init(gpa); defer ms.deinit(); var rs = RepoState.init(gpa, ms.store()); defer rs.deinit(); _ = try buildTopoT(&rs); const entries = try rs.branches(gpa); gpa.free(entries); } test "repostate §7.5: a non-leaf is never an ambiguous leaf" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); _ = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_b} }); try t.expect(!rs.isAmbiguousLeaf(rb)); } test "repostate §7.5: a lone open leaf is not ambiguous" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); try t.expect(!rs.isAmbiguousLeaf(ra)); } test "repostate §7.5: two open leaves on a branch are each ambiguous" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const rb1 = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); const rb2 = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a} }); try t.expect(rs.isAmbiguousLeaf(rb1)); try t.expect(rs.isAmbiguousLeaf(rb2)); } test "repostate §7.5: a closed sibling does not make a leaf ambiguous" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const rb1 = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); _ = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a}, .tags = &.{ .{ .type = .singleton, .name = "closed", .target = null, .value = null }, } }); try t.expect(!rs.isAmbiguousLeaf(rb1)); } test "repostate §7.5: open-leaf listing is mtime-descending" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, } }); const rb1 = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); const rb2 = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a} }); const leaves_out = try rs.openLeavesOf(t.allocator, "trunk"); defer t.allocator.free(leaves_out); try t.expectEqualSlices(Rid, &.{ rb2, rb1 }, leaves_out); } test "repostate §7.5: openLeavesOf survives allocation failure without leaking" { try t.checkAllAllocationFailures(t.allocator, openLeavesOfNoLeak, .{}); } fn openLeavesOfNoLeak(gpa: std.mem.Allocator) !void { var ms = MemStore.init(gpa); defer ms.deinit(); var rs = RepoState.init(gpa, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, } }); _ = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); _ = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_a} }); const leaves_out = try rs.openLeavesOf(gpa, "trunk"); gpa.free(leaves_out); } test "repostate §8.3 fuzz: dispatch never crashes on adversarial name bytes" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // §6.1 is one repository-wide fact; `branchOf`'s argument and the // state's own field are two routes to it, and the assert in `branchOf` // requires they agree. Production sets both through `RepoView`. rs.main_branch = "trunk"; _ = try buildTopoT(&rs); var prng = std.Random.DefaultPrng.init(0xF055_D47E); const random = prng.random(); // Every internal parser this loop can reach (rid:, hash-prefix, // expandTimestamp, punct-retry) sees adversarial bytes, NUL included // per the wire-data contract; `resolve`'s own postcondition assert // doubles as the invariant check — any bad candidate rid traps under // Debug/ReleaseSafe rather than the assertion being separately // reasserted here. const alphabet = "abcdef0123456789-: .ABCXYZ_/\\\x00"; var buf: [40]u8 = undefined; var i: usize = 0; const iterations = 2000; while (i < iterations) : (i += 1) { const len = random.uintLessThan(usize, buf.len) + 1; for (buf[0..len]) |*c| c.* = alphabet[random.uintLessThan(usize, alphabet.len)]; _ = rs.resolve(buf[0..len], .checkin) catch |err| switch (err) { error.NotFound, error.Ambiguous => {}, error.OutOfMemory => return err, }; } } test "repostate §4.2: control named target changes branch without a carrier event or leaf" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // §6.1 is one repository-wide fact; `branchOf`'s argument and the // state's own field are two routes to it, and the assert in `branchOf` // requires they agree. Production sets both through `RepoView`. rs.main_branch = "trunk"; const parent = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const manifest = try rs.crosslink(.{ .hash = h_c, .mtime = 2, .parents = &.{h_a} }); const control = try rs.ingestControlTags(h_b, 3, &.{ .{ .type = .singleton, .name = "branch", .target = h_c, .value = "release" }, }); try t.expectEqualStrings("release", rs.branchOf(manifest, "trunk")); try t.expect(rs.isLeaf(parent)); try t.expectEqual(manifest, (try rs.openLeafOf("release")).?); try t.expect(rs.mtimeOf(control) == null); try t.expectEqual(@as(usize, 0), rs.parentCount(control)); try t.expect(!rs.isLeaf(control)); try t.expect(!rs.isOpenLeaf(control)); } test "repostate §8.3: parseMtime accepts direct and canonical D-card datetimes" { const direct = parseMtime("2026-07-10 08:30:15.007").?; try t.expectEqual(direct, parseMtime("2026-07-10T08:30:15.007").?); try t.expectEqual(@as(Mtime, 0), parseMtime("1970-01-01").?); try t.expect(parseMtime("1970-01-01T00:00") == null); try t.expect(parseMtime("1969-12-31T23:59:59.999") == null); } test "repostate §5.1: tagsOf reports singleton/propagate/cancel per T-card sign" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // §5.1: the leading sign selects the type — `+`→singleton, `*`→propagate, // `-`→cancel. A cancel row is inactive (tagtype 0) and is not surfaced. const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .singleton, .name = "alpha", .target = null, .value = "av" }, .{ .type = .cancel, .name = "beta", .target = null, .value = null }, .{ .type = .propagate, .name = "gamma", .target = null, .value = "gv" }, } }); const tags = try rs.tagsOf(t.allocator, ra); defer t.allocator.free(tags); try t.expectEqual(@as(usize, 2), tags.len); try t.expectEqualStrings("alpha", tags[0].name); try t.expectEqual(TagType.singleton, tags[0].tagtype); try t.expectEqualStrings("av", tags[0].value.?); try t.expectEqualStrings("gamma", tags[1].name); try t.expectEqual(TagType.propagate, tags[1].tagtype); try t.expectEqualStrings("gv", tags[1].value.?); } test "repostate §5.1/§2: a later-mtime cancel drops the tag from tagsOf and tagNames" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "sym-rel", .target = null, .value = "rel" }, } }); // §5.1: only an active (tagtype > 0) tag is in effect. A later-mtime // cancel (tagtype 0) supersedes the active row and takes it out of use. _ = try rs.ingestControlTags(h_b, 2, &.{ .{ .type = .cancel, .name = "sym-rel", .target = h_a, .value = null }, }); const tags = try rs.tagsOf(t.allocator, ra); defer t.allocator.free(tags); try t.expectEqual(@as(usize, 0), tags.len); const names = try rs.tagNames(t.allocator); defer t.allocator.free(names); try t.expectEqual(@as(usize, 0), names.len); } test "repostate §5.2: a later write supersedes; tagsOf shows one row with the newer value" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "sym-x", .target = null, .value = "old" }, } }); // §5.2 UNIQUE (rid, tag): a later write for the same (rid, tag) supersedes // the earlier one — one live entry, carrying the newer value. _ = try rs.ingestControlTags(h_b, 2, &.{ .{ .type = .propagate, .name = "sym-x", .target = h_a, .value = "new" }, }); const tags = try rs.tagsOf(t.allocator, ra); defer t.allocator.free(tags); try t.expectEqual(@as(usize, 1), tags.len); try t.expectEqualStrings("sym-x", tags[0].name); try t.expectEqualStrings("new", tags[0].value.?); } test "repostate §5.2: tagsOf surfaces each active row's stored value payload" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .singleton, .name = "note", .target = null, .value = "hello world" }, } }); const tags = try rs.tagsOf(t.allocator, ra); defer t.allocator.free(tags); try t.expectEqual(@as(usize, 1), tags.len); try t.expectEqualStrings("note", tags[0].name); try t.expectEqualStrings("hello world", tags[0].value.?); } test "repostate §4.2: a tag on an absent UUID is invisible until that artifact arrives" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); // plain root, no tags // §4.2 step 1: a tag targeting an absent artifact interns a phantom // placeholder. Its only holder is a phantom, so the name is not yet in use. _ = try rs.ingestControlTags(h_c, 2, &.{ .{ .type = .propagate, .name = "sym-rel", .target = h_b, .value = "rel" }, }); { const names = try rs.tagNames(t.allocator); defer t.allocator.free(names); try t.expectEqual(@as(usize, 0), names.len); } // The referenced artifact arrives and is crosslinked: the tag on it // becomes visible (§4.2 step 1). _ = try rs.crosslink(.{ .hash = h_b, .mtime = 3 }); { const names = try rs.tagNames(t.allocator); defer t.allocator.free(names); try t.expectEqual(@as(usize, 1), names.len); try t.expectEqualStrings("sym-rel", names[0]); } } test "repostate §4.2/§5.4: a * tag surfaces on the primary child, a + tag does not" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "sym-star", .target = null, .value = "sv" }, .{ .type = .singleton, .name = "sym-solo", .target = null, .value = "so" }, } }); const rb = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} }); // §5.4: the propagating (*) tag reaches the primary child; the singleton // (+) tag affects only its target and triggers no walk. const child = try rs.tagsOf(t.allocator, rb); defer t.allocator.free(child); try t.expectEqual(@as(usize, 1), child.len); try t.expectEqualStrings("sym-star", child[0].name); // The origin carries both its own singleton and propagating tags. const origin = try rs.tagsOf(t.allocator, ra); defer t.allocator.free(origin); try t.expectEqual(@as(usize, 2), origin.len); try t.expectEqualStrings("sym-solo", origin[0].name); try t.expectEqualStrings("sym-star", origin[1].name); } test "repostate §6.4: tagsOf surfaces the active tagid-8 branch tag with its value" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, } }); // §6.4: branch membership is the active tagid-8 tag. tagsOf surfaces it // with its stored value (the branch name). const tags = try rs.tagsOf(t.allocator, ra); defer t.allocator.free(tags); try t.expectEqual(@as(usize, 1), tags.len); try t.expectEqualStrings("branch", tags[0].name); try t.expectEqual(TagType.propagate, tags[0].tagtype); try t.expectEqualStrings("trunk", tags[0].value.?); } test "repostate §2: tagNames returns raw reserved-prefix names; only sym- is user-facing" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // §2 reserves the 'sym-'/'wiki-'/'tkt-'/'event-' prefixes and the fixed // 'branch'/'closed' names. tagNames returns them raw — presentation (the // 'sym-' stripping that makes a name user-facing) is the caller's job. _ = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = null }, .{ .type = .singleton, .name = "wiki-Home", .target = null, .value = null }, } }); const names = try rs.tagNames(t.allocator); defer t.allocator.free(names); try t.expectEqual(@as(usize, 3), names.len); try t.expectEqualStrings("branch", names[0]); try t.expectEqualStrings("sym-trunk", names[1]); try t.expectEqualStrings("wiki-Home", names[2]); } test "repostate §4.2: a crosslinked artifact carries no implicit cluster tag (content-type unmodeled)" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // §4.2 step 3 applies an implicit singleton cluster tag to cluster // artifacts. The crosslink layer does not yet carry artifact content-type // on a Record, so no cluster tag is ever created; this codifies that a // plain crosslinked artifact surfaces none. const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 }); const tags = try rs.tagsOf(t.allocator, ra); defer t.allocator.free(tags); try t.expectEqual(@as(usize, 0), tags.len); const names = try rs.tagNames(t.allocator); defer t.allocator.free(names); try t.expectEqual(@as(usize, 0), names.len); } test "repostate §5.2: tagsOf and tagNames survive allocation failure without leaking" { try t.checkAllAllocationFailures(t.allocator, tagsNoLeak, .{}); } fn tagsNoLeak(gpa: std.mem.Allocator) !void { var ms = MemStore.init(gpa); defer ms.deinit(); var rs = RepoState.init(gpa, ms.store()); defer rs.deinit(); const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1, .tags = &.{ .{ .type = .propagate, .name = "branch", .target = null, .value = "trunk" }, .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = null }, } }); const tags = try rs.tagsOf(gpa, ra); gpa.free(tags); const names = try rs.tagNames(gpa); gpa.free(names); } // --------------------------------------------------------------------------- // Verbatim wire storage (sync §4.1/§7.2): a MemStore entry may hold the stored // §4.1 container it retransmits, materialising its plaintext lazily. // --------------------------------------------------------------------------- const flate = std.compress.flate; /// A §4.1 container (4-byte big-endian uncompressed length + RFC1950 zlib) — /// the test's own encoder, mirroring the storage layer's, so a test never /// depends on a production encode entry point. fn makeContainer(gpa: std.mem.Allocator, bytes: []const u8) ![]u8 { var aw = std.Io.Writer.Allocating.init(gpa); defer aw.deinit(); try aw.ensureUnusedCapacity(64); try aw.writer.writeInt(u32, @intCast(bytes.len), .big); const window = try gpa.alloc(u8, flate.max_window_len); defer gpa.free(window); var c = try flate.Compress.init(&aw.writer, window, .zlib, .level_6); try c.writer.writeAll(bytes); try c.finish(); return try aw.toOwnedSlice(); } test "repostate sync §4.1/§7.2: putWire stores the container; wire() serves it, content() inflates lazily and memoises" { var ms = MemStore.init(t.allocator); defer ms.deinit(); const content = "the stored container's plaintext, materialised on first read\n"; const container = try makeContainer(t.allocator, content); defer t.allocator.free(container); const rid = try ms.putWire(h_a, container); // wire() returns the stored container verbatim (borrowed). const wf = ms.store().wire(rid).?; try t.expectEqualSlices(u8, container, wf.container); // content() inflates the container to the original bytes, and memoises: a // second read returns the very same slice (the borrowed-slice contract). const first = ms.store().content(rid).?; try t.expectEqualStrings(content, first); const second = ms.store().content(rid).?; try t.expectEqual(first.ptr, second.ptr); // memoised, not re-inflated } test "repostate sync §7.2: wire() is null for a raw put and for a phantom" { var ms = MemStore.init(t.allocator); defer ms.deinit(); const raw_rid = try ms.store().put(h_a, "raw bytes"); // no stored container try t.expect(ms.store().wire(raw_rid) == null); const ph_rid = try ms.store().intern(h_b); // phantom try t.expect(ms.store().wire(ph_rid) == null); try t.expect(ms.store().content(ph_rid) == null); } test "repostate sync §4.1: putWire is idempotent and keeps the first stored container" { var ms = MemStore.init(t.allocator); defer ms.deinit(); const content = "idempotent putWire keeps the first container"; const c1 = try makeContainer(t.allocator, content); defer t.allocator.free(c1); const c2 = try makeContainer(t.allocator, content); defer t.allocator.free(c2); const rid1 = try ms.putWire(h_a, c1); const rid2 = try ms.putWire(h_a, c2); // same hash: no-op try t.expectEqual(rid1, rid2); try t.expectEqualStrings(content, ms.store().content(rid1).?); } test "repostate sync §7.2: putWireDelta serves the delta form; content() reconstructs against the basis and memoises" { var ms = MemStore.init(t.allocator); defer ms.deinit(); const base = "the basis artifact whose long shared run lets the delta be a small copy-dominated payload distinct from the reconstructed size"; const target = "the basis artifact whose long shared run lets the delta be a small copy-dominated payload distinct from the reconstructed size!!"; // The basis is stored first (an earlier rid) as a full container. const base_container = try makeContainer(t.allocator, base); defer t.allocator.free(base_container); _ = try ms.putWire(h_a, base_container); // The delta form: a §4.1 container of the raw FossilDelta against h_a. const raw = try delta.create(t.allocator, base, target); defer t.allocator.free(raw); const delta_container = try makeContainer(t.allocator, raw); defer t.allocator.free(delta_container); const target_size = try delta.targetSize(raw); const rid = try ms.putWireDelta(h_b, delta_container, h_a, target_size); // wire() returns the delta arm: the stored container plus DELTASRC and the // reconstructed-target size (the §7.2 USIZE — NOT the container prefix). const wf = ms.store().wire(rid).?; try t.expect(wf.delta != null); try t.expectEqualSlices(u8, delta_container, wf.container); try t.expectEqualStrings(h_a, wf.delta.?.src_hash); try t.expectEqual(@as(u64, target.len), wf.delta.?.target_size); // content() reconstructs the plaintext against the (materialised) basis and // memoises: a second read returns the same slice. const first = ms.store().content(rid).?; try t.expectEqualStrings(target, first); const second = ms.store().content(rid).?; try t.expectEqual(first.ptr, second.ptr); // A delta whose basis is absent yields null content (not a crash), never a // wrong reconstruction. const raw2 = try delta.create(t.allocator, base, target); defer t.allocator.free(raw2); const dc2 = try makeContainer(t.allocator, raw2); defer t.allocator.free(dc2); const orphan = try ms.putWireDelta(h_c, dc2, h_d, target.len); // h_d never stored try t.expect(ms.store().content(orphan) == null); } fn putWireOom(gpa: std.mem.Allocator, container: []const u8) !void { var ms = MemStore.init(gpa); defer ms.deinit(); _ = try ms.putWire(h_a, container); } fn putWireDeltaOom(gpa: std.mem.Allocator, base_container: []const u8, delta_container: []const u8) !void { var ms = MemStore.init(gpa); defer ms.deinit(); _ = try ms.putWire(h_a, base_container); _ = try ms.putWireDelta(h_b, delta_container, h_a, 128); } test "repostate sync §7.2: putWireDelta propagates allocation failure" { const base_container = try makeContainer(t.allocator, "basis for the putWireDelta allocation-failure probe, with a shared run"); defer t.allocator.free(base_container); const raw = try delta.create(t.allocator, "basis for the putWireDelta allocation-failure probe, with a shared run", "basis for the putWireDelta allocation-failure probe, with a shared run!"); defer t.allocator.free(raw); const delta_container = try makeContainer(t.allocator, raw); defer t.allocator.free(delta_container); try t.checkAllAllocationFailures(t.allocator, putWireDeltaOom, .{ base_container, delta_container }); } test "repostate sync §7.2/§4.1: retain records the received full container on an already-put entry; wire() then serves it verbatim" { var ms = MemStore.init(t.allocator); defer ms.deinit(); const s = ms.store(); // The receive path stores reconstructed plaintext FIRST (put), so the entry // already carries bytes — the case putWire would decline. const content = "the received artifact plaintext, stored via put before the wire is retained\n"; const rid = try s.put(h_a, content); try t.expect(s.wire(rid) == null); // no wire yet // retain() records the received §4.1 container even though bytes is set. const container = try makeContainer(t.allocator, content); defer t.allocator.free(container); try s.retain(rid, .{ .full = container }); const wf = s.wire(rid).?; try t.expect(wf.delta == null); try t.expectEqualSlices(u8, container, wf.container); // §7.2 verbatim // content() still returns the original plaintext (the put'd bytes stand). try t.expectEqualStrings(content, s.content(rid).?); } test "repostate sync §7.2: retain records the received delta wire form; wire() serves the delta arm with DELTASRC + USIZE" { var ms = MemStore.init(t.allocator); defer ms.deinit(); const s = ms.store(); const target = "the reconstructed target the receiver stored, distinct in size from the raw delta payload\n"; const rid = try s.put(h_b, target); // plaintext stored by the receive path const base = "the reconstructed target the receiver stored, distinct in size from the raw delta payload!!"; const raw = try delta.create(t.allocator, base, target); defer t.allocator.free(raw); const delta_container = try makeContainer(t.allocator, raw); defer t.allocator.free(delta_container); const target_size = try delta.targetSize(raw); try s.retain(rid, .{ .delta = .{ .src = h_a, .container = delta_container, .target_size = target_size } }); const wf = s.wire(rid).?; try t.expect(wf.delta != null); try t.expectEqualSlices(u8, delta_container, wf.container); try t.expectEqualStrings(h_a, wf.delta.?.src_hash); try t.expectEqual(@as(u64, target.len), wf.delta.?.target_size); // content() is unchanged — the plaintext the receiver already reconstructed. try t.expectEqualStrings(target, s.content(rid).?); } test "repostate sync §4.1: retain is idempotent — the first retained form wins" { var ms = MemStore.init(t.allocator); defer ms.deinit(); const s = ms.store(); const content = "idempotent retain keeps the first container"; const rid = try s.put(h_a, content); const c1 = try makeContainer(t.allocator, content); defer t.allocator.free(c1); const c2 = try makeContainer(t.allocator, content); defer t.allocator.free(c2); try s.retain(rid, .{ .full = c1 }); try s.retain(rid, .{ .full = c2 }); // no-op: first form stands try t.expectEqualSlices(u8, c1, s.wire(rid).?.container); } fn retainFullOom(gpa: std.mem.Allocator, container: []const u8) !void { var ms = MemStore.init(gpa); defer ms.deinit(); const s = ms.store(); const rid = try s.put(h_a, "retain OOM probe plaintext"); try s.retain(rid, .{ .full = container }); } test "repostate sync §7.2: retain (full) propagates allocation failure" { const container = try makeContainer(t.allocator, "retain full allocation-failure probe container payload"); defer t.allocator.free(container); try t.checkAllAllocationFailures(t.allocator, retainFullOom, .{container}); } fn retainDeltaOom(gpa: std.mem.Allocator, delta_container: []const u8) !void { var ms = MemStore.init(gpa); defer ms.deinit(); const s = ms.store(); const rid = try s.put(h_b, "retain delta OOM probe plaintext"); try s.retain(rid, .{ .delta = .{ .src = h_a, .container = delta_container, .target_size = 128 } }); } test "repostate sync §7.2: retain (delta) propagates allocation failure" { const raw = try delta.create(t.allocator, "retain delta allocation-failure probe basis with a shared run here", "retain delta allocation-failure probe basis with a shared run here!"); defer t.allocator.free(raw); const delta_container = try makeContainer(t.allocator, raw); defer t.allocator.free(delta_container); try t.checkAllAllocationFailures(t.allocator, retainDeltaOom, .{delta_container}); } test "repostate sync §4.1: putWire propagates allocation failure" { const container = try makeContainer(t.allocator, "payload for the putWire allocation-failure probe"); defer t.allocator.free(container); try t.checkAllAllocationFailures(t.allocator, putWireOom, .{container}); } // --- bisectSpan (ambiguities.md bisect-midpoint) ------------------------------- // Extra distinct hashes for larger DAG fixtures; digit prefixes order below the // letter hashes ("0" < "1" < ... < "a"), so hash tiebreaks are controllable. const h_1 = "1" ** 64; const h_2 = "2" ** 64; const h_3 = "3" ** 64; test "repostate bisect-midpoint: even span probes the lower median by mtime" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // A six-node chain c0<-c1<-c2<-c3<-c4<-c5 with strictly increasing mtime. const c0 = try rs.crosslink(.{ .hash = h_g, .mtime = 10 }); const c1 = try rs.crosslink(.{ .hash = h_1, .mtime = 20, .parents = &.{h_g} }); const c2 = try rs.crosslink(.{ .hash = h_2, .mtime = 30, .parents = &.{h_1} }); _ = try rs.crosslink(.{ .hash = h_3, .mtime = 40, .parents = &.{h_2} }); _ = try rs.crosslink(.{ .hash = h_a, .mtime = 50, .parents = &.{h_3} }); const c5 = try rs.crosslink(.{ .hash = h_b, .mtime = 60, .parents = &.{h_a} }); // good=c0, bad=c5 → untested {c1,c2,c3,c4} (even); probe = index (4-1)/2 = 1 // by ascending mtime = c2 (the earlier of the two middle elements). const out = try rs.bisectSpan(t.allocator, &.{c5}, &.{c0}, &.{}); try t.expectEqual(@as(usize, 4), out.untested); try t.expectEqual(@as(?Rid, c2), out.probe); try t.expectEqual(c5, out.culprit); _ = c1; } test "repostate bisect-midpoint: mtime tie broken by least artifact hash" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // c1 and c2 share mtime 50; c1 carries the higher hash ("b"), c2 the lower // ("a"), so the lower-median of {c1,c2} is c2 by the hash tiebreak. const c0 = try rs.crosslink(.{ .hash = h_g, .mtime = 10 }); const c1 = try rs.crosslink(.{ .hash = h_b, .mtime = 50, .parents = &.{h_g} }); const c2 = try rs.crosslink(.{ .hash = h_a, .mtime = 50, .parents = &.{h_b} }); const c3 = try rs.crosslink(.{ .hash = h_c, .mtime = 90, .parents = &.{h_a} }); const out = try rs.bisectSpan(t.allocator, &.{c3}, &.{c0}, &.{}); try t.expectEqual(@as(usize, 2), out.untested); try t.expectEqual(@as(?Rid, c2), out.probe); _ = c1; } test "repostate bisect-midpoint: full-ancestry candidates span every merge path" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // Diamond: root r, left L (primary), right R (merge parent), merge M. _ = try rs.crosslink(.{ .hash = h_g, .mtime = 10 }); const left = try rs.crosslink(.{ .hash = h_a, .mtime = 20, .parents = &.{h_g} }); const right = try rs.crosslink(.{ .hash = h_b, .mtime = 30, .parents = &.{h_g} }); const m = try rs.crosslink(.{ .hash = h_c, .mtime = 40, .parents = &.{ h_a, h_b } }); // Marking R good leaves L reachable; marking L good leaves R reachable only // via the non-primary merge edge — proving full-ancestry membership, never a // primary-only mainline walk. const via_left = try rs.bisectSpan(t.allocator, &.{m}, &.{right}, &.{}); try t.expectEqual(@as(?Rid, left), via_left.probe); const via_right = try rs.bisectSpan(t.allocator, &.{m}, &.{left}, &.{}); try t.expectEqual(@as(?Rid, right), via_right.probe); } test "repostate bisect-midpoint: narrowing terminates at the mtime-earliest bad" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const c0 = try rs.crosslink(.{ .hash = h_g, .mtime = 10 }); const c1 = try rs.crosslink(.{ .hash = h_1, .mtime = 20, .parents = &.{h_g} }); const c2 = try rs.crosslink(.{ .hash = h_2, .mtime = 30, .parents = &.{h_1} }); const c3 = try rs.crosslink(.{ .hash = h_3, .mtime = 40, .parents = &.{h_2} }); const c4 = try rs.crosslink(.{ .hash = h_a, .mtime = 50, .parents = &.{h_3} }); // Step 1: good=c0, bad=c4 → untested {c1,c2,c3}, probe = median c2. const s1 = try rs.bisectSpan(t.allocator, &.{c4}, &.{c0}, &.{}); try t.expectEqual(@as(?Rid, c2), s1.probe); // Step 2: c2 marked bad → anchor becomes c2, untested {c1}, probe c1. const s2 = try rs.bisectSpan(t.allocator, &.{ c4, c2 }, &.{c0}, &.{}); try t.expectEqual(@as(?Rid, c1), s2.probe); try t.expectEqual(c2, s2.culprit); // Step 3: c1 marked good → no untested candidate; culprit = c2 (final). const s3 = try rs.bisectSpan(t.allocator, &.{ c4, c2 }, &.{ c0, c1 }, &.{}); try t.expectEqual(@as(?Rid, null), s3.probe); try t.expectEqual(@as(usize, 0), s3.untested); try t.expectEqual(c2, s3.culprit); _ = c3; } test "repostate bisect-midpoint: skipped candidates drop from the untested set" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const c0 = try rs.crosslink(.{ .hash = h_g, .mtime = 10 }); const c1 = try rs.crosslink(.{ .hash = h_1, .mtime = 20, .parents = &.{h_g} }); const c2 = try rs.crosslink(.{ .hash = h_2, .mtime = 30, .parents = &.{h_1} }); const c3 = try rs.crosslink(.{ .hash = h_3, .mtime = 40, .parents = &.{h_2} }); // good=c0, bad=c3, skip c2 → untested {c1} only. const out = try rs.bisectSpan(t.allocator, &.{c3}, &.{c0}, &.{c2}); try t.expectEqual(@as(usize, 1), out.untested); try t.expectEqual(@as(?Rid, c1), out.probe); } test "repostate bisect-midpoint: an inverted mark set is refused" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const c0 = try rs.crosslink(.{ .hash = h_g, .mtime = 10 }); _ = try rs.crosslink(.{ .hash = h_1, .mtime = 20, .parents = &.{h_g} }); const c2 = try rs.crosslink(.{ .hash = h_2, .mtime = 30, .parents = &.{h_1} }); // bad=c0 is an ancestor of good=c2 → inverted. try t.expectError(error.InvertedMarks, rs.bisectSpan(t.allocator, &.{c0}, &.{c2}, &.{})); } test "repostate bisect-midpoint: unrelated histories are refused" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); // Two independent roots share no ancestor. const x = try rs.crosslink(.{ .hash = h_a, .mtime = 10 }); const y = try rs.crosslink(.{ .hash = h_b, .mtime = 20 }); try t.expectError(error.UnrelatedHistories, rs.bisectSpan(t.allocator, &.{x}, &.{y}, &.{})); } // --- REGRESSION B1 -------------------------------------------------------- // // Same-mtime selection must break ties by DAG ancestry (a descendant beats its // ancestor), not by artifact-hash order (docs/plans/CLI-VERB-BREAKDOWN.md // §B1/B2). These tests are fully deterministic — no wall clock — and build the // SAME three-node chain in BOTH hash orderings. That is what distinguishes an // ancestry fix from hash luck: a hash-ordered implementation passes exactly one // of the two orderings and fails the other. /// Builds a linear three-node chain root → mid → head, every node carrying the /// SAME event.mtime, with a propagating `sym-trunk` tag on the root. `ascending` /// picks which of the two hash triples is used, so the chain can be built with /// the head holding the lexicographically greatest or least artifact hash. fn buildEqualMtimeChain(rs: *RepoState, comptime ascending: bool) !struct { root: Rid, mid: Rid, head: Rid } { const hashes = if (ascending) [3][]const u8{ h_a, h_b, h_c } // head has the GREATEST hash else [3][]const u8{ h_c, h_b, h_a }; // head has the LEAST hash const root = try rs.crosslink(.{ .hash = hashes[0], .mtime = 7, .tags = &.{ .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" }, } }); const mid = try rs.crosslink(.{ .hash = hashes[1], .mtime = 7, .parents = &.{hashes[0]} }); const head = try rs.crosslink(.{ .hash = hashes[2], .mtime = 7, .parents = &.{hashes[1]} }); return .{ .root = root, .mid = mid, .head = head }; } test "repostate REGRESSION B1: tip resolves to the descendant on an equal-mtime chain (head has the greatest hash)" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const c = try buildEqualMtimeChain(&rs, true); // The chain is linear, so the head is the ONLY leaf: `tip` and the leaf set // cannot legitimately disagree. try t.expect(rs.isLeaf(c.head)); try t.expect(!rs.isLeaf(c.root)); try t.expect(!rs.isLeaf(c.mid)); try t.expectEqual(c.head, try rs.resolve("tip", .checkin)); } test "repostate REGRESSION B1: tip resolves to the descendant on an equal-mtime chain (head has the least hash)" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const c = try buildEqualMtimeChain(&rs, false); try t.expect(rs.isLeaf(c.head)); try t.expectEqual(c.head, try rs.resolve("tip", .checkin)); } test "repostate REGRESSION B1: resolveName picks the descendant on an equal-mtime chain (head has the greatest hash)" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const c = try buildEqualMtimeChain(&rs, true); // The sym-trunk tag propagates down the chain, so all three nodes are // candidates with identical event.mtime. try t.expectEqual(c.head, try rs.resolveName("trunk")); } test "repostate REGRESSION B1: resolveName picks the descendant on an equal-mtime chain (head has the least hash)" { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const c = try buildEqualMtimeChain(&rs, false); try t.expectEqual(c.head, try rs.resolveName("trunk")); } test "repostate REGRESSION B1: tip and resolveName agree with the leaf set in both hash orderings" { // Cross-verb agreement (§5.3's invariant) at the library level: whatever // `tip` and `trunk` resolve to must be a member of the leaf set. This is the // SF5 tripwire expressed as a unit test. inline for (.{ true, false }) |ascending| { var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const c = try buildEqualMtimeChain(&rs, ascending); const tip = try rs.resolve("tip", .checkin); const trunk = try rs.resolveName("trunk"); try t.expect(rs.isLeaf(tip)); try t.expect(rs.isLeaf(trunk)); try t.expectEqual(tip, trunk); try t.expectEqual(c.head, tip); } } test "repostate REGRESSION B1: mergeBaseOf picks the deepest common ancestor when every candidate shares an mtime" { // `mergeBaseOf` selects the common node with the greatest event.mtime, // ties by least artifact hash — the same defect class. On an equal-mtime // chain the whole ancestor set ties, so the base must be chosen by // ancestry: the DEEPEST common node (mid), never root. var ms = MemStore.init(t.allocator); defer ms.deinit(); var rs = RepoState.init(t.allocator, ms.store()); defer rs.deinit(); const root = try rs.crosslink(.{ .hash = h_a, .mtime = 7 }); const mid = try rs.crosslink(.{ .hash = h_b, .mtime = 7, .parents = &.{h_a} }); const left = try rs.crosslink(.{ .hash = h_c, .mtime = 7, .parents = &.{h_b} }); const right = try rs.crosslink(.{ .hash = h_d, .mtime = 7, .parents = &.{h_b} }); _ = root; try t.expectEqual(mid, (try rs.mergeBaseOf(t.allocator, left, right)).?); }