package main import ( "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" ) func TestFreeLeafHoldsAShortMessageWordForWord(t *testing.T) { message := Message{ ID: 7, Kind: "user", Text: strings.Repeat("a", nodeBytes-len("user: ")), } node, free := freeLeaf(message) require.True(t, free) require.Equal(t, Node{ Level: 0, Index: 7, Text: "user: " + message.Text, Size: nodeBytes, }, node) message.Text += "a" _, free = freeLeaf(message) require.False(t, free) } func TestFreeMergeJoinsShortSiblings(t *testing.T) { left := testNode(1, 2, "left") right := testNode(1, 3, "right") node, free := freeMerge(left, right) require.True(t, free) require.Equal(t, testNode(2, 1, "left\nright"), node) right = testNode(1, 3, strings.Repeat("r", nodeBytes-len("left"))) _, free = freeMerge(left, right) require.False(t, free) } func TestTreeBuildsEachNodeOnce(t *testing.T) { tree, err := openTree(t.Context(), t.TempDir(), 2, newTestClock().Now) require.NoError(t, err) defer func() { require.NoError(t, tree.Close()) }() require.NoError(t, tree.Add(testNode(0, 0, "user: a"))) require.ErrorContains(t, tree.Add(testNode(0, 0, "user: b")), "already built") require.ErrorContains(t, tree.Add(testNode(1, 0, "a b")), "child that is not built") require.NoError(t, tree.Add(testNode(0, 1, "reply: b"))) require.NoError(t, tree.Add(testNode(1, 0, "a b"))) require.Equal(t, "user: a", readNode(t, tree, 0, 0).Text) } func TestTreeReopenLoadsTheSameNodes(t *testing.T) { root := t.TempDir() clock := newTestClock() tree, err := openTree(t.Context(), root, 2, clock.Now) require.NoError(t, err) nodes := []Node{ testNode(0, 1, "reply: b"), testNode(0, 0, "user: a"), testNode(1, 0, "user: a\nreply: b"), } for _, node := range nodes { require.NoError(t, tree.Add(node)) } require.NoError(t, tree.Close()) data, err := os.ReadFile(filepath.Join(root, "tree", "2026-01-01.jsonl")) require.NoError(t, err) require.Equal(t, `{"l":0,"i":1,"text":"reply: b","size":8}`+"\n", strings.SplitAfter(string(data), "\n")[0]) tree, err = openTree(t.Context(), root, 2, clock.Now) require.NoError(t, err) defer func() { require.NoError(t, tree.Close()) }() require.Equal(t, len(nodes), tree.Count()) for _, node := range nodes { require.Equal(t, node, readNode(t, tree, node.Level, node.Index)) } } func TestTreeBuildsAMissingIndexAgain(t *testing.T) { root := t.TempDir() clock := newTestClock() tree, err := openTree(t.Context(), root, 2, clock.Now) require.NoError(t, err) nodes := []Node{ testNode(0, 1, "reply: b"), testNode(0, 0, "user: a"), testNode(1, 0, "user: a\nreply: b"), } for _, node := range nodes { require.NoError(t, tree.Add(node)) } require.NoError(t, tree.Close()) require.NoError(t, os.RemoveAll(filepath.Join(root, "index"))) _, err = openTree(t.Context(), root, 1, clock.Now) require.ErrorContains(t, err, "covers messages after the log") require.NoError(t, os.RemoveAll(filepath.Join(root, "index"))) tree, err = openTree(t.Context(), root, 2, clock.Now) require.NoError(t, err) defer func() { require.NoError(t, tree.Close()) }() require.Equal(t, len(nodes), tree.Count()) for _, node := range nodes { require.Equal(t, node, readNode(t, tree, node.Level, node.Index)) } } func TestTreeIndexesTheNodesAfterACrash(t *testing.T) { root := t.TempDir() clock := newTestClock() tree, err := openTree(t.Context(), root, 2, clock.Now) require.NoError(t, err) require.NoError(t, tree.Add(testNode(0, 0, "user: a"))) require.NoError(t, tree.Add(testNode(0, 1, "reply: b"))) require.NoError(t, tree.Close()) // A crash after the line write and in the middle of the index writes. order := filepath.Join(root, "index", "tree.idx") require.NoError(t, os.Truncate(order, treeRecordBytes+5)) require.NoError(t, os.Truncate(filepath.Join(root, "index", "tree-0.idx"), positionBytes)) tree, err = openTree(t.Context(), root, 2, clock.Now) require.NoError(t, err) defer func() { require.NoError(t, tree.Close()) }() require.Equal(t, 2, tree.Count()) require.Equal(t, "reply: b", readNode(t, tree, 0, 1).Text) require.NoError(t, tree.Add(testNode(1, 0, "user: a\nreply: b"))) } // isBuilt tells if the node (l, i) of tree is built. func isBuilt(t *testing.T, tree *Tree, level, index int) bool { t.Helper() built, err := tree.Built(level, index) require.NoError(t, err) return built } // readNode reads the node (l, i) of tree, which must be built. func readNode(t *testing.T, tree *Tree, level, index int) Node { t.Helper() node, built, err := tree.Node(level, index) require.NoError(t, err) require.True(t, built, "node (%d, %d)", level, index) return node } func testNode(level, index int, text string) Node { return Node{ Level: level, Index: index, Text: text, Size: len(text), } }