Artifact 17bdf4c7042f846f928570a47821035f936238f70f3d61d8d2ea3729d5f58868:
- File
src/repository_test.zig
— part of check-in
[bdb49cc5ca]
at
2026-07-19 11:56:16
on branch blockers
— repostate: break same-mtime resolution ties by DAG ancestry, not hash order
Three commits made inside the same second made `zit` return the WRONG file content for `tip` and `trunk`, and made the binary contradict itself: `zit info tip` reported "leaf: no" while `zit leaves` named the same check-in a leaf. `zit cat f.txt tip` returned the ROOT's content and `zit cat f.txt trunk` returned the MIDDLE's, on a three-commit chain whose newest content was `v3`. A version-control system that silently hands back an old revision under a name that means "newest" is disqualifying, so this lands before anything else.
WHAT WAS WRONG — three independent tie sites, not one
The originally-recorded diagnosis blamed a single lexicographic artifact-hash tiebreak in `mergeBaseOf`. That was wrong for the reported symptom: the RED-proof gate built the equal-mtime chain in BOTH hash orderings and observed IDENTICAL failures, which rules out hash order as the mechanism. Fixing only that line would have left `cat tip` and `cat trunk` still returning wrong answers. The real picture:
- `tipForm` — `mtime > best_mtime`, a strict `>` with no tiebreak at all, so the first entry in `events` INSERTION order won. Symptom: `tip` -> ROOT. - `resolveName` — ties settled by `tagxref` hash-map ITERATION order. Symptom: `trunk` -> MID. - `mergeBaseOf` — lexicographic artifact hash. A genuine defect, proven RED at unit level, but not the cause of the tip/trunk symptom and unreachable from the e2e merge path.
WHAT CHANGED
Equal-`event.mtime` ties are now settled by DAG ancestry: a descendant beats its ancestor. Genuinely concurrent nodes — same mtime, no ancestry relation — keep a deterministic least-artifact-hash fallback, documented at each site; hash order between UNRELATED leaves was never the bug. The shared helper (`MtimeMaxPick`) is used by `tipForm`, `resolveName`, `openLeafOf` and `dateResolve`, so every symbolic-name surface settles ties one way.
Timeline ordering gets a second pass that re-orders each equal-mtime RUN descendants-first, deliberately overriding the hash tiebreak. It is a per-run repeated-maximal-element extraction, NOT a sort predicate: "descends-from, else hash" is not a strict weak order, and handing it to `std.mem.sort` would be undefined behavior. Runs are bounded by the check-ins sharing one millisecond.
Settling a tie by ancestry allocates, so `resolveName`/`resolve`/`openLeafOf` gained `OutOfMemory`. Every caller switches it as an ABORT, never as a mismatch or "already correct" verdict — reporting an allocation failure as the exit-0 up-to-date message is exactly the lie those switches exist to prevent. `repository_test`'s closed-error-set assertion moved 2 -> 3 for that reason: it encoded the pre-fix set, not an inconvenient expectation.
Two ride-alongs on the same resolution surface:
- SF1 — `zit update trunk` used to fail as "invalid check-in ID": update accepted ONLY canonical 40/64-hex. Its argument is a check-in name like every other verb's, so it now runs the shared 8.3 procedure. A canonical ID still short-circuits, leaving update's cold path byte-for-byte as it was. - SF5 — a `tip` that names a non-leaf is now an in-binary assertion, not just a test. It is legitimate ONLY under a non-monotone clock (a child stamped strictly earlier than its parent); a child that TIES its parent means the ancestry tiebreak failed to fire, which is the defect itself. This tripwire would have caught the original bug.
RED PROOF (recorded by the RED-proof gate, 2026-07-19; per BLOCKER-FIX-PLAN.md section 2, no product code changed until RED was proven)
Unit, `src/repostate_test.zig`, deterministic equal-mtime chains built directly in BOTH hash orderings — no wall-clock dependence:
repostate REGRESSION B1: tip resolves to the descendant on an equal-mtime chain (head has the greatest hash) repostate REGRESSION B1: tip resolves to the descendant on an equal-mtime chain (head has the least hash) repostate REGRESSION B1: resolveName picks the descendant on an equal-mtime chain (head has the greatest hash) repostate REGRESSION B1: resolveName picks the descendant on an equal-mtime chain (head has the least hash) repostate REGRESSION B1: tip and resolveName agree with the leaf set in both hash orderings repostate REGRESSION B1: mergeBaseOf picks the deepest common ancestor when every candidate shares an mtime [failed pre-fix: expected 2, found 1]
End-to-end, `src/e2e.zig`, same-second commits with NO sleep between them — same-second IS the point:
cli REGRESSION B1: same-second commits resolve tip and trunk to the newest check-in cli REGRESSION B1 (SF1): update accepts a symbolic name and lands on the newest check-in cli REGRESSION B1: open materializes the newest same-second check-in into a fresh clone cli REGRESSION B1: a same-second two-branch merge picks the correct base and is never up to date
All of the above failed against pre-fix trunk; the two hash-ordering pairs failed IDENTICALLY, which is the evidence that retired the hash-order theory.
TEST EVIDENCE NOW
In this worktree at the committed tree: `zig fmt --check .`, `zig build`, and `zig build test` each exit 0 — full battery, no filtering. The only pre-existing expectation changed is the `repository_test` error-set count noted above, and it changed because it encoded the old set.
Markers `// REGRESSION B1:` are stable so state probes can grep trunk for them.
No borrowed technique: the ancestry tiebreak and the per-run topological micro-ordering are original to this change. (user: dmestas, size: 51097) [annotate] [blame] [check-ins using] [more...]
//! Public-surface tests for the `Repository` facade. //! //! Every fixture builds a real on-disk `.zit` (a writable `LogStore` seeded with //! artifacts assembled by `artifact.create`) plus, where a checkout matters, a //! `.zit-checkout` record — then opens it through `Repository.open` exactly as //! the CLI does. Expected artifact names are the artifact §7.2 hash-over- //! uncompressed of the fixture's own bytes; expected derivations come from the //! repo-state RFC (§5-9), never from Fossil. const std = @import("std"); const artifact = @import("artifact.zig"); const repostate = @import("repostate.zig"); const logstore = @import("logstore.zig"); const storage = @import("storage.zig"); const checkout = @import("checkout.zig"); const Repository = @import("repository.zig").Repository; const testing = std.testing; const hex_digits = "0123456789abcdef"; /// A tmp-backed repository builder: seeds a writable `.zit` log, then closes it /// so `Repository.open` reads a settled on-disk repository. The tmp dir persists /// until `cleanup`, so `Repository` can reopen `.zit` for lazy content. const Repo = struct { tmp: std.testing.TmpDir, fs: storage.FileStorage, log: logstore.LogStore, file: std.Io.File, io: std.Io, fn init(self: *Repo, io: std.Io) !void { self.io = io; self.tmp = testing.tmpDir(.{}); self.file = try self.tmp.dir.createFile(io, ".zit", .{ .read = true }); self.fs = storage.FileStorage.init(io, self.file, 0); self.log = try logstore.LogStore.open(testing.allocator, self.fs.storageFile(), 0); } /// Commit the seeded frames and release the writable handle; the `.zit` file /// stays on disk under `tmp` for `Repository.open` to read. fn seal(self: *Repo) !void { try self.log.commit(); self.log.deinit(testing.allocator); self.file.close(self.io); } fn cleanup(self: *Repo) void { self.tmp.cleanup(); } fn config(self: *Repo, default_branch: []const u8) !void { try self.log.putConfig(.{ .hash_policy = 0, .default_branch = default_branch, .project_id = "p" }); } /// Serialize `m`, store it under its own sha3-256 digest, return its 64-hex /// artifact name (caller frees). fn put(self: *Repo, m: *const artifact.Manifest) ![]u8 { const bytes = try artifact.create(m, testing.allocator); defer testing.allocator.free(bytes); return self.putRaw(bytes); } /// Store arbitrary `bytes` under their sha3-256 digest, return the 64-hex /// name (caller frees). Used for both artifacts and plain file blobs. fn putRaw(self: *Repo, bytes: []const u8) ![]u8 { var digest: [32]u8 = undefined; std.crypto.hash.sha3.Sha3_256.hash(bytes, &digest, .{}); _ = try self.log.put(&digest, bytes); return hexName(&digest); } }; /// Whether a `.zit-cache` sidecar exists beside the repo's `.zit` — the on-disk /// signal that the derivation cache was materialized (its name is a repocache /// internal, so it is spelled literally here rather than imported). fn cacheExists(repo: *Repo, io: std.Io) bool { var f = repo.tmp.dir.openFile(io, ".zit-cache", .{}) catch return false; f.close(io); return true; } fn hexName(raw: []const u8) ![]u8 { const out = try testing.allocator.alloc(u8, raw.len * 2); for (raw, 0..) |b, i| { out[i * 2] = hex_digits[b >> 4]; out[i * 2 + 1] = hex_digits[b & 0x0f]; } return out; } /// A skeleton manifest: a root carries the propagating `branch` tag (plus any /// `extra_tags`); a child names one `parent`. Files are irrelevant to the /// derivations under test, so there are none. const Spec = struct { ts: []const u8, parent: ?[]const u8 = null, branch_value: []const u8 = "trunk", extra_tags: []const artifact.TCard = &.{}, }; fn manifestOf(spec: Spec, tag_buf: []artifact.TCard, parent_buf: []artifact.PCard) artifact.Manifest { var n: usize = 0; if (spec.parent == null) { tag_buf[n] = .{ .sign = .propagate, .name = "branch", .target = null, .value = spec.branch_value }; n += 1; } for (spec.extra_tags) |t| { tag_buf[n] = t; n += 1; } var p_slice: []const artifact.PCard = &.{}; if (spec.parent) |parent| { parent_buf[0] = .{ .parents = parent, .count = 1 }; p_slice = parent_buf[0..1]; } return baseManifest(.manifest, spec.ts, p_slice, tag_buf[0..n]); } fn baseManifest(kind: artifact.ArtifactType, ts: []const u8, p: []const artifact.PCard, t: []const artifact.TCard) artifact.Manifest { return .{ .arena_state = std.heap.ArenaAllocator.init(testing.allocator), .kind = kind, .a = null, .b = null, .c = if (kind == .manifest) .{ .comment = "c" } else null, .d = .{ .timestamp = ts }, .e = null, .g = null, .h = null, .i = null, .k = null, .l = null, .n = null, .r = null, .u = .{ .login = "u" }, .z = null, .w = null, .f = &.{}, .j = &.{}, .m = &.{}, .p = p, .q = &.{}, .t = t, }; } fn putCheckin(repo: *Repo, spec: Spec) ![]u8 { var tag_buf: [8]artifact.TCard = undefined; var parent_buf: [1]artifact.PCard = undefined; var m = manifestOf(spec, &tag_buf, &parent_buf); defer m.deinit(); return repo.put(&m); } fn putControl(repo: *Repo, ts: []const u8, tags: []const artifact.TCard) ![]u8 { var m = baseManifest(.control, ts, &.{}, tags); defer m.deinit(); return repo.put(&m); } /// One F card (§4.7.6): a present `hash` is a file at that blob id, a null hash /// is a delete marker. Perm/old-name are irrelevant to the file-set walk. fn fcard(name: []const u8, hash: ?[]const u8) artifact.FCard { return .{ .filename = name, .hash = hash, .perm = null, .old_name = null, .mode = .regular }; } /// A file-bearing check-in: a baseline manifest carrying `files` (root unless a /// `parent` is given), or a delta manifest when `baseline` names a B-card hash. /// `files` MUST already be sorted by decoded filename (§8.1); the writer emits /// them verbatim and the parser re-checks the order. const FSpec = struct { ts: []const u8, parent: ?[]const u8 = null, baseline: ?[]const u8 = null, files: []const artifact.FCard = &.{}, }; fn putFiles(repo: *Repo, spec: FSpec) ![]u8 { var tag_buf: [1]artifact.TCard = undefined; var n: usize = 0; if (spec.parent == null) { tag_buf[n] = .{ .sign = .propagate, .name = "branch", .target = null, .value = "trunk" }; n += 1; } var parent_buf: [1]artifact.PCard = undefined; var p_slice: []const artifact.PCard = &.{}; if (spec.parent) |parent| { parent_buf[0] = .{ .parents = parent, .count = 1 }; p_slice = parent_buf[0..1]; } var m = baseManifest(.manifest, spec.ts, p_slice, tag_buf[0..n]); defer m.deinit(); m.f = spec.files; if (spec.baseline) |bh| m.b = .{ .hash = bh }; return repo.put(&m); } /// Drain a `FileList` into `out` (caller-owned) so a test can assert the whole /// effective set at once — names, order, and blob ids. fn collectFiles(list: *const Repository.FileList, out: *std.ArrayList(artifact.FCard)) !void { var it = list.iterator(); while (it.next()) |f| try out.append(testing.allocator, f); } /// Open `repo` through the facade exactly as the CLI does. fn openRepo(out: *Repository, repo: *Repo, io: std.Io) !void { try Repository.open(out, testing.allocator, io, repo.tmp.dir, .{}); } test "repository §8.3: resolve a symbolic name to its check-in rid" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A root, then a child tagged with the symbolic name `sym-feature` (§8.3 // resolves a plain name through its active sym- tag, §5.1). const root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(root); const sym = [_]artifact.TCard{.{ .sign = .propagate, .name = "sym-feature", .target = null, .value = null }}; const child = try putCheckin(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = root, .extra_tags = &sym }); defer testing.allocator.free(child); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); // §8.3: "feature" resolves to the check-in carrying its active sym- tag. const rid = try facade.resolve("feature", .checkin); try testing.expectEqualStrings(child, facade.hashOf(rid)); // The full artifact id resolves to the same rid (§8.3 hash-prefix form). try testing.expectEqual(rid, try facade.resolve(child, .checkin)); } test "repository §8.2: resolve is recency-based — names a closed non-leaf that leaves omits" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A root that both bears a sym- name and is later closed AND superseded by a // child: it is neither a leaf nor open, yet §8.2 resolution (max-mtime among // sym-tagged) still names it — resolution never filters on leaf/closed. The // sym tag is a singleton (§5.1 `+`), so it names this check-in alone — a // propagating tag would flow to the child and resolve there instead. const named = [_]artifact.TCard{.{ .sign = .add, .name = "sym-rel", .target = null, .value = null }}; const root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00", .extra_tags = &named }); defer testing.allocator.free(root); const child = try putCheckin(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = root }); defer testing.allocator.free(child); const close = [_]artifact.TCard{.{ .sign = .add, .name = "closed", .target = root, .value = null }}; const ctl = try putControl(&repo, "2020-01-03T00:00:00", &close); defer testing.allocator.free(ctl); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); const rid = try facade.resolve("rel", .checkin); try testing.expectEqualStrings(root, facade.hashOf(rid)); // §8.2: the resolved check-in is a non-leaf (has a same-branch child) — // exactly what leaf enumeration omits, proving resolve ≠ leaf listing. try testing.expect(!facade.state().isLeaf(rid)); } test "repository §8.3: an unresolvable name is NotFound; the closed ResolveError forwards RepoState.resolve" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); const root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(root); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); // §8.3: a name matching no id, tag, or keyword is NotFound. try testing.expectError(error.NotFound, facade.resolve("zzzz-nope", .checkin)); // A hex-shaped prefix (≥4 digits) that names no stored id is also NotFound. try testing.expectError(error.NotFound, facade.resolve("deadbeef", .checkin)); // The other arm, error.Ambiguous, is raised only by the §8.3 hash-prefix // form when a ≥4-hex prefix matches two ids — impossible to force // hermetically without colliding sha3-256 digests on 16 bits. `resolve` // forwards `RepoState.resolve`'s closed `{ NotFound, Ambiguous, OutOfMemory }` // verbatim (no added logic), so its Ambiguous arm is covered by repostate's // own §8.3 resolver tests; the CLI switches the same closed set // exhaustively. `OutOfMemory` joined the set when equal-`event.mtime` ties // started being settled by DAG ancestry (`MtimeMaxPick`), which allocates. comptime { const E = @typeInfo(@typeInfo(@TypeOf(Repository.resolve)).@"fn".return_type.?).error_union.error_set; const names = @typeInfo(E).error_set.?; std.debug.assert(names.len == 3); // exactly { NotFound, Ambiguous, OutOfMemory } } } test "repository §7.1/§7.4: leaves lists open leaves; a closed leaf surfaces only under the all predicate" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // Two independent roots on trunk; close one. Both are leaf-table members // (§7.1), but only the open one passes the §7.4 open predicate. const open_root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(open_root); const closed_root = try putCheckin(&repo, .{ .ts = "2020-01-02T00:00:00" }); defer testing.allocator.free(closed_root); const close = [_]artifact.TCard{.{ .sign = .add, .name = "closed", .target = closed_root, .value = null }}; const ctl = try putControl(&repo, "2020-01-03T00:00:00", &close); defer testing.allocator.free(ctl); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); const open_rid = facade.lookup(open_root).?; const closed_rid = facade.lookup(closed_root).?; // §7.1: both are leaf-table members. try testing.expect(facade.state().isLeaf(open_rid)); try testing.expect(facade.state().isLeaf(closed_rid)); // §7.4: only the open leaf passes the open predicate. try testing.expect(facade.state().isOpenLeaf(open_rid)); try testing.expect(!facade.state().isOpenLeaf(closed_rid)); // Enumerating "open only" yields exactly the open leaf; "all" yields both. var open_only: usize = 0; var all: usize = 0; var it = facade.state().leaves(); while (it.next()) |rid| { all += 1; if (facade.state().isOpenLeaf(rid)) open_only += 1; } try testing.expectEqual(@as(usize, 2), all); try testing.expectEqual(@as(usize, 1), open_only); } test "repository §7.5: currentCheckoutRid marks the checkout leaf; null when unborn/absent" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); const root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(root); try repo.seal(); // (a) No `.zit-checkout`: nothing to mark. { var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); try testing.expect(facade.currentCheckoutRid() == null); } // (b) A checkout naming the root: currentCheckoutRid resolves to its rid. try checkout.write(io, repo.tmp.dir, .{ .version = root, .branch = "trunk", .merge_parent = "", .special_op = "" }); { var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); const cur = facade.currentCheckoutRid() orelse return error.NothingMarked; try testing.expectEqualStrings(root, facade.hashOf(cur)); } // (c) A checkout naming a version this repo never interned: null (§7.5 — a // version the view never saw marks nothing). try checkout.write(io, repo.tmp.dir, .{ .version = "f" ** 64, .branch = "trunk", .merge_parent = "", .special_op = "" }); { var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); try testing.expect(facade.currentCheckoutRid() == null); } } test "repository §6.1: branchOf uses the config main-branch as fallback, else trunk" { const io = testing.io; // (a) No config frame, a tagless resolution falls back to "trunk". { var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); const root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(root); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); try testing.expectEqualStrings("trunk", facade.mainBranch()); // §6.1 unset ⇒ trunk try testing.expectEqualStrings("trunk", facade.branchOf(facade.lookup(root).?)); } // (b) A config frame naming "mainline": branchOf threads the configured // fallback (§6.1 MUST read the configured value). { var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); try repo.config("mainline"); const root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00", .branch_value = "mainline" }); defer testing.allocator.free(root); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); try testing.expectEqualStrings("mainline", facade.mainBranch()); try testing.expectEqualStrings("mainline", facade.branchOf(facade.lookup(root).?)); } } test "repository §5.1/§5.2: tagsOf returns active tags on a resolved check-in, valued and valueless" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A root carrying the valued `branch` tag (§5.2 value = "trunk", added by // manifestOf) plus a valueless singleton sym- tag. const extra = [_]artifact.TCard{.{ .sign = .add, .name = "sym-v1", .target = null, .value = null }}; const root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00", .extra_tags = &extra }); defer testing.allocator.free(root); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); const rid = facade.lookup(root).?; const active = try facade.state().tagsOf(testing.allocator, rid); defer testing.allocator.free(active); // §5.1/§5.2: the `branch` tag carries its value; the valueless sym tag // carries none. Both are surfaced as active. var saw_valued = false; var saw_valueless = false; for (active) |t| { if (std.mem.eql(u8, t.name, "branch")) { saw_valued = true; try testing.expectEqualStrings("trunk", t.value.?); } if (std.mem.eql(u8, t.name, "sym-v1")) { saw_valueless = true; try testing.expect(t.value == null); } } try testing.expect(saw_valued and saw_valueless); } test "repository: an unborn repository has no leaves — empty result, no error" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); try repo.seal(); // an empty log: header only, no check-ins var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); var count: usize = 0; var it = facade.state().leaves(); while (it.next()) |_| count += 1; try testing.expectEqual(@as(usize, 0), count); } test "repository open: not-a-repo → NotARepo; a .zit that cannot be read → CannotRead; a corrupt log → Corrupt" { const io = testing.io; // NotARepo: a directory with no `.zit`. { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); var facade: Repository = undefined; try testing.expectError(error.NotARepo, Repository.open(&facade, testing.allocator, io, tmp.dir, .{})); } // CannotRead: `.zit` exists but is a directory, so the open cannot read it. { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDir(io, ".zit", .default_dir); var facade: Repository = undefined; try testing.expectError(error.CannotRead, Repository.open(&facade, testing.allocator, io, tmp.dir, .{})); } // Corrupt: a `.zit` whose bytes are not a valid log (short garbage header). { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); var f = try tmp.dir.createFile(io, ".zit", .{}); try f.writePositionalAll(io, "not a log", 0); f.close(io); var facade: Repository = undefined; try testing.expectError(error.Corrupt, Repository.open(&facade, testing.allocator, io, tmp.dir, .{})); } } test "repository open: no leak or crash under injected OOM at every derive allocation" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); const root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(root); const child = try putCheckin(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = root }); defer testing.allocator.free(child); try repo.seal(); // A manual `FailingAllocator` sweep rather than `checkAllAllocationFailures`: // `open` deliberately performs a best-effort `.zit-cache` write that SWALLOWS // OOM (the cache is advisory — a failed write is not an open failure), which // the "never swallow" checker cannot model. This sweep still proves the real // obligation at every allocation point: `open` returns ok or OutOfMemory, // never crashes, and never leaks (the wrapped testing.allocator asserts no // leak at test end). `--no-cache` forces the full openReadOnly + derive path. var i: usize = 0; while (true) : (i += 1) { var fa = std.testing.FailingAllocator.init(testing.allocator, .{ .fail_index = i }); const a = fa.allocator(); var facade: Repository = undefined; if (Repository.open(&facade, a, io, repo.tmp.dir, .{ .bypass_cache = true })) |_| { facade.deinit(a); // The injected fail point lay at or past the last allocation, so no // failure was forced this round — every allocation site is covered. if (!fa.has_induced_failure) break; } else |err| { try testing.expectEqual(error.OutOfMemory, err); } } } test "repository cache parity: a .zit-cache fast-path view equals a from-scratch derive" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A root, a same-branch child, a fork, and a control close — shape enough to // exercise leaves, propagation, and tags. const root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(root); const child = try putCheckin(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = root }); defer testing.allocator.free(child); const fork = try putCheckin(&repo, .{ .ts = "2020-01-03T00:00:00", .parent = root }); defer testing.allocator.free(fork); const close = [_]artifact.TCard{.{ .sign = .add, .name = "closed", .target = fork, .value = null }}; const ctl = try putControl(&repo, "2020-01-04T00:00:00", &close); defer testing.allocator.free(ctl); try repo.seal(); // First open with --no-cache derives from scratch AND writes a fresh cache. var scratch: Repository = undefined; try Repository.open(&scratch, testing.allocator, io, repo.tmp.dir, .{ .bypass_cache = true }); defer scratch.deinit(testing.allocator); // Second open takes the O(1) `.zit-cache` fast path. var cached: Repository = undefined; try Repository.open(&cached, testing.allocator, io, repo.tmp.dir, .{}); defer cached.deinit(testing.allocator); // The two views are observably identical (§1 memo == §7.3 rebuild). try testing.expectEqualStrings(scratch.mainBranch(), cached.mainBranch()); var ls = scratch.state().leaves(); var lc = cached.state().leaves(); while (ls.next()) |rs| { const rc = lc.next() orelse return error.LeafCountMismatch; try testing.expectEqual(rs, rc); try testing.expectEqualStrings(scratch.hashOf(rs), cached.hashOf(rc)); try testing.expectEqual(scratch.state().isOpenLeaf(rs), cached.state().isOpenLeaf(rc)); try testing.expectEqualStrings(scratch.branchOf(rs), cached.branchOf(rc)); } try testing.expect(lc.next() == null); } test "repository warmCache §1/§7.1: eagerly warming a cold repo writes a .zit-cache whose .fast_read image load is query-equivalent to a from-scratch derive" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A root, a same-branch child, and a fork — shape enough to exercise the // leaf table (§7.1), the open predicate (§7.4), and branch-of (§6.1). const root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(root); const child = try putCheckin(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = root }); defer testing.allocator.free(child); const fork = try putCheckin(&repo, .{ .ts = "2020-01-03T00:00:00", .parent = root }); defer testing.allocator.free(fork); try repo.seal(); // A fresh checkout is cold: no `.zit-cache` yet, so the first read would pay // the full derive. Eagerly warm it — exactly what runClone does at clone-end. try testing.expect(!cacheExists(&repo, io)); Repository.warmCache(testing.allocator, io, repo.tmp.dir); try testing.expect(cacheExists(&repo, io)); // The next read verb now takes the zero-parse image path (backing == .image), // NOT a cold derive, and its freed queries answer identically to a from- // scratch derive (repo-state §1 memo: derived state is a function of the log). var warm: Repository = undefined; try Repository.open(&warm, testing.allocator, io, repo.tmp.dir, .{ .mode = .fast_read }); defer warm.deinit(testing.allocator); try testing.expect(warm.state().backing == .image); var scratch: Repository = undefined; try Repository.open(&scratch, testing.allocator, io, repo.tmp.dir, .{ .bypass_cache = true }); defer scratch.deinit(testing.allocator); var ls = scratch.state().leaves(); var lw = warm.state().leaves(); while (ls.next()) |rs| { const rw = lw.next() orelse return error.LeafCountMismatch; try testing.expectEqual(rs, rw); // §7.1: same leaf set and order try testing.expectEqualStrings(scratch.hashOf(rs), warm.hashOf(rw)); // §7.2: same names try testing.expectEqual(scratch.state().isOpenLeaf(rs), warm.state().isOpenLeaf(rw)); // §7.4 try testing.expectEqualStrings(scratch.branchOf(rs), warm.branchOf(rw)); // §6.1 } try testing.expect(lw.next() == null); } test "repository manifest/blob: lazy content read returns the uncompressed blob whose hash matches (artifact §7.2)" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A plain file blob and a check-in manifest, each content-addressed by its // sha3-256 (artifact §7.2 hash-over-uncompressed). const blob_bytes = "hello, repository\n"; const blob_name = try repo.putRaw(blob_bytes); defer testing.allocator.free(blob_name); const root = try putCheckin(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(root); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); // blob: the lazily-opened log returns the exact uncompressed bytes, and the // facade verified they hash to the requested name (§7.2). const got = (try facade.blob(testing.allocator, blob_name)) orelse return error.BlobMissing; defer testing.allocator.free(got); try testing.expectEqualStrings(blob_bytes, got); var digest: [32]u8 = undefined; std.crypto.hash.sha3.Sha3_256.hash(got, &digest, .{}); const rehash = try hexName(&digest); defer testing.allocator.free(rehash); try testing.expectEqualStrings(blob_name, rehash); // §7.2: name == hash of uncompressed // An unknown name is a clean "not present", not an error. try testing.expect((try facade.blob(testing.allocator, "a" ** 64)) == null); // manifest: resolve the root, read its manifest lazily, and confirm the // round-trip through parse recovered the D-card timestamp we stored. const rid = facade.lookup(root).?; var om = (try facade.manifest(testing.allocator, rid)) orelse return error.ManifestMissing; defer om.deinit(testing.allocator); try testing.expectEqualStrings("2020-01-01T00:00:00", om.manifest.d.?.timestamp); } // F-card blob ids used by the file-set tests. Any valid 64-hex token works — // `files` walks manifests, never reads file blobs — so these need not name // stored content; they only have to round-trip through create/parse. const ha = "a" ** 64; const hb = "b" ** 64; const hc = "c" ** 64; const hd = "d" ** 64; test "repository files §8.2: a baseline manifest yields its F cards in canonical order" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // §4.5.3: F cards are sorted by decoded filename; §4.7.6: a baseline's cards // every carry a hash. The effective set is exactly the authored set, in order. const files = [_]artifact.FCard{ fcard("a", ha), fcard("b", hb), fcard("c", hc) }; const root = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &files }); defer testing.allocator.free(root); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); var list = (try facade.files(testing.allocator, facade.lookup(root).?)) orelse return error.NoFiles; defer list.deinit(testing.allocator); var got: std.ArrayList(artifact.FCard) = .empty; defer got.deinit(testing.allocator); try collectFiles(&list, &got); try testing.expectEqual(@as(usize, 3), got.items.len); try testing.expectEqualStrings("a", got.items[0].filename); try testing.expectEqualStrings("b", got.items[1].filename); try testing.expectEqualStrings("c", got.items[2].filename); try testing.expectEqualStrings(ha, got.items[0].hash.?); try testing.expectEqualStrings(hb, got.items[1].hash.?); try testing.expectEqualStrings(hc, got.items[2].hash.?); } test "repository files §8.2: delta manifest merges baseline+delta; a replaced file takes the delta hash (step 1)" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // Baseline {a:ha, b:hb}; delta replaces b (→hc, §8.2 step 1) and adds c (hd). // Effective set (§8.2 filename-ordered merge): a:ha, b:hc, c:hd. const base_files = [_]artifact.FCard{ fcard("a", ha), fcard("b", hb) }; const base = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &base_files }); defer testing.allocator.free(base); const delta_files = [_]artifact.FCard{ fcard("b", hc), fcard("c", hd) }; const delta = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = base, .baseline = base, .files = &delta_files }); defer testing.allocator.free(delta); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); var list = (try facade.files(testing.allocator, facade.lookup(delta).?)) orelse return error.NoFiles; defer list.deinit(testing.allocator); var got: std.ArrayList(artifact.FCard) = .empty; defer got.deinit(testing.allocator); try collectFiles(&list, &got); try testing.expectEqual(@as(usize, 3), got.items.len); try testing.expectEqualStrings("a", got.items[0].filename); try testing.expectEqualStrings(ha, got.items[0].hash.?); // unchanged baseline entry try testing.expectEqualStrings("b", got.items[1].filename); try testing.expectEqualStrings(hc, got.items[1].hash.?); // step 1: delta hash wins try testing.expectEqualStrings("c", got.items[2].filename); try testing.expectEqualStrings(hd, got.items[2].hash.?); // step 3: delta-only add } test "repository files §8.2: a delta delete-marker suppresses the baseline file (step 2, §8.2-r2)" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // Baseline {a:ha, b:hb}; delta carries a delete marker for b (null hash). // §8.2 step 2: matched delete emits nothing — effective set is {a:ha}. const base_files = [_]artifact.FCard{ fcard("a", ha), fcard("b", hb) }; const base = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &base_files }); defer testing.allocator.free(base); const delta_files = [_]artifact.FCard{fcard("b", null)}; const delta = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = base, .baseline = base, .files = &delta_files }); defer testing.allocator.free(delta); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); var list = (try facade.files(testing.allocator, facade.lookup(delta).?)) orelse return error.NoFiles; defer list.deinit(testing.allocator); var got: std.ArrayList(artifact.FCard) = .empty; defer got.deinit(testing.allocator); try collectFiles(&list, &got); try testing.expectEqual(@as(usize, 1), got.items.len); try testing.expectEqualStrings("a", got.items[0].filename); try testing.expectEqualStrings(ha, got.items[0].hash.?); } test "repository files §8.2: unmatched/tail delete markers never appear in the effective set (§4.7.6 hash-less card)" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // Baseline {b:hb}; delta carries two delete markers with no baseline match: // "a" (sorts before b — §8.2 step-3 unmatched) and "c" (delta tail, §8.2-m1). // Both are emitted by the raw walk but carry no hash, so the effective set is // just {b:hb} (§4.7.6: consumers filter hash-less cards). const base_files = [_]artifact.FCard{fcard("b", hb)}; const base = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &base_files }); defer testing.allocator.free(base); const delta_files = [_]artifact.FCard{ fcard("a", null), fcard("c", null) }; const delta = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = base, .baseline = base, .files = &delta_files }); defer testing.allocator.free(delta); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); var list = (try facade.files(testing.allocator, facade.lookup(delta).?)) orelse return error.NoFiles; defer list.deinit(testing.allocator); var got: std.ArrayList(artifact.FCard) = .empty; defer got.deinit(testing.allocator); try collectFiles(&list, &got); try testing.expectEqual(@as(usize, 1), got.items.len); try testing.expectEqualStrings("b", got.items[0].filename); try testing.expectEqualStrings(hb, got.items[0].hash.?); } test "repository files §8.3: an unresolvable baseline is BaselineUnresolvable (§8.3-m1)" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A delta whose B card names a baseline that was never stored. §8.3 hard // failure: file-set enumeration MUST treat it as fatal, not degrade to // delta-only. const root = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(root); const delta_files = [_]artifact.FCard{fcard("a", ha)}; const delta = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = root, .baseline = hb, .files = &delta_files }); defer testing.allocator.free(delta); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); try testing.expectError(error.BaselineUnresolvable, facade.files(testing.allocator, facade.lookup(delta).?)); } test "repository files §4.7.3: a baseline that is itself a delta is BaselineIsDelta" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A: a genuine baseline. B: a delta over A. C: a delta whose baseline is B — // a delta-of-delta, rejected at resolution time (§4.7.3). const base_files = [_]artifact.FCard{fcard("a", ha)}; const a_ci = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &base_files }); defer testing.allocator.free(a_ci); const b_files = [_]artifact.FCard{fcard("a", hb)}; const b_ci = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = a_ci, .baseline = a_ci, .files = &b_files }); defer testing.allocator.free(b_ci); const c_files = [_]artifact.FCard{fcard("a", hc)}; const c_ci = try putFiles(&repo, .{ .ts = "2020-01-03T00:00:00", .parent = b_ci, .baseline = b_ci, .files = &c_files }); defer testing.allocator.free(c_ci); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); try testing.expectError(error.BaselineIsDelta, facade.files(testing.allocator, facade.lookup(c_ci).?)); } test "repository files: a non-manifest (control) artifact is NotManifest" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A control artifact is interned (its hash gains a rid) but is not a check-in // (§5.1); `files` on it is NotManifest, never a file listing. const root = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(root); const tags = [_]artifact.TCard{.{ .sign = .add, .name = "closed", .target = root, .value = null }}; const ctl = try putControl(&repo, "2020-01-02T00:00:00", &tags); defer testing.allocator.free(ctl); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); try testing.expectError(error.NotManifest, facade.files(testing.allocator, facade.lookup(ctl).?)); } test "repository files: missing target content returns null" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A child whose parent hash was never stored: the parent is interned as a // phantom (a rid with no content). `files` on it is a clean null, not a fault. const child = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = hd }); defer testing.allocator.free(child); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); const phantom = facade.lookup(hd).?; // interned as the unstored parent try testing.expect((try facade.files(testing.allocator, phantom)) == null); } test "repository files: no leak under injected OOM at every allocation" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A delta over a baseline exercises the deepest allocation path: read+parse // the target, eagerly read+parse the baseline, then enumerate. const base_files = [_]artifact.FCard{ fcard("a", ha), fcard("b", hb) }; const base = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &base_files }); defer testing.allocator.free(base); const delta_files = [_]artifact.FCard{ fcard("b", hc), fcard("c", hd) }; const delta = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = base, .baseline = base, .files = &delta_files }); defer testing.allocator.free(delta); try repo.seal(); // A manual `FailingAllocator` sweep rather than `checkAllAllocationFailures`: // `open` runs a best-effort `.zit-cache` write that SWALLOWS OOM (the cache is // advisory), which the "never swallow" checker cannot model — the same reason // the `open` OOM test sweeps manually. Each round opens a fresh repository and // runs `files` under one failing allocator, proving both return ok or // OutOfMemory, never crash, and never leak (the wrapped allocator asserts). var i: usize = 0; while (true) : (i += 1) { var fa = std.testing.FailingAllocator.init(testing.allocator, .{ .fail_index = i }); const a = fa.allocator(); var facade: Repository = undefined; if (Repository.open(&facade, a, io, repo.tmp.dir, .{ .bypass_cache = true })) |_| { const rid = facade.lookup(delta).?; if (facade.files(a, rid)) |maybe| { if (maybe) |ml| { var list = ml; list.deinit(a); } facade.deinit(a); // The injected fail point lay at or past the last allocation, so // no failure was forced — every allocation site is covered. if (!fa.has_induced_failure) break; } else |err| { facade.deinit(a); try testing.expectEqual(error.OutOfMemory, err); } } else |err| { try testing.expectEqual(error.OutOfMemory, err); } } } test "repository §8.4: fileAt on a baseline manifest returns the named file's F-card hash; an absent name is null" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A baseline {a:ha, b:hb, c:hc}. §8.4 exact-name lookup returns the F card // whose filename equals the query; a name no card carries is null. const files = [_]artifact.FCard{ fcard("a", ha), fcard("b", hb), fcard("c", hc) }; const root = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &files }); defer testing.allocator.free(root); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); const rid = facade.lookup(root).?; const found = (try facade.fileAt(testing.allocator, rid, "b")) orelse return error.NotFound; defer Repository.freeFileCard(testing.allocator, found); try testing.expectEqualStrings("b", found.filename); try testing.expectEqualStrings(hb, found.hash.?); // §8.4: a name entirely absent from the (baseline) manifest is not-found. try testing.expect((try facade.fileAt(testing.allocator, rid, "zzz")) == null); } test "repository §8.4: fileAt searches the delta's own F cards first — a file the delta replaced returns the delta hash, not the baseline hash" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // Baseline {a:ha, b:hb}; delta replaces b (→hc) and adds c (hd). §8.4: // "b" is found in the delta's own cards first (hc wins over the baseline hb); // "a" is untouched by the delta so it comes from the baseline (ha); "c" is a // delta-only addition (hd). const base_files = [_]artifact.FCard{ fcard("a", ha), fcard("b", hb) }; const base = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &base_files }); defer testing.allocator.free(base); const delta_files = [_]artifact.FCard{ fcard("b", hc), fcard("c", hd) }; const delta = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = base, .baseline = base, .files = &delta_files }); defer testing.allocator.free(delta); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); const rid = facade.lookup(delta).?; const b = (try facade.fileAt(testing.allocator, rid, "b")) orelse return error.NotFound; defer Repository.freeFileCard(testing.allocator, b); try testing.expectEqualStrings(hc, b.hash.?); // delta hash, not baseline hb const a = (try facade.fileAt(testing.allocator, rid, "a")) orelse return error.NotFound; defer Repository.freeFileCard(testing.allocator, a); try testing.expectEqualStrings(ha, a.hash.?); // untouched baseline entry const c = (try facade.fileAt(testing.allocator, rid, "c")) orelse return error.NotFound; defer Repository.freeFileCard(testing.allocator, c); try testing.expectEqualStrings(hd, c.hash.?); // delta-only add } test "repository §8.4: a delta delete-marker reports not-found without consulting the baseline (§4.7.6 hash-less card)" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // Baseline {a:ha, b:hb}; delta carries a delete marker (null hash) for b. // §8.4: an entry whose hash is absent reports not-found for "b" without // consulting the baseline; "a" (unmentioned by the delta) still resolves. const base_files = [_]artifact.FCard{ fcard("a", ha), fcard("b", hb) }; const base = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &base_files }); defer testing.allocator.free(base); const delta_files = [_]artifact.FCard{fcard("b", null)}; const delta = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = base, .baseline = base, .files = &delta_files }); defer testing.allocator.free(delta); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); const rid = facade.lookup(delta).?; try testing.expect((try facade.fileAt(testing.allocator, rid, "b")) == null); const a = (try facade.fileAt(testing.allocator, rid, "a")) orelse return error.NotFound; defer Repository.freeFileCard(testing.allocator, a); try testing.expectEqualStrings(ha, a.hash.?); } test "repository §8.4: a name entirely absent from the delta resolves against the baseline" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // Baseline {a:ha, b:hb}; delta touches only c (hd). §8.4: "a" is entirely // absent from the delta, so the baseline is resolved and searched — ha. const base_files = [_]artifact.FCard{ fcard("a", ha), fcard("b", hb) }; const base = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &base_files }); defer testing.allocator.free(base); const delta_files = [_]artifact.FCard{fcard("c", hd)}; const delta = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = base, .baseline = base, .files = &delta_files }); defer testing.allocator.free(delta); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); const rid = facade.lookup(delta).?; const a = (try facade.fileAt(testing.allocator, rid, "a")) orelse return error.NotFound; defer Repository.freeFileCard(testing.allocator, a); try testing.expectEqualStrings(ha, a.hash.?); } test "repository §8.4: fileAt forwards the §8.3/§4.7.3 baseline faults (BaselineUnresolvable, BaselineIsDelta) unchanged" { const io = testing.io; // (a) §8.3: a delta whose B card names a never-stored baseline. fileAt runs // the same §8.2 walk as `files`, so the hard failure surfaces unchanged. { var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); const root = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00" }); defer testing.allocator.free(root); const delta_files = [_]artifact.FCard{fcard("a", ha)}; const delta = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = root, .baseline = hb, .files = &delta_files }); defer testing.allocator.free(delta); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); try testing.expectError(error.BaselineUnresolvable, facade.fileAt(testing.allocator, facade.lookup(delta).?, "a")); } // (b) §4.7.3: a delta whose baseline is itself a delta (delta-of-delta). { var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); const base_files = [_]artifact.FCard{fcard("a", ha)}; const a_ci = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &base_files }); defer testing.allocator.free(a_ci); const b_files = [_]artifact.FCard{fcard("a", hb)}; const b_ci = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = a_ci, .baseline = a_ci, .files = &b_files }); defer testing.allocator.free(b_ci); const c_files = [_]artifact.FCard{fcard("a", hc)}; const c_ci = try putFiles(&repo, .{ .ts = "2020-01-03T00:00:00", .parent = b_ci, .baseline = b_ci, .files = &c_files }); defer testing.allocator.free(c_ci); try repo.seal(); var facade: Repository = undefined; try openRepo(&facade, &repo, io); defer facade.deinit(testing.allocator); try testing.expectError(error.BaselineIsDelta, facade.fileAt(testing.allocator, facade.lookup(c_ci).?, "a")); } } test "repository §8.4: fileAt leaks nothing under injected OOM at every allocation (checkAllAllocationFailures)" { const io = testing.io; var repo: Repo = undefined; try repo.init(io); defer repo.cleanup(); // A delta over a baseline: fileAt reads+parses the target, eagerly reads+ // parses the baseline, enumerates, then duplicates the matched card — the // deepest allocation path. A manual `FailingAllocator` sweep (not // `checkAllAllocationFailures`) because `open` swallows the advisory cache // write's OOM, which the never-swallow checker cannot model. const base_files = [_]artifact.FCard{ fcard("a", ha), fcard("b", hb) }; const base = try putFiles(&repo, .{ .ts = "2020-01-01T00:00:00", .files = &base_files }); defer testing.allocator.free(base); const delta_files = [_]artifact.FCard{ fcard("b", hc), fcard("c", hd) }; const delta = try putFiles(&repo, .{ .ts = "2020-01-02T00:00:00", .parent = base, .baseline = base, .files = &delta_files }); defer testing.allocator.free(delta); try repo.seal(); var i: usize = 0; while (true) : (i += 1) { var fa = std.testing.FailingAllocator.init(testing.allocator, .{ .fail_index = i }); const a = fa.allocator(); var facade: Repository = undefined; if (Repository.open(&facade, a, io, repo.tmp.dir, .{ .bypass_cache = true })) |_| { const rid = facade.lookup(delta).?; if (facade.fileAt(a, rid, "b")) |maybe| { if (maybe) |card| Repository.freeFileCard(a, card); facade.deinit(a); if (!fa.has_induced_failure) break; // fail point past the last alloc: fully covered } else |err| { facade.deinit(a); try testing.expectEqual(error.OutOfMemory, err); } } else |err| { try testing.expectEqual(error.OutOfMemory, err); } } }