Artifact 0f5c4ae8b3782e6910beb30b4d1960fdd16ec3e7c37b3727633d47ab52b0b8db:
- File
src/repostate_test.zig
— part of check-in
[fc9bcb81bd]
at
2026-07-31 02:15:00
on branch sub-manifest
— repostate: resolve a stored sym- selector directly
Names beginning with `sym-` are ambiguous in this repository. A user tag written `sym-x` is stored under the canonical spelling `sym-sym-x`, so resolving `sym-x` correctly looks for `sym-sym-x`. An imported repository, though, can carry a tag whose stored canonical spelling already begins with `sym-`, and nothing could address it: the ordinary lookup rewrote the name and missed the artifact that was actually there.
Resolution now attempts the ordinary user-tag path first and consults the stored spelling only when that fails. The precedence is deliberate. A user tag continues to win over an imported canonical name, so no existing repository changes meaning, and the fallback is reachable only where the ordinary answer does not exist at all. (user: dmestas, size: 79033) [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(); 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(); 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(); 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(); 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(); 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(); _ = 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(); 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)).?); }