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"))
}
// --- the memory half of the same read ------------------------------------------
// memoryTracker is a tracker whose config holds memories beside its settings.
// The prefix is a parameter because a tracker that names none still holds
// memories, and the two halves of the projection are independent.
func memoryTracker(head, prefix string, slugs ...string) *fakeReadyDB {
page := &browse.RowPage{
Columns: []string{"key", "value"},
Rows: [][]string{{"compact_tier2_days", "30"}},
}
if prefix != "" {
page.Rows = append(page.Rows, []string{"issue_prefix", prefix})
}
for _, s := range slugs {
page.Rows = append(page.Rows, []string{memoryPrefix + s, "the body of " + s})
}
page.Total = len(page.Rows)
return &fakeReadyDB{
branches: []browse.Branch{{Name: "main", Head: head}},
tables: beadsTables(),
rows: map[string]*browse.RowPage{"config": page},
}
}
// The slugs come out of the same read the prefix does, and a slug no database
// holds is unknown — which is also the answer for one held by a database this
// caller may not browse, since such a database is never handed to the index.
func TestPrefixesAcrossIndexesMemorySlugs(t *testing.T) {
in := &readyInstance{dbs: map[int]*fakeReadyDB{
1: memoryTracker("h-global", "global", "go-vcs-stamp-dirty", "srht-service-release"),
2: memoryTracker("h-artifacts", "artifacts", "apk-index-cadence"),
}}
dbs := []ReadyDatabase{
{ID: 1, OwnerName: "bigbes", Name: "beads-global"},
{ID: 2, OwnerName: "bigbes", Name: "sourcehut-artifacts"},
}
index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow)
assert.Equal(t,
[]string{"apk-index-cadence", "go-vcs-stamp-dirty", "srht-service-release"},
index.MemorySlugs())
d, ok := index.LookupMemory("go-vcs-stamp-dirty")
require.True(t, ok)
assert.Equal(t, "bigbes/beads-global", d.Slug())
d, ok = index.LookupMemory("apk-index-cadence")
require.True(t, ok)
assert.Equal(t, "bigbes/sourcehut-artifacts", d.Slug())
_, ok = index.LookupMemory("never-written")
assert.False(t, ok)
// One read per database answered both questions.
assert.Equal(t, 1, in.dbs[1].rowReads)
assert.Equal(t, []string{"artifacts", "global"}, index.Prefixes())
}
// A slug two trackers hold resolves to the first one listed, unlike a prefix two
// trackers claim, which resolves to neither. The difference is what the
// duplicate means: two trackers claiming one prefix own two different id spaces,
// while the same memory slug in two trackers is normally one memory that was
// re-filed and left a copy behind. The caller lists the page's own database
// first, so this is "the tracker you are reading wins".
func TestPrefixesAcrossResolvesADuplicateSlugToTheFirstDatabase(t *testing.T) {
in := &readyInstance{dbs: map[int]*fakeReadyDB{
1: memoryTracker("h1", "here", "shared-note"),
2: memoryTracker("h2", "there", "shared-note"),
}}
dbs := []ReadyDatabase{
{ID: 1, OwnerName: "alice", Name: "own"},
{ID: 2, OwnerName: "bob", Name: "other"},
}
index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow)
d, ok := index.LookupMemory("shared-note")
require.True(t, ok, "a slug two trackers hold is still a link")
assert.Equal(t, "alice/own", d.Slug())
}
// A tracker that names no issue prefix still contributes its memories: the two
// halves of the projection do not gate each other.
func TestPrefixesAcrossIndexesMemoriesOfAPrefixlessDatabase(t *testing.T) {
in := &readyInstance{dbs: map[int]*fakeReadyDB{
1: memoryTracker("h1", "", "only-a-memory"),
}}
dbs := []ReadyDatabase{{ID: 1, OwnerName: "alice", Name: "no-prefix"}}
index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow)
assert.Empty(t, index.Prefixes())
d, ok := index.LookupMemory("only-a-memory")
require.True(t, ok)
assert.Equal(t, "alice/no-prefix", d.Slug())
}
// The cached projection carries the slugs, so a second build behind the
// head-hash gate answers the memory question without reading a row either.
func TestPrefixesAcrossCachesTheSlugs(t *testing.T) {
in := &readyInstance{dbs: map[int]*fakeReadyDB{
1: memoryTracker("h1", "here", "cached-slug"),
}}
dbs := []ReadyDatabase{{ID: 1, OwnerName: "alice", Name: "own"}}
cache := &PrefixCache{}
PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow)
reads := in.dbs[1].rowReads
require.Greater(t, reads, 0)
second := PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow.Add(time.Second))
assert.Equal(t, reads, in.dbs[1].rowReads, "the gate holds for the slugs too")
_, ok := second.LookupMemory("cached-slug")
assert.True(t, ok)
}
// An index that was never built holds no memory either.
func TestNilPrefixIndexKnowsNoMemory(t *testing.T) {
var index *PrefixIndex
assert.Nil(t, index.MemorySlugs())
_, ok := index.LookupMemory("anything")
assert.False(t, ok)
}