package beads import ( "context" "fmt" "net/url" "sort" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sourcecraft.dev/bigbes/sr-ht-dolt/browse" ) // --- fixture ----------------------------------------------------------------- // memoryNow is the instant the memory tests measure staleness against. Nothing // about it is special beyond being fixed: staleness is a function of the // fixture's dates and this clock, never of when the suite ran. var memoryNow = time.Date(2026, 8, 12, 12, 0, 0, 0, time.UTC) // fakeHistory is a MemorySession over canned per-ref content: the config table // at each commit, that table's content hash there, and a commit log. It records // every ref a row read was issued at, which is how the walk's central claim — // that a commit whose table hash is unchanged is skipped without reading a // single row — is asserted rather than assumed. type fakeHistory struct { commits []browse.CommitInfo // newest first, as Log returns them config map[string]map[string]string // ref → key → value; a missing ref has no config table hashes map[string]string // ref → config's content hash; "" = table absent reads []string // refs Rows was called at, in order hashCalls int logCalls int } func (f *fakeHistory) Rows(_ context.Context, ref, table string, _, _ int) (*browse.RowPage, error) { f.reads = append(f.reads, ref) rows, ok := f.config[ref] if !ok || table != "config" { return nil, fmt.Errorf("%w: %s", browse.ErrTableNotFound, table) } page := &browse.RowPage{Columns: []string{"key", "value"}} keys := make([]string, 0, len(rows)) for k := range rows { keys = append(keys, k) } sort.Strings(keys) // a store returns rows in key order; so does this for _, k := range keys { page.Rows = append(page.Rows, []string{k, rows[k]}) } page.Total = len(page.Rows) return page, nil } func (f *fakeHistory) Log(_ context.Context, _, _ string, limit int) ([]browse.CommitInfo, string, error) { f.logCalls++ if limit < len(f.commits) { return f.commits[:limit], f.commits[limit].Hash, nil } return f.commits, "", nil } func (f *fakeHistory) TableHash(_ context.Context, ref, table string) (string, bool, error) { f.hashCalls++ if table != "config" { return "", false, nil } h, ok := f.hashes[ref] if !ok || h == "" { return "", false, nil } return h, true, nil } // memoryHistory is the fixture the walk is asserted against: five commits, and a // config table that changes at only two of them. // // c4 (3 hours ago) alpha=v2 beta=b gamma=g hash h-c ← head // c3 (2 days ago) alpha=v2 beta=b gamma=g hash h-c wrote alpha // c2 (71 days ago) alpha=v1 beta=b gamma=g hash h-b // c1 (99 days ago) alpha=v1 beta=b gamma=g hash h-b wrote beta // c0 (200 days ago) gamma=g hash h-a the root // // So alpha resolves to c3, beta to c1, and gamma — never changed anywhere in the // history — to the root commit that introduced it. func memoryHistory() *fakeHistory { at := func(d time.Duration) time.Time { return memoryNow.Add(-d) } head := map[string]string{ "kv.memory.alpha": "second version", "kv.memory.beta": "beta text", "kv.memory.gamma": "gamma text", "issue_prefix": "demo", // not a memory: the prefix filter drops it } older := map[string]string{ "kv.memory.alpha": "first version", "kv.memory.beta": "beta text", "kv.memory.gamma": "gamma text", "issue_prefix": "demo", } root := map[string]string{ "kv.memory.gamma": "gamma text", "issue_prefix": "demo", } return &fakeHistory{ commits: []browse.CommitInfo{ {Hash: "c4", Author: "bigbes", Date: at(3 * time.Hour)}, {Hash: "c3", Author: "bigbes", Date: at(2 * 24 * time.Hour)}, {Hash: "c2", Author: "bigbes", Date: at(71 * 24 * time.Hour)}, {Hash: "c1", Author: "alice", Date: at(99 * 24 * time.Hour)}, {Hash: "c0", Author: "alice", Date: at(200 * 24 * time.Hour)}, }, config: map[string]map[string]string{ "main": head, "c4": head, "c3": head, "c2": older, "c1": older, "c0": root, }, hashes: map[string]string{ "main": "h-c", "c4": "h-c", "c3": "h-c", "c2": "h-b", "c1": "h-b", "c0": "h-a", }, } } func buildMemories(t *testing.T, sess MemorySession, query string) *MemoryView { t.Helper() q, err := url.ParseQuery(query) require.NoError(t, err) v, err := BuildMemories(context.Background(), sess, "main", q, memoryNow) require.NoError(t, err) return v } func slugsOf(v *MemoryView) []string { out := make([]string, 0, len(v.Memories)) for _, m := range v.Memories { out = append(out, m.Slug) } return out } // --- the projection ---------------------------------------------------------- // Only the kv.memory.* rows are memories, and the slug is the key without that // prefix. The rest of config is the tracker's settings and must not appear. func TestMemoriesPrefixFiltering(t *testing.T) { v := buildMemories(t, memoryHistory(), "") assert.Equal(t, []string{"alpha", "beta", "gamma"}, slugsOf(v)) assert.Equal(t, 3, v.Total, "issue_prefix is not a memory") assert.Equal(t, MemorySortSlug, v.Sort) assert.False(t, v.WalkTruncated) } // A value carries both newline spellings — real ones when it was written from a // file, literal backslash-n when it was typed into a shell string — and both // have to become the same text. It is the one shaping this projection does: the // markdown itself is the renderer's to read, and it cannot read a value whose // line breaks are still two characters. func TestMemoriesNormaliseBothNewlineSpellings(t *testing.T) { const escaped = `First paragraph.\n\nSecond paragraph, line one.\nLine two.` real := "First paragraph.\n\nSecond paragraph, line one.\nLine two." crlf := "First paragraph.\r\n\r\nSecond paragraph, line one.\r\nLine two." sess := memoryHistory() for _, ref := range []string{"main", "c4", "c3"} { sess.config[ref] = map[string]string{ "kv.memory.escaped": escaped, "kv.memory.real": real, "kv.memory.crlf": crlf, } } v := buildMemories(t, sess, "") require.Len(t, v.Memories, 3) for _, m := range v.Memories { assert.Equal(t, "First paragraph.\n\nSecond paragraph, line one.\nLine two.", m.Text, "memory %q", m.Slug) assert.NotContains(t, m.Text, `\n`, "the literal escape must not survive into the text") } } // ?q= is a substring of the slug or of the text, case-insensitively. func TestMemoriesSearchFilter(t *testing.T) { bySlug := buildMemories(t, memoryHistory(), "q=ALPH") assert.Equal(t, []string{"alpha"}, slugsOf(bySlug)) assert.Equal(t, 3, bySlug.Total, "Total counts the tracker, not the page") assert.Equal(t, "ALPH", bySlug.Search) byText := buildMemories(t, memoryHistory(), "q=second+version") assert.Equal(t, []string{"alpha"}, slugsOf(byText)) none := buildMemories(t, memoryHistory(), "q=nothing+matches+this") assert.Empty(t, none.Memories) assert.Equal(t, 3, none.Total) } // ?key= renders one memory; an unknown slug renders none rather than everything. func TestMemoriesSingleKey(t *testing.T) { one := buildMemories(t, memoryHistory(), "key=beta") require.Len(t, one.Memories, 1) assert.Equal(t, "beta", one.Memories[0].Slug) assert.Equal(t, "beta", one.Key) missing := buildMemories(t, memoryHistory(), "key=nosuch") assert.Empty(t, missing.Memories) assert.Equal(t, 3, missing.Total) } // Slug order is the default; age order is oldest first, which is the review // queue. An unknown ?sort= value falls back to slug rather than to nothing. func TestMemoriesSortOrders(t *testing.T) { assert.Equal(t, []string{"alpha", "beta", "gamma"}, slugsOf(buildMemories(t, memoryHistory(), ""))) assert.Equal(t, []string{"alpha", "beta", "gamma"}, slugsOf(buildMemories(t, memoryHistory(), "sort=slug"))) assert.Equal(t, []string{"alpha", "beta", "gamma"}, slugsOf(buildMemories(t, memoryHistory(), "sort=sideways"))) // gamma (root, 200 days) → beta (c1, 99 days) → alpha (c3, 2 days). byAge := buildMemories(t, memoryHistory(), "sort=age") assert.Equal(t, []string{"gamma", "beta", "alpha"}, slugsOf(byAge)) assert.Equal(t, MemorySortAge, byAge.Sort) } // A tracker with no memories at all — and one with no config table — is an empty // view, not an error: the tab exists wherever the beads fingerprint does. func TestMemoriesEmptyTracker(t *testing.T) { noMemories := memoryHistory() for ref := range noMemories.config { noMemories.config[ref] = map[string]string{"issue_prefix": "demo"} } v := buildMemories(t, noMemories, "") assert.Empty(t, v.Memories) assert.Equal(t, 0, v.Total) assert.Empty(t, noMemories.reads[1:], "with nothing to date, the walk must not run") noTable := memoryHistory() noTable.config = map[string]map[string]string{} empty := buildMemories(t, noTable, "") assert.Empty(t, empty.Memories) assert.Equal(t, 0, empty.Total) } // AppliesMemories is the beads fingerprint plus a config table carrying key and // value. It sees shapes and never rows, so an empty config still gets the tab. func TestAppliesMemories(t *testing.T) { configTable := browse.TableInfo{Name: "config", Columns: []browse.ColumnInfo{ {Name: "key", PrimaryKey: true}, {Name: "value"}, }} full := append(beadsTables(), configTable) assert.True(t, AppliesMemories(full)) assert.False(t, AppliesMemories(beadsTables()), "no config table") assert.False(t, AppliesMemories([]browse.TableInfo{configTable}), "config without the beads fingerprint") assert.False(t, AppliesMemories(append(beadsTables(), browse.TableInfo{Name: "config", Columns: []browse.ColumnInfo{{Name: "key"}}})), "a config table without a value column is somebody else's config") } // --- the revision walk ------------------------------------------------------- // The walk attributes a memory to the commit that changed its value, skips the // commits that did not touch config without reading a row, and — when the // history itself runs out — attributes what never changed to the root commit. func TestMemoryRevisionWalk(t *testing.T) { sess := memoryHistory() v := buildMemories(t, sess, "") require.Len(t, v.Memories, 3) byslug := map[string]Memory{} for _, m := range v.Memories { byslug[m.Slug] = m } // alpha changed between c2 and c3, so c3 wrote it — not c4, which merely has // the same value, and not c2, which has the older one. alpha := byslug["alpha"] require.NotNil(t, alpha.Revision) assert.Equal(t, "c3", alpha.Revision.Commit) assert.Equal(t, memoryNow.Add(-2*24*time.Hour), alpha.Revision.Date) assert.Equal(t, "bigbes", alpha.Revision.Author) assert.False(t, alpha.Stale, "two days old") // beta appeared at c1 (absent at the root) and was left alone since. beta := byslug["beta"] require.NotNil(t, beta.Revision) assert.Equal(t, "c1", beta.Revision.Commit) assert.Equal(t, "alice", beta.Revision.Author) assert.True(t, beta.Stale, "99 days old") // gamma never changed anywhere in the history: the root is what wrote it. gamma := byslug["gamma"] require.NotNil(t, gamma.Revision) assert.Equal(t, "c0", gamma.Revision.Commit) assert.True(t, gamma.Stale, "200 days old") assert.False(t, v.WalkTruncated, "the whole history fits inside the walk") // The cost claim: rows were read at the head and at the two commits whose // config hash differs from their newer neighbour's. c3 and c1 are byte-equal // to the commit above them and were skipped without a read; c4 is the head // itself, already read once. assert.Equal(t, []string{"main", "c2", "c0"}, sess.reads) assert.Equal(t, 5, sess.hashCalls, "one O(1) table hash per commit") assert.Equal(t, 1, sess.logCalls, "one log call for the whole walk") } // A key that never changes inside the walk's budget gets no date at all rather // than a date the walk cannot support — and the view says the walk was cut off, // which is the only way a memory can carry no revision. func TestMemoryRevisionUnresolvedWithinWalk(t *testing.T) { sess := &fakeHistory{ config: map[string]map[string]string{"main": {"kv.memory.ancient": "unchanged"}}, hashes: map[string]string{"main": "h"}, } // One commit more than the walk examines, none of which touched config. for i := 0; i <= memoryWalkMax; i++ { h := fmt.Sprintf("k%03d", i) sess.commits = append(sess.commits, browse.CommitInfo{ Hash: h, Author: "bigbes", Date: memoryNow.Add(-time.Duration(i) * time.Hour), }) sess.config[h] = map[string]string{"kv.memory.ancient": "unchanged"} sess.hashes[h] = "h" } v := buildMemories(t, sess, "") require.Len(t, v.Memories, 1) assert.Nil(t, v.Memories[0].Revision, "no date the walk cannot support") assert.True(t, v.WalkTruncated) assert.Equal(t, memoryWalkMax, v.WalkMax) // Still older than the threshold? The walk's own oldest commit is 500 hours // back, which is short of 60 days, so the question is not asked either. assert.False(t, v.Memories[0].Stale) assert.Equal(t, []string{"main"}, sess.reads, "500 commits that did not touch config must cost no row read at all") assert.Equal(t, memoryWalkMax, sess.hashCalls) } // The undated arm of staleness: with no revision, the walk's oldest commit is a // floor on the memory's age, and a floor already past the threshold supports the // question. func TestMemoryStaleWithoutARevision(t *testing.T) { sess := &fakeHistory{ config: map[string]map[string]string{"main": {"kv.memory.ancient": "unchanged"}}, hashes: map[string]string{"main": "h"}, } for i := 0; i <= memoryWalkMax; i++ { h := fmt.Sprintf("k%03d", i) sess.commits = append(sess.commits, browse.CommitInfo{ Hash: h, Date: memoryNow.Add(-time.Duration(i) * 24 * time.Hour), }) sess.config[h] = map[string]string{"kv.memory.ancient": "unchanged"} sess.hashes[h] = "h" } v := buildMemories(t, sess, "") require.Len(t, v.Memories, 1) assert.Nil(t, v.Memories[0].Revision) assert.True(t, v.Memories[0].Stale, "the oldest commit walked is 499 days back") } // A commit that created the config table is attributed correctly: walking past // it, the table is simply absent, and absent is an ordinary answer rather than // an error. func TestMemoryWalkPastTheTablesCreation(t *testing.T) { sess := &fakeHistory{ commits: []browse.CommitInfo{ {Hash: "b2", Author: "bigbes", Date: memoryNow.Add(-time.Hour)}, {Hash: "b1", Author: "bigbes", Date: memoryNow.Add(-48 * time.Hour)}, {Hash: "b0", Author: "bigbes", Date: memoryNow.Add(-72 * time.Hour)}, }, config: map[string]map[string]string{ "main": {"kv.memory.first": "hello"}, "b2": {"kv.memory.first": "hello"}, "b1": {"kv.memory.first": "hello"}, // b0 predates the config table entirely: no entry at all. }, hashes: map[string]string{"main": "h1", "b2": "h1", "b1": "h1"}, } v := buildMemories(t, sess, "") require.Len(t, v.Memories, 1) require.NotNil(t, v.Memories[0].Revision) assert.Equal(t, "b1", v.Memories[0].Revision.Commit, "the commit that created config is the one that wrote the memory") assert.False(t, v.WalkTruncated) } // A history that cannot be read is an error, not a page of memories with every // date quietly missing — that page is indistinguishable from a tracker whose // memories are all older than the walk. func TestMemoriesFailWhenTheHistoryCannotBeRead(t *testing.T) { sess := &failingLog{fakeHistory: memoryHistory()} _, err := BuildMemories(context.Background(), sess, "main", url.Values{}, memoryNow) require.Error(t, err) assert.Contains(t, err.Error(), "read history") } type failingLog struct{ *fakeHistory } func (*failingLog) Log(context.Context, string, string, int) ([]browse.CommitInfo, string, error) { return nil, "", fmt.Errorf("browse: walk commits: corrupt chunk") }