atproto utils for zig zat.dev
atproto sdk zig
26
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 }