atproto utils for zig
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add MST tests ported from atmos, expose delete rebalancing bug

Add 10 new MST tests: get from empty tree, delete nonexistent key,
remove all keys produces empty tree CID, update existing key, order
independence (50 keys in 3 orderings), 100-key stress test, keyHeight
edge cases. One test (insert-then-remove-every-other) is skipped because
it exposes an integer overflow bug in MST tree rebalancing after
deletions — the tree.get() call panics after removing keys. This needs
investigation in the delete/rebalance path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

jcalabro bfc2c69a 4522ce53

+212
+211
src/internal/repo/mst_test.zig
··· 1 + //! additional MST tests ported from atmos (Go implementation). 2 + //! 3 + //! focuses on edge cases, stress tests, and compliance gaps not covered 4 + //! by the inline tests in mst.zig. 5 + 6 + const std = @import("std"); 7 + const mst = @import("mst.zig"); 8 + const cbor = @import("cbor.zig"); 9 + const Mst = mst.Mst; 10 + const Cid = cbor.Cid; 11 + 12 + // known empty tree CID from the AT Protocol reference implementations 13 + const empty_tree_cid = "bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm"; 14 + 15 + fn testCid(a: std.mem.Allocator, data: []const u8) !Cid { 16 + return Cid.forDagCbor(a, data); 17 + } 18 + 19 + // === edge cases === 20 + 21 + test "get from empty tree returns null" { 22 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 23 + defer arena.deinit(); 24 + const a = arena.allocator(); 25 + 26 + const tree = Mst.init(a); 27 + try std.testing.expect(tree.get("anything") == null); 28 + try std.testing.expect(tree.get("app.bsky.feed.post/abc123") == null); 29 + } 30 + 31 + test "delete nonexistent key is no-op" { 32 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 33 + defer arena.deinit(); 34 + const a = arena.allocator(); 35 + 36 + var tree = Mst.init(a); 37 + const cid = try testCid(a, "value"); 38 + try tree.put("key1", cid); 39 + 40 + // deleting a key that doesn't exist should not error 41 + try tree.delete("nonexistent"); 42 + 43 + // original key still present 44 + try std.testing.expect(tree.get("key1") != null); 45 + } 46 + 47 + test "remove all keys produces empty tree CID" { 48 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 49 + defer arena.deinit(); 50 + const a = arena.allocator(); 51 + 52 + var tree = Mst.init(a); 53 + const cid = try testCid(a, "value"); 54 + 55 + try tree.put("key1", cid); 56 + try tree.put("key2", cid); 57 + try tree.put("key3", cid); 58 + 59 + try tree.delete("key1"); 60 + try tree.delete("key2"); 61 + try tree.delete("key3"); 62 + 63 + const root = try tree.rootCid(); 64 + const expected = try mst.parseCidString(a, empty_tree_cid); 65 + try std.testing.expectEqualSlices(u8, expected.raw, root.raw); 66 + } 67 + 68 + test "update existing key changes value" { 69 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 70 + defer arena.deinit(); 71 + const a = arena.allocator(); 72 + 73 + var tree = Mst.init(a); 74 + const cid1 = try testCid(a, "v1"); 75 + const cid2 = try testCid(a, "v2"); 76 + 77 + try tree.put("key", cid1); 78 + try std.testing.expectEqualSlices(u8, cid1.raw, tree.get("key").?.raw); 79 + 80 + try tree.put("key", cid2); 81 + try std.testing.expectEqualSlices(u8, cid2.raw, tree.get("key").?.raw); 82 + } 83 + 84 + // === order independence === 85 + 86 + test "order independence: 50 keys in 3 orderings produce same root CID" { 87 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 88 + defer arena.deinit(); 89 + const a = arena.allocator(); 90 + 91 + // generate 50 deterministic keys 92 + const n = 50; 93 + var keys: [n][]const u8 = undefined; 94 + var key_bufs: [n][64]u8 = undefined; 95 + for (0..n) |i| { 96 + const len = std.fmt.bufPrint(&key_bufs[i], "app.bsky.feed.post/{d:0>12}", .{i}) catch unreachable; 97 + keys[i] = len; 98 + } 99 + 100 + const val = try testCid(a, "value"); 101 + 102 + // forward order 103 + var tree1 = Mst.init(a); 104 + for (keys) |k| try tree1.put(k, val); 105 + const cid1 = try tree1.rootCid(); 106 + 107 + // reverse order 108 + var tree2 = Mst.init(a); 109 + var i: usize = n; 110 + while (i > 0) { 111 + i -= 1; 112 + try tree2.put(keys[i], val); 113 + } 114 + const cid2 = try tree2.rootCid(); 115 + 116 + // interleaved order (even indices first, then odd) 117 + var tree3 = Mst.init(a); 118 + for (0..n) |j| { 119 + if (j % 2 == 0) try tree3.put(keys[j], val); 120 + } 121 + for (0..n) |j| { 122 + if (j % 2 == 1) try tree3.put(keys[j], val); 123 + } 124 + const cid3 = try tree3.rootCid(); 125 + 126 + try std.testing.expectEqualSlices(u8, cid1.raw, cid2.raw); 127 + try std.testing.expectEqualSlices(u8, cid1.raw, cid3.raw); 128 + } 129 + 130 + // === stress tests === 131 + 132 + test "100 keys: all retrievable after insertion" { 133 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 134 + defer arena.deinit(); 135 + const a = arena.allocator(); 136 + 137 + const n = 100; 138 + var tree = Mst.init(a); 139 + var key_bufs: [n][64]u8 = undefined; 140 + var keys: [n][]const u8 = undefined; 141 + var cids: [n]Cid = undefined; 142 + 143 + for (0..n) |i| { 144 + keys[i] = std.fmt.bufPrint(&key_bufs[i], "app.bsky.feed.post/{d:0>12}", .{i}) catch unreachable; 145 + cids[i] = try testCid(a, keys[i]); 146 + try tree.put(keys[i], cids[i]); 147 + } 148 + 149 + // all keys retrievable with correct CIDs 150 + for (0..n) |i| { 151 + const got = tree.get(keys[i]) orelse { 152 + std.debug.print("FAIL: key {s} not found after insert\n", .{keys[i]}); 153 + return error.TestExpectedEqual; 154 + }; 155 + try std.testing.expectEqualSlices(u8, cids[i].raw, got.raw); 156 + } 157 + } 158 + 159 + // TODO: this test exposes an integer overflow bug in MST tree rebalancing 160 + // after deletions. The tree.get() call panics after removing keys. 161 + // Needs investigation in the delete/rebalance path of mst.zig. 162 + test "insert 10 keys then remove every other" { 163 + if (true) return error.SkipZigTest; // skip until MST delete bug is fixed 164 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 165 + defer arena.deinit(); 166 + const a = arena.allocator(); 167 + 168 + const n = 10; 169 + var tree = Mst.init(a); 170 + var key_bufs: [n][64]u8 = undefined; 171 + var keys: [n][]const u8 = undefined; 172 + const val = try testCid(a, "value"); 173 + 174 + for (0..n) |i| { 175 + keys[i] = std.fmt.bufPrint(&key_bufs[i], "app.bsky.feed.post/{d:0>12}", .{i}) catch unreachable; 176 + try tree.put(keys[i], val); 177 + } 178 + 179 + // remove even-indexed keys 180 + for (0..n) |i| { 181 + if (i % 2 == 0) try tree.delete(keys[i]); 182 + } 183 + 184 + // verify: even keys gone, odd keys present 185 + for (0..n) |i| { 186 + const got = tree.get(keys[i]); 187 + if (i % 2 == 0) { 188 + try std.testing.expect(got == null); 189 + } else { 190 + try std.testing.expect(got != null); 191 + } 192 + } 193 + } 194 + 195 + // === height edge cases === 196 + 197 + test "keyHeight: empty key has height 0" { 198 + try std.testing.expectEqual(@as(u32, 0), mst.keyHeight("")); 199 + } 200 + 201 + test "keyHeight: deterministic for same input" { 202 + const h1 = mst.keyHeight("app.bsky.feed.post/3jqfcqzm3fo2j"); 203 + const h2 = mst.keyHeight("app.bsky.feed.post/3jqfcqzm3fo2j"); 204 + try std.testing.expectEqual(h1, h2); 205 + } 206 + 207 + test "keyHeight: different keys can have different heights" { 208 + // "blue" is known to have height 1 from the interop fixtures 209 + try std.testing.expectEqual(@as(u32, 1), mst.keyHeight("blue")); 210 + try std.testing.expectEqual(@as(u32, 0), mst.keyHeight("asdf")); 211 + }
+1
src/root.zig
··· 78 78 _ = @import("internal/repo/cbor_write_test.zig"); 79 79 _ = @import("internal/repo/car_test.zig"); 80 80 _ = @import("internal/repo/cbor_rfc8949_test.zig"); 81 + _ = @import("internal/repo/mst_test.zig"); 81 82 } 82 83 }