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}, &.{}));
}
test "repostate timeline tied runs: generation order lists every descendant before its ancestor; concurrents by depth then hash" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
var rs = RepoState.init(t.allocator, ms.store());
defer rs.deinit();
// One same-mtime batch shaped like a converted history: a chain
// root -> mid -> head, a side branch off root, and a merge of head+side —
// every node at ONE mtime, hashes chosen so plain hash order would list
// the merge LAST (the B1 failure shape).
const root = try rs.crosslink(.{ .hash = h_a, .mtime = 7, .tags = &.{
.{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" },
} });
const mid = try rs.crosslink(.{ .hash = h_c, .mtime = 7, .parents = &.{h_a} });
const head = try rs.crosslink(.{ .hash = h_b, .mtime = 7, .parents = &.{h_c} });
const side = try rs.crosslink(.{ .hash = h_d, .mtime = 7, .parents = &.{h_a} });
const merge = try rs.crosslink(.{ .hash = h_e, .mtime = 7, .parents = &.{ h_b, h_d } });
const rids = try rs.checkinsByMtime(t.allocator);
defer t.allocator.free(rids);
try t.expectEqual(@as(usize, 5), rids.len);
// Generations: root 1, mid 2, side 2, head 3, merge 4. Order is
// generation DESC (descendants first — B1 exactly), equal generations
// (mid/side: true concurrents) by least hash: merge, head, mid, side, root.
try t.expectEqualSlices(Rid, &.{ merge, head, mid, side, root }, rids);
}
test "repostate #16: a cancel walk halts at a rowless node (Fossil tag.c coalesce), a propagating walk does not" {
var ms = MemStore.init(t.allocator);
defer ms.deinit();
var rs = RepoState.init(t.allocator, ms.store());
defer rs.deinit();
// A four-node primary chain a -> b -> c -> d.
const ra = try rs.crosslink(.{ .hash = h_a, .mtime = 1 });
_ = try rs.crosslink(.{ .hash = h_b, .mtime = 2, .parents = &.{h_a} });
const rc = try rs.crosslink(.{ .hash = h_c, .mtime = 3, .parents = &.{h_b} });
const rd = try rs.crosslink(.{ .hash = h_d, .mtime = 4, .parents = &.{h_c} });
_ = ra;
const h_e2 = "f" ** 64;
const h_e3 = "0" ** 64;
// A propagating tag applied at a rides the whole chain: three
// propagation steps (b, c, d), every one qualifying on a rowless node —
// Fossil's tagType==2 coalesce default, unchanged by #16.
RepoState.propagate_visits = 0;
_ = try rs.ingestControlTags(h_e, 10, &.{
.{ .type = .propagate, .name = "mark", .target = h_a, .value = "v" },
});
try t.expectEqual(@as(u64, 3), RepoState.propagate_visits);
try t.expect(rs.activeTagValue(rd, "mark") != null);
// Cancelling at a strips the inherited rows: the walk visits b (removes),
// c (removes), d (removes) — three steps over ROWED nodes.
RepoState.propagate_visits = 0;
_ = try rs.ingestControlTags(h_e2, 20, &.{
.{ .type = .cancel, .name = "mark", .target = h_a, .value = null },
});
try t.expectEqual(@as(u64, 3), RepoState.propagate_visits);
try t.expect(rs.activeTagValue(rc, "mark") == null);
try t.expect(rs.activeTagValue(rd, "mark") == null);
// A SECOND cancel meets a rowless b and must halt there — one visit, not
// a walk through the whole gap (Fossil's LEFT-JOIN NULL falls back to
// tagType==2, false for a cancel, and pqueuex_insert never fires).
RepoState.propagate_visits = 0;
_ = try rs.ingestControlTags(h_e3, 30, &.{
.{ .type = .cancel, .name = "mark", .target = h_a, .value = null },
});
try t.expectEqual(@as(u64, 1), RepoState.propagate_visits);
}
// --- 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)).?);
}