···3344 doctest MST.Height
5566+ @interop_fixtures Path.join([__DIR__, "..", "fixtures", "interop", "key_heights.json"])
77+ |> File.read!()
88+ |> Jason.decode!()
99+610 describe "for_key/1" do
711 # Spec examples from https://atproto.com/specs/repository#mst-structure
812 test "spec example: depth 0" do
···4852 d0 = MST.Height.for_key("2653ae71")
4953 d1 = MST.Height.for_key("blue")
5054 assert d0 != d1
5555+ end
5656+ end
5757+5858+ describe "key_heights.json interop" do
5959+ test "all fixture entries match expected height" do
6060+ for %{"key" => key, "height" => expected} <- @interop_fixtures do
6161+ assert MST.Height.for_key(key) == expected,
6262+ "for_key(#{inspect(key)}) expected #{expected}, got #{MST.Height.for_key(key)}"
6363+ end
5164 end
5265 end
5366end
+315
test/mst/interop_test.exs
···11+defmodule MST.InteropTest do
22+ @moduledoc """
33+ Interoperability tests using fixtures from atproto-interop-tests and
44+ jacquard's additional edge-case vectors.
55+66+ Fixture sources:
77+ - https://github.com/bluesky-social/atproto-interop-tests/tree/main/mst
88+ - https://github.com/orual/jacquard (tests/fixtures/)
99+1010+ Covers:
1111+ - 156 real-world-shaped keys: full insert, selective delete, insertion-order
1212+ determinism.
1313+ - 5 commit-proof CID-exact scenarios stressing height-gap splits, leafless
1414+ splits, edge inserts, and merge-then-split sequences.
1515+ - The "rsky" 2-key tree regression from rsky's
1616+ `handle_new_layers_that_are_two_higher_than_existing` test.
1717+ - Trees spanning heights 0–8 to exercise multi-level intermediate empty
1818+ nodes.
1919+ """
2020+2121+ use ExUnit.Case, async: true
2222+2323+ alias DASL.CID
2424+ alias MST.Tree
2525+2626+ @fixture_dir Path.join([__DIR__, "..", "fixtures", "interop"])
2727+2828+ # 156 keys of the form "X{level}/{number}" where each key's MST height
2929+ # matches the digit in the name (generated by atproto-interop-tests/mst/gen_keys.py).
3030+ @example_keys Path.join(@fixture_dir, "example_keys.txt")
3131+ |> File.read!()
3232+ |> String.split("\n", trim: true)
3333+3434+ # Stable key→CID mapping reused across example-key tests so that insertion
3535+ # order doesn't affect which value a key maps to.
3636+ @example_kv Map.new(Enum.with_index(@example_keys), fn {k, i} ->
3737+ {k, CID.compute(<<i::8>>, :raw)}
3838+ end)
3939+4040+ # 5 commit-proof scenarios; each specifies an initial key set, a batch of
4141+ # adds/deletes, and the expected root CIDs before and after.
4242+ @commit_proof Path.join(@fixture_dir, "commit_proof.json")
4343+ |> File.read!()
4444+ |> Jason.decode!()
4545+4646+ defp new_tree, do: Tree.new(MST.Store.Memory.new())
4747+4848+ # ---------------------------------------------------------------------------
4949+ # Example keys — insert / delete / determinism
5050+ # ---------------------------------------------------------------------------
5151+5252+ describe "example keys" do
5353+ @tag :slow
5454+ test "insert all keys and retrieve each" do
5555+ tree =
5656+ Enum.reduce(@example_keys, new_tree(), fn key, acc ->
5757+ {:ok, t} = Tree.put(acc, key, @example_kv[key])
5858+ t
5959+ end)
6060+6161+ for key <- @example_keys do
6262+ assert {:ok, @example_kv[key]} == Tree.get(tree, key),
6363+ "key not found after insert: #{key}"
6464+ end
6565+6666+ assert {:ok, count} = Tree.length(tree)
6767+ assert count == length(@example_keys)
6868+ end
6969+7070+ @tag :slow
7171+ test "delete every other key; correct half remains" do
7272+ tree =
7373+ Enum.reduce(@example_keys, new_tree(), fn key, acc ->
7474+ {:ok, t} = Tree.put(acc, key, @example_kv[key])
7575+ t
7676+ end)
7777+7878+ indexed = Enum.with_index(@example_keys)
7979+ {evens, odds} = Enum.split_with(indexed, fn {_, i} -> rem(i, 2) == 0 end)
8080+8181+ tree =
8282+ Enum.reduce(evens, tree, fn {key, _}, acc ->
8383+ {:ok, t} = Tree.delete(acc, key)
8484+ t
8585+ end)
8686+8787+ for {key, _} <- evens do
8888+ assert {:error, :not_found} == Tree.get(tree, key),
8989+ "deleted key still present: #{key}"
9090+ end
9191+9292+ for {key, _} <- odds do
9393+ assert {:ok, @example_kv[key]} == Tree.get(tree, key),
9494+ "surviving key not found: #{key}"
9595+ end
9696+9797+ assert {:ok, remaining} = Tree.length(tree)
9898+ assert remaining == length(odds)
9999+ end
100100+101101+ @tag :slow
102102+ test "root CID is identical regardless of insertion order" do
103103+ forward =
104104+ Enum.reduce(@example_keys, new_tree(), fn key, acc ->
105105+ {:ok, t} = Tree.put(acc, key, @example_kv[key])
106106+ t
107107+ end)
108108+109109+ reverse =
110110+ Enum.reduce(Enum.reverse(@example_keys), new_tree(), fn key, acc ->
111111+ {:ok, t} = Tree.put(acc, key, @example_kv[key])
112112+ t
113113+ end)
114114+115115+ assert forward.root == reverse.root,
116116+ "insertion order changed root CID"
117117+ end
118118+119119+ @tag :slow
120120+ test "delete all keys produces empty tree" do
121121+ tree =
122122+ Enum.reduce(@example_keys, new_tree(), fn key, acc ->
123123+ {:ok, t} = Tree.put(acc, key, @example_kv[key])
124124+ t
125125+ end)
126126+127127+ empty =
128128+ Enum.reduce(@example_keys, tree, fn key, acc ->
129129+ {:ok, t} = Tree.delete(acc, key)
130130+ t
131131+ end)
132132+133133+ assert {:ok, []} = Tree.to_list(empty)
134134+ assert empty.root == nil
135135+ end
136136+ end
137137+138138+ # ---------------------------------------------------------------------------
139139+ # Commit-proof fixtures — CID-exact spec vectors
140140+ # ---------------------------------------------------------------------------
141141+142142+ describe "commit proof fixtures" do
143143+ # Each fixture drives a scenario that would catch specific structural bugs:
144144+ #
145145+ # "two deep split" — height-2 insert between height-1 nodes,
146146+ # requires two levels of intermediate empties.
147147+ # "two deep leafless split" — height-2 insert with no height-1 nodes
148148+ # anywhere near the split point.
149149+ # "add on edge with neighbor two layers down"
150150+ # — new height-2 key adjacent to a subtree
151151+ # whose highest key is 2 levels lower.
152152+ # "merge and split in multi-op" — simultaneous adds and deletes; the
153153+ # merge path (delete) and split path
154154+ # (insert) both execute.
155155+ # "complex multi-op commit" — larger batch with both creates and
156156+ # deletes across multiple height levels.
157157+158158+ for fixture <- @commit_proof do
159159+ @fixture fixture
160160+ test @fixture["comment"] do
161161+ run_commit_proof_fixture(@fixture)
162162+ end
163163+ end
164164+ end
165165+166166+ # ---------------------------------------------------------------------------
167167+ # rsky edge case — 2-key tree with a known root CID
168168+ # ---------------------------------------------------------------------------
169169+170170+ describe "rsky simple case" do
171171+ # Regression from rsky's `handle_new_layers_that_are_two_higher_than_existing`.
172172+ # Two height-0 keys (same collection prefix) that differ only in their last
173173+ # few chars. The expected root CID is taken from the reference TypeScript
174174+ # implementation.
175175+ test "two height-0 keys produce the known root CID" do
176176+ {:ok, leaf} = CID.new("bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454")
177177+178178+ {:ok, tree} = Tree.put(new_tree(), "com.example.record/3jqfcqzm3ft2j", leaf)
179179+ {:ok, tree} = Tree.put(tree, "com.example.record/3jqfcqzm3fz2j", leaf)
180180+181181+ assert CID.encode(tree.root) ==
182182+ "bafyreidfcktqnfmykz2ps3dbul35pepleq7kvv526g47xahuz3rqtptmky"
183183+ end
184184+ end
185185+186186+ # ---------------------------------------------------------------------------
187187+ # Multi-height trees — spec keys spanning heights 0–8
188188+ # ---------------------------------------------------------------------------
189189+190190+ describe "keys spanning multiple heights" do
191191+ # Keys taken directly from the atproto spec and key_heights.json; each has
192192+ # a well-known height. Together they force intermediate empty nodes at every
193193+ # level from 0 up to the maximum height.
194194+ @spec_keys [
195195+ {"2653ae71", 0},
196196+ {"blue", 1},
197197+ {"88bfafc7", 2},
198198+ {"2a92d355", 4},
199199+ {"884976f5", 6},
200200+ {"app.bsky.feed.post/9adeb165882c", 8}
201201+ ]
202202+203203+ test "insert keys at heights 0/1/2/4/6/8 — all retrievable" do
204204+ val = CID.compute("v", :raw)
205205+206206+ tree =
207207+ Enum.reduce(@spec_keys, new_tree(), fn {key, _}, acc ->
208208+ {:ok, t} = Tree.put(acc, key, val)
209209+ t
210210+ end)
211211+212212+ for {key, _} <- @spec_keys do
213213+ assert {:ok, ^val} = Tree.get(tree, key), "not found: #{key}"
214214+ end
215215+216216+ assert {:ok, n} = Tree.length(tree)
217217+ assert n == length(@spec_keys)
218218+ end
219219+220220+ test "delete the height-4 key; other heights unaffected" do
221221+ val = CID.compute("v", :raw)
222222+223223+ tree =
224224+ Enum.reduce(@spec_keys, new_tree(), fn {key, _}, acc ->
225225+ {:ok, t} = Tree.put(acc, key, val)
226226+ t
227227+ end)
228228+229229+ {del_key, _} = Enum.find(@spec_keys, fn {_, h} -> h == 4 end)
230230+ {:ok, tree} = Tree.delete(tree, del_key)
231231+232232+ assert {:error, :not_found} = Tree.get(tree, del_key)
233233+234234+ for {key, _} <- @spec_keys, key != del_key do
235235+ assert {:ok, ^val} = Tree.get(tree, key), "not found after delete: #{key}"
236236+ end
237237+ end
238238+239239+ test "root CID is stable regardless of insertion order" do
240240+ val = CID.compute("v", :raw)
241241+ keys = Enum.map(@spec_keys, &elem(&1, 0))
242242+243243+ forward =
244244+ Enum.reduce(keys, new_tree(), fn key, acc ->
245245+ {:ok, t} = Tree.put(acc, key, val)
246246+ t
247247+ end)
248248+249249+ reverse =
250250+ Enum.reduce(Enum.reverse(keys), new_tree(), fn key, acc ->
251251+ {:ok, t} = Tree.put(acc, key, val)
252252+ t
253253+ end)
254254+255255+ assert forward.root == reverse.root
256256+ end
257257+258258+ test "full delete cycle returns to empty" do
259259+ val = CID.compute("v", :raw)
260260+ keys = Enum.map(@spec_keys, &elem(&1, 0))
261261+262262+ tree =
263263+ Enum.reduce(keys, new_tree(), fn key, acc ->
264264+ {:ok, t} = Tree.put(acc, key, val)
265265+ t
266266+ end)
267267+268268+ empty =
269269+ Enum.reduce(keys, tree, fn key, acc ->
270270+ {:ok, t} = Tree.delete(acc, key)
271271+ t
272272+ end)
273273+274274+ assert {:ok, []} = Tree.to_list(empty)
275275+ assert empty.root == nil
276276+ end
277277+ end
278278+279279+ # ---------------------------------------------------------------------------
280280+ # Helpers
281281+ # ---------------------------------------------------------------------------
282282+283283+ defp run_commit_proof_fixture(fixture) do
284284+ {:ok, leaf} = CID.new(fixture["leafValue"])
285285+286286+ before_tree =
287287+ Enum.reduce(fixture["keys"], new_tree(), fn key, acc ->
288288+ {:ok, t} = Tree.put(acc, key, leaf)
289289+ t
290290+ end)
291291+292292+ assert CID.encode(before_tree.root) == fixture["rootBeforeCommit"],
293293+ ~s(root before commit: expected #{fixture["rootBeforeCommit"]}, ) <>
294294+ ~s(got #{CID.encode(before_tree.root)})
295295+296296+ after_tree =
297297+ before_tree
298298+ |> then(fn t ->
299299+ Enum.reduce(fixture["adds"], t, fn key, acc ->
300300+ {:ok, t2} = Tree.put(acc, key, leaf)
301301+ t2
302302+ end)
303303+ end)
304304+ |> then(fn t ->
305305+ Enum.reduce(fixture["dels"], t, fn key, acc ->
306306+ {:ok, t2} = Tree.delete(acc, key)
307307+ t2
308308+ end)
309309+ end)
310310+311311+ assert CID.encode(after_tree.root) == fixture["rootAfterCommit"],
312312+ ~s(root after commit: expected #{fixture["rootAfterCommit"]}, ) <>
313313+ ~s(got #{CID.encode(after_tree.root)})
314314+ end
315315+end
+21
test/mst/node_test.exs
···1212 @cid_b CID.compute("value_b", :raw)
1313 @cid_c CID.compute("value_c", :raw)
14141515+ @prefix_interop Path.join([__DIR__, "..", "fixtures", "interop", "common_prefix.json"])
1616+ |> File.read!()
1717+ |> Jason.decode!()
1818+1519 describe "empty/0" do
1620 test "returns an empty node" do
1721 assert %Node{left: nil, entries: []} = Node.empty()
···177181 entries = Node.compress_entries(triples)
178182 node = %Node{left: nil, entries: entries}
179183 assert Node.keys(node) == keys
184184+ end
185185+ end
186186+187187+ describe "common_prefix.json interop" do
188188+ # The common prefix length between two keys is what determines prefix_len
189189+ # in the second of any two adjacent compress_entries inputs. We test via
190190+ # compress_entries since common_prefix_length/2 is private.
191191+ test "all fixture pairs produce correct prefix_len" do
192192+ cid = CID.compute("test", :raw)
193193+194194+ for %{"left" => left, "right" => right, "len" => expected} <- @prefix_interop do
195195+ [_first, second] = Node.compress_entries([{left, cid, nil}, {right, cid, nil}])
196196+197197+ assert second.prefix_len == expected,
198198+ "common_prefix(#{inspect(left)}, #{inspect(right)}) " <>
199199+ "expected #{expected}, got #{second.prefix_len}"
200200+ end
180201 end
181202 end
182203
+285
test/mst/stress_test.exs
···11+defmodule MST.StressTest do
22+ @moduledoc """
33+ Stress tests for MST structural correctness under long random mutation sequences.
44+55+ Inspired by jacquard's `large_proof_tests.rs`, which applies hundreds of
66+ random create/update/delete operations and validates commit proofs after each
77+ batch. We can't validate firehose commits (that layer doesn't exist here), but
88+ we capture the same invariant: after every mutation the tree's contents must
99+ exactly match a shadow map maintained in parallel.
1010+1111+ Three test dimensions:
1212+ - **small**: 100 ops verified after every single operation — catches bugs in
1313+ individual transitions.
1414+ - **large**: 300 ops verified after every batch of 10 — catches accumulated
1515+ drift.
1616+ - **high-height**: 50 ops over a pool that includes height 8 keys — stresses
1717+ intermediate empty-node creation and destruction.
1818+1919+ Each test ends with a determinism check: rebuild the same key-value set into
2020+ a fresh tree and assert the root CID matches the evolved tree.
2121+ """
2222+2323+ use ExUnit.Case, async: true
2424+2525+ alias DASL.CID
2626+ alias MST.Tree
2727+2828+ @fixture_dir Path.join([__DIR__, "..", "fixtures", "interop"])
2929+3030+ # 81 real-world-shaped keys at heights 0–5 from the interop fixture.
3131+ @key_pool @fixture_dir
3232+ |> Path.join("example_keys.txt")
3333+ |> File.read!()
3434+ |> String.split("\n", trim: true)
3535+3636+ # Additional keys at heights 2, 4, 6, 8 — not in the example_keys file —
3737+ # to ensure multi-level intermediate empty nodes are exercised.
3838+ @high_height_keys [
3939+ "88bfafc7",
4040+ "2a92d355",
4141+ "884976f5",
4242+ "app.bsky.feed.post/9adeb165882c"
4343+ ]
4444+4545+ @full_pool @key_pool ++ @high_height_keys
4646+4747+ # Keys used for the focused high-height test. Heights: 0, 0, 1, 2, 4, 4, 6, 8.
4848+ @height_spanning_keys [
4949+ "2653ae71",
5050+ "asdf",
5151+ "blue",
5252+ "88bfafc7",
5353+ "2a92d355",
5454+ "app.bsky.feed.post/454397e440ec",
5555+ "884976f5",
5656+ "app.bsky.feed.post/9adeb165882c"
5757+ ]
5858+5959+ defp new_tree, do: Tree.new(MST.Store.Memory.new())
6060+6161+ # ---------------------------------------------------------------------------
6262+ # PRNG helpers — explicit state threading for reproducibility
6363+ # ---------------------------------------------------------------------------
6464+6565+ # Returns {{:put, key, val} | {:delete, key}, new_rng_state}.
6666+ # Weights: 50% create-or-overwrite, 30% update-existing, 20% delete-existing.
6767+ # Falls back to create when the shadow is empty.
6868+ @spec rand_op(:rand.state(), map(), [binary()]) ::
6969+ {{:put, binary(), CID.t()} | {:delete, binary()}, :rand.state()}
7070+ defp rand_op(rng, shadow, pool) do
7171+ {dice, rng} = :rand.uniform_s(100, rng)
7272+ existing = Map.keys(shadow)
7373+ n = length(existing)
7474+7575+ cond do
7676+ dice <= 50 or n == 0 ->
7777+ {idx, rng} = :rand.uniform_s(length(pool), rng)
7878+ {seed, rng} = :rand.uniform_s(1_000_000_000, rng)
7979+ key = Enum.at(pool, idx - 1)
8080+ val = CID.compute("#{key}:#{seed}", :raw)
8181+ {{:put, key, val}, rng}
8282+8383+ dice <= 80 ->
8484+ {idx, rng} = :rand.uniform_s(n, rng)
8585+ {seed, rng} = :rand.uniform_s(1_000_000_000, rng)
8686+ key = Enum.at(existing, idx - 1)
8787+ val = CID.compute("#{key}:#{seed}", :raw)
8888+ {{:put, key, val}, rng}
8989+9090+ true ->
9191+ {idx, rng} = :rand.uniform_s(n, rng)
9292+ key = Enum.at(existing, idx - 1)
9393+ {{:delete, key}, rng}
9494+ end
9595+ end
9696+9797+ # Apply a single op to both the tree and shadow map.
9898+ @spec apply_op(Tree.t(), map(), {:put, binary(), CID.t()} | {:delete, binary()}) ::
9999+ {Tree.t(), map()}
100100+ defp apply_op(tree, shadow, {:put, key, val}) do
101101+ {:ok, tree} = Tree.put(tree, key, val)
102102+ {tree, Map.put(shadow, key, val)}
103103+ end
104104+105105+ defp apply_op(tree, shadow, {:delete, key}) do
106106+ {:ok, tree} = Tree.delete(tree, key)
107107+ {tree, Map.delete(shadow, key)}
108108+ end
109109+110110+ # Assert every key in `shadow` is present in `tree` with the correct value,
111111+ # that the sizes match, and that `to_list/1` returns keys in sorted order.
112112+ @spec assert_matches_shadow(Tree.t(), map()) :: :ok
113113+ defp assert_matches_shadow(tree, shadow) do
114114+ {:ok, pairs} = Tree.to_list(tree)
115115+116116+ assert length(pairs) == map_size(shadow),
117117+ "size mismatch: tree has #{length(pairs)} keys, shadow has #{map_size(shadow)}"
118118+119119+ keys = Enum.map(pairs, &elem(&1, 0))
120120+ assert keys == Enum.sort(keys), "to_list/1 returned keys out of order"
121121+122122+ for {key, expected_val} <- shadow do
123123+ assert {:ok, ^expected_val} = Tree.get(tree, key),
124124+ "wrong value for #{inspect(key)}"
125125+ end
126126+127127+ :ok
128128+ end
129129+130130+ # Build a fresh tree from a shadow map (sorted insertion order) and return
131131+ # the root CID. Used to verify determinism: the evolved tree and the
132132+ # freshly-built tree must share the same root.
133133+ @spec root_from_shadow(map()) :: CID.t() | nil
134134+ defp root_from_shadow(shadow) do
135135+ shadow
136136+ |> Enum.sort_by(&elem(&1, 0))
137137+ |> Enum.reduce(new_tree(), fn {key, val}, acc ->
138138+ {:ok, t} = Tree.put(acc, key, val)
139139+ t
140140+ end)
141141+ |> Map.fetch!(:root)
142142+ end
143143+144144+ # ---------------------------------------------------------------------------
145145+ # Tests
146146+ # ---------------------------------------------------------------------------
147147+148148+ describe "stress" do
149149+ # -------------------------------------------------------------------------
150150+ # Small: 100 ops, verified after every single mutation
151151+ # -------------------------------------------------------------------------
152152+153153+ @tag :slow
154154+ test "100 random ops over 20-key seed, verified after each op" do
155155+ rng = :rand.seed_s(:exsss, {42, 1337, 99})
156156+157157+ seed_keys = Enum.take(@full_pool, 20)
158158+159159+ {tree, shadow} =
160160+ Enum.reduce(seed_keys, {new_tree(), %{}}, fn key, {t, s} ->
161161+ val = CID.compute("seed:#{key}", :raw)
162162+ {:ok, t} = Tree.put(t, key, val)
163163+ {t, Map.put(s, key, val)}
164164+ end)
165165+166166+ assert_matches_shadow(tree, shadow)
167167+168168+ {tree, shadow, _rng} =
169169+ Enum.reduce(1..100, {tree, shadow, rng}, fn _i, {t, s, rng} ->
170170+ {op, rng} = rand_op(rng, s, @full_pool)
171171+ {t, s} = apply_op(t, s, op)
172172+ assert_matches_shadow(t, s)
173173+ {t, s, rng}
174174+ end)
175175+176176+ assert tree.root == root_from_shadow(shadow),
177177+ "evolved tree root differs from scratch-rebuilt root — history-dependence bug"
178178+ end
179179+180180+ # -------------------------------------------------------------------------
181181+ # Large: 300 ops in batches of 10, verified after each batch
182182+ # -------------------------------------------------------------------------
183183+184184+ @tag :slow
185185+ test "300 random ops over 50-key seed, verified per batch of 10" do
186186+ rng = :rand.seed_s(:exsss, {7, 13, 21})
187187+188188+ seed_keys = Enum.take(@full_pool, 50)
189189+190190+ {tree, shadow} =
191191+ Enum.reduce(seed_keys, {new_tree(), %{}}, fn key, {t, s} ->
192192+ val = CID.compute("seed:#{key}", :raw)
193193+ {:ok, t} = Tree.put(t, key, val)
194194+ {t, Map.put(s, key, val)}
195195+ end)
196196+197197+ {tree, shadow, _rng} =
198198+ Enum.reduce(1..30, {tree, shadow, rng}, fn _batch, {t, s, rng} ->
199199+ {t, s, rng} =
200200+ Enum.reduce(1..10, {t, s, rng}, fn _i, {t, s, rng} ->
201201+ {op, rng} = rand_op(rng, s, @full_pool)
202202+ {t, s} = apply_op(t, s, op)
203203+ {t, s, rng}
204204+ end)
205205+206206+ assert_matches_shadow(t, s)
207207+ {t, s, rng}
208208+ end)
209209+210210+ assert tree.root == root_from_shadow(shadow),
211211+ "evolved tree root differs from scratch-rebuilt root — history-dependence bug"
212212+ end
213213+214214+ # -------------------------------------------------------------------------
215215+ # High-height: 50 ops over a pool spanning heights 0–8, per-op verified
216216+ # -------------------------------------------------------------------------
217217+218218+ @tag :slow
219219+ test "50 random ops over height-spanning pool (h0–h8), verified after each op" do
220220+ # This specifically targets the intermediate empty-node paths: keys at
221221+ # heights 6 and 8 force multiple levels of empty wrappers. Deleting them
222222+ # exercises trim_top and the recursive merge across those levels.
223223+ rng = :rand.seed_s(:exsss, {100, 200, 300})
224224+225225+ {tree, shadow} =
226226+ Enum.reduce(@height_spanning_keys, {new_tree(), %{}}, fn key, {t, s} ->
227227+ val = CID.compute("seed:#{key}", :raw)
228228+ {:ok, t} = Tree.put(t, key, val)
229229+ {t, Map.put(s, key, val)}
230230+ end)
231231+232232+ assert_matches_shadow(tree, shadow)
233233+234234+ {tree, shadow, _rng} =
235235+ Enum.reduce(1..50, {tree, shadow, rng}, fn _i, {t, s, rng} ->
236236+ {op, rng} = rand_op(rng, s, @height_spanning_keys)
237237+ {t, s} = apply_op(t, s, op)
238238+ assert_matches_shadow(t, s)
239239+ {t, s, rng}
240240+ end)
241241+242242+ assert tree.root == root_from_shadow(shadow),
243243+ "evolved tree root differs from scratch-rebuilt root — history-dependence bug"
244244+ end
245245+246246+ # -------------------------------------------------------------------------
247247+ # Delete-all then re-insert: evolved root must match original
248248+ # -------------------------------------------------------------------------
249249+250250+ @tag :slow
251251+ test "delete all keys then re-insert in reverse order produces original root" do
252252+ # Covers the full tree lifecycle: grow → shrink to nil → regrow.
253253+ # Uses height-spanning keys so intermediate empty nodes are created and
254254+ # destroyed at every level.
255255+ keys = @height_spanning_keys ++ Enum.take(@key_pool, 22)
256256+257257+ {original_tree, shadow} =
258258+ Enum.reduce(keys, {new_tree(), %{}}, fn key, {t, s} ->
259259+ val = CID.compute("v:#{key}", :raw)
260260+ {:ok, t} = Tree.put(t, key, val)
261261+ {t, Map.put(s, key, val)}
262262+ end)
263263+264264+ assert_matches_shadow(original_tree, shadow)
265265+266266+ empty =
267267+ Enum.reduce(keys, original_tree, fn key, t ->
268268+ {:ok, t} = Tree.delete(t, key)
269269+ t
270270+ end)
271271+272272+ assert empty.root == nil
273273+ assert {:ok, []} = Tree.to_list(empty)
274274+275275+ rebuilt =
276276+ Enum.reduce(Enum.reverse(keys), new_tree(), fn key, t ->
277277+ {:ok, t} = Tree.put(t, key, shadow[key])
278278+ t
279279+ end)
280280+281281+ assert rebuilt.root == original_tree.root,
282282+ "re-insert in reverse order produced a different root — determinism bug"
283283+ end
284284+ end
285285+end