package search
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
"sourcecraft.dev/bigbes/sr-ht-spec/doc"
)
// Ported from warren: an empty or whitespace-only query returns nothing rather
// than everything.
func TestSearchIgnoresWhitespaceOnlyQuery(t *testing.T) {
idx := indexCorpus(t, newCorpus(t, "~bigbes/specs", "rev1").
add("specs/storage.md", "---\ntitle: Needle handbook\n---\n\nreference material\n"))
for _, q := range []string{"", " \t\n "} {
res, err := idx.Search(context.Background(), Query{Text: q, Spaces: core.EverythingFilter()})
require.NoError(t, err)
require.Empty(t, res.Hits)
require.Zero(t, res.Total)
}
}
// Ported from warren's keyword fixture: a title match outranks a body match, so
// a query that names a document returns the document.
func TestTitleMatchOutranksBodyMatch(t *testing.T) {
idx := indexCorpus(t, newCorpus(t, "~bigbes/specs", "rev1").
add("specs/handbook.md", "---\nid: SPEC-0001\ntitle: Needle handbook\n---\n\nreference material\n").
add("specs/other.md", "---\nid: SPEC-0002\ntitle: Other document\n---\n\na needle appears in the body\n"))
require.Equal(t, []string{"SPEC-0001", "SPEC-0002"}, hitIDs(t, idx, Query{Text: "needle", Spaces: core.EverythingFilter()}))
}
// A project is a saved filter over the one global index. This is the whole of
// it: the same index, queried with a space set.
func TestProjectIsASpaceFilterOverOneIndex(t *testing.T) {
rfcs := newCorpus(t, "~bigbes/rfcs", "rev1").
add("specs/storage.md", "---\nid: SPEC-0001\ntitle: Storage model\n---\n\nA shared vocabulary term.\n")
ops := newCorpus(t, "~bigbes/home-ops", "rev1").
add("notes/hosts.md", "---\nid: NOTE-0001\ntitle: Hosts\n---\n\nAnother shared vocabulary term.\n")
other := newCorpus(t, "~someone/private", "rev1").
add("specs/x.md", "---\nid: SPEC-0009\ntitle: Elsewhere\n---\n\nA third shared vocabulary term.\n")
idx := indexCorpus(t, rfcs, ops, other)
// The meta-project: a filter that excludes nothing.
all := hitIDs(t, idx, Query{Text: "vocabulary", Spaces: core.EverythingFilter()})
require.ElementsMatch(t, []string{"SPEC-0001", "NOTE-0001", "SPEC-0009"}, all)
// A project over two of the three spaces.
project := hitIDs(t, idx, Query{Text: "vocabulary", Spaces: core.SpacesFilter([]core.SpaceRef{rfcs.Space, ops.Space}, nil)})
require.ElementsMatch(t, []string{"SPEC-0001", "NOTE-0001"}, project)
// One space.
require.Equal(t, []string{"SPEC-0009"},
hitIDs(t, idx, Query{Text: "vocabulary", Spaces: core.SpacesFilter([]core.SpaceRef{other.Space}, nil)}))
}
// Space names are matched whole. Filtering through an analyzed field — which is
// what warren did for sections — would tokenize "~bigbes/home-ops" and let a
// query for one space return another.
func TestSpaceFilterMatchesWholeNamesOnly(t *testing.T) {
ops := newCorpus(t, "~bigbes/home-ops", "rev1").
add("notes/a.md", "---\nid: NOTE-0001\ntitle: A\n---\n\nshared vocabulary\n")
home := newCorpus(t, "~bigbes/home", "rev1").
add("notes/b.md", "---\nid: NOTE-0002\ntitle: B\n---\n\nshared vocabulary\n")
idx := indexCorpus(t, ops, home)
require.Equal(t, []string{"NOTE-0002"},
hitIDs(t, idx, Query{Text: "vocabulary", Spaces: core.SpacesFilter([]core.SpaceRef{home.Space}, nil)}))
}
// The whole reason Query.Spaces is a filter and not a []core.SpaceRef: an
// empty project selects nothing, and "nothing" must never widen to "the entire
// corpus" on the way into a query. With a slice it did — no terms read as no
// restriction — and every test written with a non-empty project passed anyway.
func TestEmptyProjectFindsNothingRatherThanEverything(t *testing.T) {
rfcs := newCorpus(t, "~bigbes/rfcs", "rev1").
add("specs/storage.md", "---\nid: SPEC-0001\ntitle: Storage model\n---\n\nA shared vocabulary term.\n")
ops := newCorpus(t, "~bigbes/home-ops", "rev1").
add("notes/hosts.md", "---\nid: NOTE-0001\ntitle: Hosts\n---\n\nAnother shared vocabulary term.\n")
idx := indexCorpus(t, rfcs, ops)
// What service.ResolveProject returns for a project nobody has added a
// space to yet.
empty := core.SpacesFilter(nil, nil)
require.True(t, empty.MatchesNothing())
require.False(t, empty.Everything())
res, err := idx.Search(context.Background(), Query{Text: "vocabulary", Spaces: empty})
require.NoError(t, err)
require.Empty(t, res.Hits, "an empty project must return no hits, not the corpus")
require.Zero(t, res.Total)
// The same query over the meta-project, to show the corpus was there to be
// returned and the filter is what withheld it.
require.Len(t, hitIDs(t, idx, Query{Text: "vocabulary", Spaces: core.EverythingFilter()}), 2)
}
// A scope nobody set is neither answer, and is refused rather than defaulted:
// both plausible defaults are wrong for one of the two callers that could
// produce it.
func TestSearchRefusesAQueryWithNoScope(t *testing.T) {
idx := indexCorpus(t, newCorpus(t, "~bigbes/specs", "rev1").
add("a.md", "---\ntitle: A\n---\n\nbody text here\n"))
_, err := idx.Search(context.Background(), Query{Text: "body"})
require.ErrorContains(t, err, "no space scope")
}
func TestSearchRejectsAnEmptySpaceFilter(t *testing.T) {
idx := indexCorpus(t, newCorpus(t, "~bigbes/specs", "rev1").
add("a.md", "---\ntitle: A\n---\n\nbody text here\n"))
_, err := idx.Search(context.Background(), Query{Text: "body", Spaces: core.SpacesFilter([]core.SpaceRef{{}}, nil)})
require.ErrorContains(t, err, "empty space")
}
// Ported from warren: log entries summarise other documents, so they are out of
// an unrestricted search and reachable by naming the section.
func TestLogEntriesAreExcludedUntilAskedFor(t *testing.T) {
idx := indexCorpus(t, newCorpus(t, "~bigbes/specs", "rev1").
add("specs/storage.md", "---\nid: SPEC-0001\ntitle: Storage model\n---\n\nThe write path resolves a tree.\n").
add("log.md", "# Log\n\n## [2026-05-31] update | Storage model\nRewrote the write path section.\n"))
require.Equal(t, []string{"SPEC-0001"}, hitIDs(t, idx, Query{Text: "write path", Spaces: core.EverythingFilter()}))
// Naming the section is the way back in. The log document itself carries no
// body, so only its entry matches the text.
require.Equal(t, []string{"log#2026-05-31-1"},
hitIDs(t, idx, Query{Text: "write path", Sections: []string{doc.LogSection}, Spaces: core.EverythingFilter()}))
require.Equal(t, []string{"log"},
hitIDs(t, idx, Query{Text: "Log", Sections: []string{doc.LogSection}, Spaces: core.EverythingFilter()}),
"the log document stays findable by name")
}
func TestSectionFilterRestrictsResults(t *testing.T) {
idx := indexCorpus(t, newCorpus(t, "~bigbes/specs", "rev1").
add("specs/storage.md", "---\nid: SPEC-0001\ntitle: Storage\n---\n\nshared vocabulary term\n").
add("notes/scratch.md", "---\nid: NOTE-0001\ntitle: Scratch\n---\n\nshared vocabulary term\n"))
require.Equal(t, []string{"SPEC-0001"}, hitIDs(t, idx, Query{Text: "vocabulary", Sections: []string{"specs"}, Spaces: core.EverythingFilter()}))
require.Equal(t, []string{"NOTE-0001"}, hitIDs(t, idx, Query{Text: "vocabulary", Sections: []string{"notes"}, Spaces: core.EverythingFilter()}))
require.Len(t, hitIDs(t, idx, Query{Text: "vocabulary", Sections: []string{"specs", "notes"}, Spaces: core.EverythingFilter()}), 2)
}
func TestSearchRejectsAnEmptySection(t *testing.T) {
idx := indexCorpus(t, newCorpus(t, "~bigbes/specs", "rev1").
add("a.md", "---\ntitle: A\n---\n\nbody text here\n"))
_, err := idx.Search(context.Background(), Query{Text: "body", Sections: []string{""}, Spaces: core.EverythingFilter()})
require.ErrorContains(t, err, "empty section")
}
func TestHitCarriesAPinnedAddress(t *testing.T) {
idx := indexCorpus(t, newCorpus(t, "~bigbes/specs", "8f14e45fceea167a").
add("specs/storage.md", "---\nid: SPEC-0001\ntitle: Storage model\n---\n\nThe write path resolves a tree.\n"))
res, err := idx.Search(context.Background(), Query{Text: "resolves", Spaces: core.EverythingFilter()})
require.NoError(t, err)
require.Len(t, res.Hits, 1)
h := res.Hits[0]
require.Equal(t, space(t, "~bigbes/specs"), h.Space)
require.Equal(t, "SPEC-0001", h.ID)
require.Equal(t, "8f14e45fceea167a", h.Rev)
require.Equal(t, "specs/storage.md", h.Path)
require.Equal(t, "specs", h.Section)
require.Equal(t, "Storage model", h.Title)
require.Equal(t, LangEN, h.Lang)
require.Greater(t, h.Score, 0.0)
require.Contains(t, h.Snippet, "<mark>resolves</mark>")
}
// Snippets are rendered as HTML by the review UI, so the text around the marks
// must be escaped. bleve's html formatter does it; this pins the behaviour.
func TestSnippetIsHTMLEscaped(t *testing.T) {
idx := indexCorpus(t, newCorpus(t, "~bigbes/specs", "rev1").
add("specs/x.md", "---\nid: SPEC-0001\ntitle: Escaping\n---\n\n"+
"A needle inside `<script>alert(1)</script>` and more prose after it.\n"))
res, err := idx.Search(context.Background(), Query{Text: "needle", Spaces: core.EverythingFilter()})
require.NoError(t, err)
require.Len(t, res.Hits, 1)
require.Contains(t, res.Hits[0].Snippet, "<script>")
require.NotContains(t, res.Hits[0].Snippet, "<script>")
}
// A rebuild is per space at a revision: documents that are gone at the new
// revision leave the index, and the other spaces are untouched.
func TestRebuildSpaceReplacesTheSpaceAndOnlyTheSpace(t *testing.T) {
rfcs := newCorpus(t, "~bigbes/rfcs", "rev1").
add("specs/keep.md", "---\nid: SPEC-0001\ntitle: Kept\n---\n\nshared vocabulary term\n").
add("specs/gone.md", "---\nid: SPEC-0002\ntitle: Removed\n---\n\nshared vocabulary term\n")
ops := newCorpus(t, "~bigbes/home-ops", "rev1").
add("notes/hosts.md", "---\nid: NOTE-0001\ntitle: Hosts\n---\n\nshared vocabulary term\n")
idx := indexCorpus(t, rfcs, ops)
require.ElementsMatch(t, []string{"SPEC-0001", "SPEC-0002", "NOTE-0001"},
hitIDs(t, idx, Query{Text: "vocabulary", Spaces: core.EverythingFilter()}))
next := newCorpus(t, "~bigbes/rfcs", "rev2").
add("specs/keep.md", "---\nid: SPEC-0001\ntitle: Kept\n---\n\nshared vocabulary term, reworded\n").
add("specs/new.md", "---\nid: SPEC-0003\ntitle: Added\n---\n\nshared vocabulary term\n")
st, err := idx.RebuildSpace(context.Background(), next.Space, next.extract(t))
require.NoError(t, err)
require.Equal(t, 2, st.Indexed)
require.Equal(t, 1, st.Deleted, "SPEC-0002 is gone at rev2")
require.Positive(t, st.Took)
require.ElementsMatch(t, []string{"SPEC-0001", "SPEC-0003", "NOTE-0001"},
hitIDs(t, idx, Query{Text: "vocabulary", Spaces: core.EverythingFilter()}))
res, err := idx.Search(context.Background(), Query{Text: "reworded", Spaces: core.EverythingFilter()})
require.NoError(t, err)
require.Len(t, res.Hits, 1)
require.Equal(t, "rev2", res.Hits[0].Rev, "a surviving document is re-indexed at the new revision")
}
func TestRebuildSpaceRejectsADocumentFromAnotherSpace(t *testing.T) {
idx := openIndex(t)
other := newCorpus(t, "~bigbes/rfcs", "rev1").
add("a.md", "---\ntitle: A\n---\n\nbody text here\n")
_, err := idx.RebuildSpace(context.Background(), space(t, "~bigbes/home-ops"), other.extract(t))
require.ErrorContains(t, err, "belongs to space ~bigbes/rfcs, not ~bigbes/home-ops")
}
func TestRebuildSpaceNeedsASpace(t *testing.T) {
_, err := openIndex(t).RebuildSpace(context.Background(), core.SpaceRef{}, nil)
require.ErrorContains(t, err, "needs a space")
}
func TestDeleteSpaceEmptiesIt(t *testing.T) {
rfcs := newCorpus(t, "~bigbes/rfcs", "rev1").
add("specs/a.md", "---\nid: SPEC-0001\ntitle: A\n---\n\nshared vocabulary term\n")
ops := newCorpus(t, "~bigbes/home-ops", "rev1").
add("notes/b.md", "---\nid: NOTE-0001\ntitle: B\n---\n\nshared vocabulary term\n")
idx := indexCorpus(t, rfcs, ops)
st, err := idx.DeleteSpace(context.Background(), rfcs.Space)
require.NoError(t, err)
require.Equal(t, 1, st.Deleted)
require.Equal(t, []string{"NOTE-0001"}, hitIDs(t, idx, Query{Text: "vocabulary", Spaces: core.EverythingFilter()}))
n, err := idx.Count()
require.NoError(t, err)
require.Equal(t, uint64(1), n)
}
// RebuildAll builds beside the live index and swaps, so the old index stays
// open and serving until the new one is complete.
func TestRebuildAllReplacesEverything(t *testing.T) {
stale := newCorpus(t, "~bigbes/rfcs", "rev1").
add("specs/a.md", "---\nid: SPEC-0001\ntitle: A\n---\n\nshared vocabulary term\n")
idx := indexCorpus(t, stale)
rfcs := newCorpus(t, "~bigbes/rfcs", "rev2").
add("specs/b.md", "---\nid: SPEC-0002\ntitle: B\n---\n\nshared vocabulary term\n")
ops := newCorpus(t, "~bigbes/home-ops", "rev1").
add("notes/c.md", "---\nid: NOTE-0001\ntitle: C\n---\n\nshared vocabulary term\n")
st, err := idx.RebuildAll(context.Background(), append(rfcs.extract(t), ops.extract(t)...))
require.NoError(t, err)
require.Equal(t, 2, st.Spaces)
require.Equal(t, 2, st.Indexed)
require.Positive(t, st.Took)
require.ElementsMatch(t, []string{"SPEC-0002", "NOTE-0001"}, hitIDs(t, idx, Query{Text: "vocabulary", Spaces: core.EverythingFilter()}))
// Neither working directory survives a successful rebuild.
for _, p := range tmpPaths(idx.Path()) {
_, err := os.Stat(p)
require.True(t, os.IsNotExist(err), "%s should be gone", filepath.Base(p))
}
}
func TestCleanStaleRemovesRebuildLeftovers(t *testing.T) {
base := filepath.Join(t.TempDir(), "spec.bleve")
for _, p := range tmpPaths(base) {
require.NoError(t, os.MkdirAll(p, 0o755))
}
require.NoError(t, CleanStale(base))
for _, p := range tmpPaths(base) {
_, err := os.Stat(p)
require.True(t, os.IsNotExist(err))
}
}
// The index survives a restart: it is a cache, but not one that has to be
// rebuilt on every boot.
func TestOpenReusesAnExistingIndex(t *testing.T) {
path := filepath.Join(t.TempDir(), "spec.bleve")
c := newCorpus(t, "~bigbes/rfcs", "rev1").
add("specs/a.md", "---\nid: SPEC-0001\ntitle: A\n---\n\nshared vocabulary term\n")
idx, err := Open(path)
require.NoError(t, err)
_, err = idx.RebuildSpace(context.Background(), c.Space, c.extract(t))
require.NoError(t, err)
require.NoError(t, idx.Close())
reopened, err := Open(path)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, reopened.Close()) })
require.Equal(t, []string{"SPEC-0001"}, hitIDs(t, reopened, Query{Text: "vocabulary", Spaces: core.EverythingFilter()}))
}
func TestOpenNeedsAPath(t *testing.T) {
_, err := Open("")
require.ErrorContains(t, err, "path is required")
}
func TestClosedIndexRefusesWork(t *testing.T) {
idx, err := Open(filepath.Join(t.TempDir(), "spec.bleve"))
require.NoError(t, err)
require.NoError(t, idx.Close())
require.NoError(t, idx.Close(), "closing twice is not an error")
_, err = idx.Search(context.Background(), Query{Text: "anything", Spaces: core.EverythingFilter()})
require.ErrorContains(t, err, "index is closed")
_, err = idx.Count()
require.ErrorContains(t, err, "index is closed")
_, err = idx.RebuildSpace(context.Background(), space(t, "~bigbes/rfcs"), nil)
require.ErrorContains(t, err, "index is closed")
}
func TestSearchPaginates(t *testing.T) {
c := newCorpus(t, "~bigbes/rfcs", "rev1")
for i := range 5 {
c.add(fmt.Sprintf("specs/%d.md", i),
fmt.Sprintf("---\nid: SPEC-000%d\ntitle: Document %d\n---\n\nshared vocabulary term\n", i, i))
}
idx := indexCorpus(t, c)
res, err := idx.Search(context.Background(), Query{Text: "vocabulary", Limit: 2, Spaces: core.EverythingFilter()})
require.NoError(t, err)
require.Len(t, res.Hits, 2)
require.Equal(t, uint64(5), res.Total, "Total counts matches, not returned hits")
page2, err := idx.Search(context.Background(), Query{Text: "vocabulary", Limit: 2, Offset: 2, Spaces: core.EverythingFilter()})
require.NoError(t, err)
require.Len(t, page2.Hits, 2)
require.NotEqual(t, res.Hits[0].ID, page2.Hits[0].ID)
_, err = idx.Search(context.Background(), Query{Text: "vocabulary", Offset: -1, Spaces: core.EverythingFilter()})
require.ErrorContains(t, err, "negative offset")
}
func TestRebuildHonoursCancellation(t *testing.T) {
c := newCorpus(t, "~bigbes/rfcs", "rev1").
add("specs/a.md", "---\nid: SPEC-0001\ntitle: A\n---\n\nbody text here\n")
idx := openIndex(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := idx.RebuildAll(ctx, c.extract(t))
require.ErrorIs(t, err, context.Canceled)
_, err = idx.RebuildSpace(ctx, c.Space, c.extract(t))
require.ErrorIs(t, err, context.Canceled)
}
func TestStatsString(t *testing.T) {
require.Equal(t, "spaces=2 indexed=10 deleted=1 took=0s", Stats{Spaces: 2, Indexed: 10, Deleted: 1}.String())
}
func TestKeyIsScopedToItsSpace(t *testing.T) {
a := space(t, "~bigbes/rfcs")
b := space(t, "~bigbes/home-ops")
require.Equal(t, "~bigbes/rfcs:specs/storage", Key(a, "specs/storage"))
require.NotEqual(t, Key(a, "specs/storage"), Key(b, "specs/storage"))
}
// Two spaces whose documents fall back to the same path-derived id are two
// documents in the global index, not one overwriting the other.
func TestSameIDInTwoSpacesStaysTwoDocuments(t *testing.T) {
a := newCorpus(t, "~bigbes/rfcs", "rev1").
add("specs/storage.md", "# Storage\n\nshared vocabulary term\n")
b := newCorpus(t, "~bigbes/home-ops", "rev1").
add("specs/storage.md", "# Storage\n\nshared vocabulary term\n")
idx := indexCorpus(t, a, b)
res, err := idx.Search(context.Background(), Query{Text: "vocabulary", Spaces: core.EverythingFilter()})
require.NoError(t, err)
require.Len(t, res.Hits, 2)
require.ElementsMatch(t,
[]core.SpaceRef{a.Space, b.Space},
[]core.SpaceRef{res.Hits[0].Space, res.Hits[1].Space})
for _, h := range res.Hits {
require.Equal(t, "specs/storage", h.ID)
}
}
// Rebuild timing on a corpus far larger than the confirmed volume. The design
// absorbs the batch rebuild on the argument that at tens of documents a day it
// is cheap; this is the measurement that argument is owed. It asserts almost
// nothing — the number is the point, and it is logged.
func TestRebuildAllTimingOnARealisticCorpus(t *testing.T) {
if testing.Short() {
t.Skip("timing run")
}
const spaces, perSpace = 5, 200
var all []Document
for s := range spaces {
c := newCorpus(t, fmt.Sprintf("~bigbes/space-%d", s), "rev1")
for i := range perSpace {
c.add(fmt.Sprintf("specs/doc-%03d.md", i), syntheticDocument(s, i))
}
all = append(all, c.extract(t)...)
}
idx := openIndex(t)
st, err := idx.RebuildAll(context.Background(), all)
require.NoError(t, err)
require.Equal(t, spaces*perSpace, st.Indexed)
t.Logf("RebuildAll over %d documents in %d spaces: %s", st.Indexed, st.Spaces, st)
one := all[:perSpace]
spaceStats, err := idx.RebuildSpace(context.Background(), one[0].Space, one)
require.NoError(t, err)
t.Logf("RebuildSpace over %d documents: %s", spaceStats.Indexed, spaceStats)
for _, q := range []string{"ревизия", "proposal", "предложение"} {
res, err := idx.Search(context.Background(), Query{Text: q, Limit: 10, Spaces: core.EverythingFilter()})
require.NoError(t, err)
t.Logf("query %q over %d documents: %d matches in %s", q, st.Indexed, res.Total, res.Took)
require.Positive(t, res.Total)
}
}
// syntheticDocument is roughly the size and shape of a real spec: frontmatter,
// English prose, Russian prose, and a fenced block.
func syntheticDocument(space, n int) string {
var b strings.Builder
fmt.Fprintf(&b, "---\nid: SPEC-%d%04d\ntitle: Storage model revision %d\n"+
"status: draft\ntags: [storage, review, index]\nowners: ['~bigbes']\n"+
"summary: One tier, git objects only, revision %d\n---\n\n", space, n, n, n)
for i := range 6 {
fmt.Fprintf(&b, "## Section %d\n\n%s\n\n%s\n\n", i, enSpecBody, ruSpecBody)
fmt.Fprintf(&b, "```go\nfunc Rebuild%d(ctx context.Context) error { return nil }\n```\n\n", i)
}
return b.String()
}
// A failed rebuild must leave the live index serving. RebuildAll builds beside
// it and swaps only on success, so a cancellation mid-rebuild is a no-op.
func TestFailedRebuildAllLeavesTheLiveIndexIntact(t *testing.T) {
live := newCorpus(t, "~bigbes/rfcs", "rev1").
add("specs/a.md", "---\nid: SPEC-0001\ntitle: A\n---\n\nshared vocabulary term\n")
idx := indexCorpus(t, live)
next := newCorpus(t, "~bigbes/rfcs", "rev2").
add("specs/b.md", "---\nid: SPEC-0002\ntitle: B\n---\n\nshared vocabulary term\n")
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := idx.RebuildAll(ctx, next.extract(t))
require.ErrorIs(t, err, context.Canceled)
require.Equal(t, []string{"SPEC-0001"}, hitIDs(t, idx, Query{Text: "vocabulary", Spaces: core.EverythingFilter()}),
"the old index is still open and still answering")
for _, p := range tmpPaths(idx.Path()) {
_, err := os.Stat(p)
require.True(t, os.IsNotExist(err), "%s should have been cleaned up", filepath.Base(p))
}
}
// The daemon answers queries while a rebuild runs. Searches take a read lock
// and a rebuild takes the write lock; this is the check that they compose.
func TestSearchesRunConcurrentlyWithARebuild(t *testing.T) {
c := newCorpus(t, "~bigbes/rfcs", "rev1")
for i := range 50 {
c.add(fmt.Sprintf("specs/%d.md", i),
fmt.Sprintf("---\nid: SPEC-00%02d\ntitle: Document %d\n---\n\nshared vocabulary term\n", i, i))
}
idx := indexCorpus(t, c)
docs := c.extract(t)
var wg sync.WaitGroup
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
for range 20 {
_, err := idx.Search(context.Background(), Query{Text: "vocabulary", Spaces: core.EverythingFilter()})
assert.NoError(t, err)
}
}()
}
for range 3 {
wg.Add(1)
go func() {
defer wg.Done()
_, err := idx.RebuildSpace(context.Background(), c.Space, docs)
assert.NoError(t, err)
}()
}
wg.Wait()
require.Len(t, hitIDs(t, idx, Query{Text: "vocabulary", Limit: 100, Spaces: core.EverythingFilter()}), 50)
}
// A document at the space root has no section. Excluding the log section must
// not also exclude it: "not in the log" is not "has a section".
func TestRootLevelDocumentsSurviveTheDefaultLogExclusion(t *testing.T) {
idx := indexCorpus(t, newCorpus(t, "~bigbes/rfcs", "rev1").
add("README.md", "---\nid: SPEC-0001\ntitle: Read me\n---\n\nshared vocabulary term\n").
add("specs/a.md", "---\nid: SPEC-0002\ntitle: A\n---\n\nshared vocabulary term\n"))
require.ElementsMatch(t, []string{"SPEC-0001", "SPEC-0002"}, hitIDs(t, idx, Query{Text: "vocabulary", Spaces: core.EverythingFilter()}))
}