package beads import ( "errors" "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sourcecraft.dev/bigbes/sr-ht-dolt/browse" ) // --- fixtures ---------------------------------------------------------------- // prefixTracker is a beads database whose config names an issue prefix, with the // settings a real tracker carries beside it — the projection has to pick one row // out of the table rather than read the first one. func prefixTracker(head, prefix string) *fakeReadyDB { return &fakeReadyDB{ branches: []browse.Branch{{Name: "main", Head: head}}, tables: beadsTables(), rows: map[string]*browse.RowPage{ "config": { Columns: []string{"key", "value"}, Rows: [][]string{ {"compact_tier2_days", "30"}, {"kv.memory.handoff", "not a prefix"}, {"issue_prefix", prefix}, }, Total: 3, }, }, } } // prefixFixture is two trackers, "global" and "artifacts", as the instance has // them: one cross-project tracker and one per-project. func prefixFixture() (*readyInstance, []ReadyDatabase) { in := &readyInstance{dbs: map[int]*fakeReadyDB{ 1: prefixTracker("h-global", "global"), 2: prefixTracker("h-artifacts", "artifacts"), }} return in, []ReadyDatabase{ {ID: 1, OwnerName: "bigbes", Name: "beads-global"}, {ID: 2, OwnerName: "bigbes", Name: "sourcehut-artifacts"}, } } // --- tests ------------------------------------------------------------------- // The index: one prefix per database, read out of config, and nothing claimed // for a prefix no database named. func TestPrefixesAcrossIndexesTheDatabasesItIsHanded(t *testing.T) { in, dbs := prefixFixture() index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow) assert.Equal(t, []string{"artifacts", "global"}, index.Prefixes()) d, ok := index.Lookup("global") require.True(t, ok) assert.Equal(t, "bigbes/beads-global", d.Slug()) d, ok = index.Lookup("artifacts") require.True(t, ok) assert.Equal(t, "bigbes/sourcehut-artifacts", d.Slug()) _, ok = index.Lookup("nosuch") assert.False(t, ok, "a prefix no database named is not in the index") assert.Empty(t, index.Failed) // Every session opened is closed again: this is the read pattern the // per-request browse discipline is bounded against. for id, f := range in.dbs { assert.Equal(t, f.opens, f.closes, "database %d: %d opens, %d closes", id, f.opens, f.closes) } } // The head-hash gate: a second build with unmoved heads reads no rows. Asserted // by counting reads on the fake, never by timing. func TestPrefixesAcrossHeadHashGateSkipsTheRead(t *testing.T) { in, dbs := prefixFixture() cache := &PrefixCache{} first := PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow) require.Len(t, first.Prefixes(), 2) global := in.dbs[1] require.Greater(t, global.rowReads, 0, "the first build must read config") reads := global.rowReads second := PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow.Add(30*time.Second)) assert.Equal(t, reads, global.rowReads, "a second build with an unmoved head must read no rows") assert.Equal(t, first.Prefixes(), second.Prefixes(), "and it is the same answer") // The session is still opened and its branches listed — that is what the gate // is gated on, and it is the cheap half. assert.Equal(t, 2, global.opens) assert.Equal(t, 2, global.closes) } // A head that moved is a prefix that may have been renamed: read again, even // well inside the TTL. func TestPrefixesAcrossMovedHeadForcesAReread(t *testing.T) { in, dbs := prefixFixture() cache := &PrefixCache{} global := in.dbs[1] PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow) reads := global.rowReads global.branches = []browse.Branch{{Name: "main", Head: "h-global-2"}} PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow.Add(time.Second)) assert.Greater(t, global.rowReads, reads, "a moved head must be re-read") assert.Equal(t, 1, in.dbs[2].rowReads, "the sibling's head did not move") } // The TTL is the same one /ready is bounded by, because it is the same cache. func TestPrefixesAcrossTTLExpiryForcesAReread(t *testing.T) { in, dbs := prefixFixture() cache := &PrefixCache{} global := in.dbs[1] PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow) reads := global.rowReads PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow.Add(ReadyCacheTTL-time.Nanosecond)) assert.Equal(t, reads, global.rowReads, "inside the TTL the projection stands") PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow.Add(ReadyCacheTTL)) assert.Greater(t, global.rowReads, reads, "at the TTL the projection is re-read") } // The ceiling: at most ReadyMaxDatabases stores opened per build, and the first // database listed — the caller puts the page's own database there — is inside // it. func TestPrefixesAcrossCeiling(t *testing.T) { in := &readyInstance{dbs: map[int]*fakeReadyDB{}} var dbs []ReadyDatabase for i := 1; i <= ReadyMaxDatabases+3; i++ { in.dbs[i] = prefixTracker(fmt.Sprintf("h%d", i), fmt.Sprintf("p%02d", i)) dbs = append(dbs, ReadyDatabase{ID: i, OwnerName: "alice", Name: fmt.Sprintf("db%02d", i)}) } index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow) assert.Len(t, index.Prefixes(), ReadyMaxDatabases) _, ok := index.Lookup("p01") assert.True(t, ok, "the first database listed is never dropped by the ceiling") for i := ReadyMaxDatabases + 1; i <= ReadyMaxDatabases+3; i++ { assert.Zero(t, in.dbs[i].opens, "database %d is past the ceiling", i) } } // Two databases claiming one prefix: neither is linked. Linking to one of two // candidates would be a guess rendered as a fact, and the id reads as text // either way. func TestPrefixesAcrossDropsAnAmbiguousPrefix(t *testing.T) { in := &readyInstance{dbs: map[int]*fakeReadyDB{ 1: prefixTracker("h1", "shared"), 2: prefixTracker("h2", "shared"), 3: prefixTracker("h3", "mine"), }} dbs := []ReadyDatabase{ {ID: 1, OwnerName: "alice", Name: "one"}, {ID: 2, OwnerName: "bob", Name: "two"}, {ID: 3, OwnerName: "carol", Name: "three"}, } index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow) _, ok := index.Lookup("shared") assert.False(t, ok, "a prefix two databases claim belongs to neither") assert.Equal(t, []string{"mine"}, index.Prefixes(), "the unambiguous ones are unaffected") } // A database that names no prefix — no config table at all, or a config without // the row — is simply not in the index. func TestPrefixesAcrossSkipsADatabaseWithNoPrefix(t *testing.T) { noConfig := &fakeReadyDB{branches: []browse.Branch{{Name: "main", Head: "h1"}}, tables: beadsTables()} noRow := &fakeReadyDB{ branches: []browse.Branch{{Name: "main", Head: "h2"}}, tables: beadsTables(), rows: map[string]*browse.RowPage{ "config": {Columns: []string{"key", "value"}, Rows: [][]string{{"compact_tier2_days", "30"}}, Total: 1}, }, } empty := &fakeReadyDB{} // a store with no branches: never pushed to in := &readyInstance{dbs: map[int]*fakeReadyDB{1: noConfig, 2: noRow, 3: empty}} dbs := []ReadyDatabase{ {ID: 1, OwnerName: "alice", Name: "plain"}, {ID: 2, OwnerName: "alice", Name: "settings-only"}, {ID: 3, OwnerName: "alice", Name: "fresh"}, } index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow) assert.Empty(t, index.Prefixes()) assert.Empty(t, index.Failed, "naming no prefix is not a failure") assert.Zero(t, empty.rowReads, "a store with no branches is not read") } // A database that cannot be opened, or whose config cannot be read, costs the // ids it owns their link and nothing else. func TestPrefixesAcrossFailingDatabaseCostsItselfOnly(t *testing.T) { in, dbs := prefixFixture() in.dbs[1].openErr = errors.New("browse: open store: no such file or directory") index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow) require.Len(t, index.Failed, 1) assert.Equal(t, "bigbes/beads-global", index.Failed[0].Database.Slug()) assert.ErrorContains(t, index.Failed[0].Err, "no such file") assert.Equal(t, []string{"artifacts"}, index.Prefixes()) // A store that opens but cannot list its branches fails the same way, and // still closes its session. in2, dbs2 := prefixFixture() in2.dbs[1].branchesErr = errors.New("browse: list branches: corrupt chunk") index2 := PrefixesAcross(t.Context(), dbs2, in2.open, &PrefixCache{}, readyNow) require.Len(t, index2.Failed, 1) assert.Equal(t, []string{"artifacts"}, index2.Prefixes()) assert.Equal(t, 1, in2.dbs[1].closes) } // The pattern: what counts as an id and what does not. The suffix carries no // hyphen, which is what makes the split unambiguous for a hyphenated prefix. func TestPrefixIndexScan(t *testing.T) { index := &PrefixIndex{byPrefix: map[string]ReadyDatabase{ "global": {ID: 1, OwnerName: "bigbes", Name: "beads-global"}, "artifacts": {ID: 2, OwnerName: "bigbes", Name: "sourcehut-artifacts"}, "sr-ht-dolt": {ID: 3, OwnerName: "bigbes", Name: "sourcehut-dolt"}, }} for _, tc := range []struct { name string text string want []string }{ {"a plain id", "blocked by artifacts-nex", []string{"artifacts-nex"}}, {"the subtask form", "see artifacts-46c.2 for the bucket", []string{"artifacts-46c.2"}}, {"a hyphenated prefix", "sr-ht-dolt-44n.6 lands first", []string{"sr-ht-dolt-44n.6"}}, {"several in one line", "global-b08 needs artifacts-46c.2", []string{"global-b08", "artifacts-46c.2"}}, {"an unknown prefix", "nosuch-46c and other-1 are not ids", nil}, {"a hyphenated word", "the read-only surface", nil}, {"an id at the end of a sentence", "closed by artifacts-46c.", []string{"artifacts-46c"}}, {"an id in brackets", "(artifacts-46c) and [global-b08]", []string{"artifacts-46c", "global-b08"}}, {"an id inside a longer word", "xxartifacts-46c and artifactsx-1", nil}, {"uppercase is not an id", "ARTIFACTS-46C", nil}, {"a prefix on its own", "the artifacts tracker", nil}, {"a shorter known prefix inside a longer token", "some-global-b08", nil}, {"empty text", "", nil}, } { t.Run(tc.name, func(t *testing.T) { var got []string for _, ref := range index.Scan(tc.text) { got = append(got, ref.ID) assert.Equal(t, ref.ID, tc.text[ref.Start:ref.End], "the offsets must name the id they carry") } assert.Equal(t, tc.want, got) }) } // Each reference names the database that owns it. refs := index.Scan("global-b08 blocks artifacts-46c.2") require.Len(t, refs, 2) assert.Equal(t, "bigbes/beads-global", refs[0].Database.Slug()) assert.Equal(t, "bigbes/sourcehut-artifacts", refs[1].Database.Slug()) } // An index that was never built knows no prefix — the same answer an unknown one // gets, which is what keeps "the caller may not see that database" and "there is // no such database" indistinguishable. func TestNilPrefixIndexKnowsNothing(t *testing.T) { var index *PrefixIndex assert.Nil(t, index.Scan("artifacts-46c.2")) assert.Nil(t, index.Prefixes()) _, ok := index.Lookup("artifacts") assert.False(t, ok) // So does an empty one. empty := PrefixesAcross(t.Context(), nil, nil, &PrefixCache{}, readyNow) assert.Nil(t, empty.Scan("artifacts-46c.2")) }