diff --git a/src/internal/repo/mst.zig b/src/internal/repo/mst.zig index 5f40ff7..d12d691 100644 --- a/src/internal/repo/mst.zig +++ b/src/internal/repo/mst.zig @@ -2440,3 +2440,62 @@ test "a lazily loaded tree heals a stranded empty node whose block is absent" { } try std.testing.expect(healed); } + +test "create+delete through an empty left child prunes it back to canonical" { + const alloc = std.testing.allocator; + var arena = std.heap.ArenaAllocator.init(alloc); + defer arena.deinit(); + const a = arena.allocator(); + + // zat.dev's surviving shape: an h2 entry whose right subtree (layer 1) has the + // stranded empty node as its LEFT child. Nothing lives under it, so an ordinary + // delete cannot descend into it; only a create that lands inside, followed by a + // delete, makes pruneIfEmpty fire. + const anchor = "io.atcr.manifest/1009a8cbfd067c58a9a273f7442d61307bedb0bdd4b53d71ccc1a6dd44f4441f"; + const first = "io.atcr.manifest/1f07f214ea05b602d7cf56c572af9d3ea580a35ac85863d5ad1f8c1beea0bab1"; + const trigger = "io.atcr.manifest/1b00000000000000000000000000000000000000000000000000000000000000"; + try std.testing.expectEqual(@as(u32, 2), keyHeight(anchor)); + try std.testing.expectEqual(@as(u32, 1), keyHeight(first)); + try std.testing.expectEqual(@as(u32, 0), keyHeight(trigger)); + try std.testing.expect(std.mem.lessThan(u8, anchor, trigger)); + try std.testing.expect(std.mem.lessThan(u8, trigger, first)); + + const value = try cbor.Cid.forDagCbor(a, "record"); + + var staged = Mst.init(a); + try staged.put(anchor, value); + try staged.put(first, value); + + var store = TestBlockStore{}; + // strand an empty node as the left child of the layer-1 subtree, as v0.3.10 did, + // and store its block — which is what the incident mitigation restored + const sroot = staged.root.?; + const sub = sroot.entries.items[Mst.entryLowerBound(sroot.entries.items, anchor)].right.?; + const empty_cid = try store.putBlock(a, try staged.serializeEmptyNode()); + const stranded = try staged.createNode(0); + stranded.cid = empty_cid; + stranded.dirty = false; + sub.left = stranded; + sub.dirty = true; + sroot.dirty = true; + + var staged_blocks: std.ArrayList(car.Block) = .empty; + defer staged_blocks.deinit(a); + const stranded_root = try staged.rootCid(); + try staged.collectBlocksInto(a, &staged_blocks); + for (staged_blocks.items) |b| try store.blocks.put(a, b.cid_raw, b.data); + + var oracle = Mst.init(a); + try oracle.put(anchor, value); + try oracle.put(first, value); + const canonical = try oracle.rootCid(); + try std.testing.expect(!std.mem.eql(u8, stranded_root.raw, canonical.raw)); + + // the forward commit, against the repo as it actually sits: create the trigger + // key, then delete it + var tree = try Mst.loadLazy(a, stranded_root.raw, store.reader()); + try tree.put(trigger, value); + _ = try tree.deleteReturn(trigger) orelse return error.NotFound; + + try std.testing.expectEqualSlices(u8, canonical.raw, (try tree.rootCid()).raw); +}