package search
import (
"testing"
"github.com/stretchr/testify/require"
"sourcecraft.dev/bigbes/sr-ht-spec/doc"
"sourcecraft.dev/bigbes/sr-ht-spec/gitx"
)
func byID(docs []Document) map[string]Document {
m := make(map[string]Document, len(docs))
for _, d := range docs {
m[d.ID] = d
}
return m
}
func TestExtractCarriesSpaceRevisionAndPath(t *testing.T) {
c := newCorpus(t, "~bigbes/specs", "8f14e45fceea167a5a36dedd4bea2543").
add("specs/storage.md", "---\nid: SPEC-0001\ntitle: Storage model\n---\n\nBodies live in git.\n")
docs := c.extract(t)
require.Len(t, docs, 1)
require.Equal(t, c.Space, docs[0].Space)
require.Equal(t, "8f14e45fceea167a5a36dedd4bea2543", docs[0].Rev)
require.Equal(t, "specs/storage.md", docs[0].Path)
require.Equal(t, "specs", docs[0].Section)
require.Equal(t, "SPEC-0001", docs[0].ID)
require.Empty(t, docs[0].Anchor)
}
// Frontmatter is prepended to the searchable text, so a tag or an owner finds
// the document even though neither is prose. Carried over from warren.
func TestExtractIndexesFrontmatterAsText(t *testing.T) {
c := newCorpus(t, "~bigbes/specs", "rev1").
add("specs/storage.md", "---\nid: SPEC-0001\ntitle: Storage model\n"+
"tags: [storage, review]\nowners: ['~bigbes']\nsummary: One tier, git objects only\n---\n\n"+
"Bodies live in git.\n")
docs := c.extract(t)
require.Len(t, docs, 1)
require.Contains(t, docs[0].Text, "storage")
require.Contains(t, docs[0].Text, "review")
require.Contains(t, docs[0].Text, "One tier, git objects only")
require.Contains(t, docs[0].Text, "Bodies live in git.")
}
// A catalog is a page of one-line descriptions of other documents. Indexed
// whole, a query lands on the description instead of the document that owns it.
func TestExtractIndexesACatalogByTitleOnly(t *testing.T) {
c := newCorpus(t, "~bigbes/specs", "rev1").
add("specs/index.md", "# Specifications\n\n- [[SPEC-0001]] — the storage model\n- [[SPEC-0002]] — review\n").
add("specs/storage.md", "---\nid: SPEC-0001\ntitle: Storage model\n---\n\nBodies live in git.\n")
docs := byID(c.extract(t))
catalog, ok := docs["specs/index"]
require.True(t, ok, "the catalog is still indexed: %v", docs)
require.Equal(t, doc.KindCatalog, kindOf(t, c, "specs/index.md"))
require.Empty(t, catalog.Text, "a catalog contributes no body")
require.Equal(t, "Specifications", catalog.Title)
}
func kindOf(t *testing.T, c *corpus, path string) doc.PageKind {
t.Helper()
arc := doc.FromDocuments(c.Space, c.Rev, c.docs)
p, ok := arc.ByPath(path)
require.True(t, ok)
return p.Kind
}
// An activity log contributes its own title-only document plus one per dated
// entry, each anchored into the log it came from. Carried over from warren,
// where a hit anywhere in a 248 KB log resolved to the whole file.
func TestExtractSplitsAnActivityLogIntoEntries(t *testing.T) {
c := newCorpus(t, "~bigbes/specs", "rev1").
add("notes/log.md", "# Work log\n\n"+
"## [2026-05-31] ingest | Imported the RFC set\nPulled 40 documents in from the old wiki.\n\n"+
"## [2026-05-30] lint | Fixed frontmatter\nEvery document now carries a status.\n")
docs := byID(c.extract(t))
require.Len(t, docs, 3)
page, ok := docs["notes/log"]
require.True(t, ok)
require.Equal(t, doc.LogSection, page.Section, "the log page moves to the log section")
require.Empty(t, page.Text)
first, ok := docs["notes/log#2026-05-31-1"]
require.True(t, ok, "entries are keyed under their own document: %v", docs)
require.Equal(t, doc.LogSection, first.Section)
require.Equal(t, "notes/log.md", first.Path, "an entry points back at the log it lives in")
require.Equal(t, "e-2026-05-31-1", first.Anchor)
require.Equal(t, "2026-05-31 Imported the RFC set", first.Title)
require.Contains(t, first.Text, "Pulled 40 documents in from the old wiki.")
require.NotContains(t, first.Text, "Fixed frontmatter", "entries do not bleed into each other")
}
// doc.SplitLog names entries "log#<date>-<n>", after warren's single vault-wide
// log. Two logs in one space would then produce colliding ids, and in one index
// colliding ids are the same document — one log would silently overwrite the
// other. Entry ids are namespaced by their own document to prevent it.
func TestExtractNamespacesLogEntriesPerDocument(t *testing.T) {
entry := "\n## [2026-05-31] ingest | Same day, two logs\nBody.\n"
c := newCorpus(t, "~bigbes/specs", "rev1").
add("log.md", "# Space log\n"+entry).
add("notes/log.md", "# Notes log\n"+entry)
docs := byID(c.extract(t))
require.Contains(t, docs, "log#2026-05-31-1", "a log whose page id is \"log\" keeps warren's ids")
require.Contains(t, docs, "notes/log#2026-05-31-1")
require.Len(t, docs, 4, "two log pages and two entries, none of them merged")
}
// A page in the archive with no body is a caller bug. Indexing it with an empty
// body would leave a document that exists and is unfindable, which is the
// failure mode that surfaces months later.
func TestExtractRefusesAPageWithNoBody(t *testing.T) {
c := newCorpus(t, "~bigbes/specs", "rev1").
add("specs/storage.md", "---\nid: SPEC-0001\ntitle: Storage model\n---\n\nBodies live in git.\n")
arc := doc.FromDocuments(c.Space, c.Rev, c.docs)
_, err := Extract(arc, map[string][]byte{})
require.ErrorContains(t, err, "no body supplied for specs/storage.md")
}
// A document whose frontmatter core rejects still renders and still indexes:
// --push-option=skip-validation means a broken header can reach the approved
// branch, and refusing to index it would turn a typo into a silent hole.
func TestExtractIndexesADocumentWithABrokenHeader(t *testing.T) {
c := newCorpus(t, "~bigbes/specs", "rev1").
add("specs/broken.md", "---\nid: [not, a, string\n---\n\n# Broken but readable\n\nThe body is still prose.\n")
docs := c.extract(t)
require.Len(t, docs, 1)
require.Equal(t, "specs/broken", docs[0].ID, "it falls back to its path")
require.Contains(t, docs[0].Text, "The body is still prose.")
}
func TestBodiesKeysByPath(t *testing.T) {
got := Bodies([]gitx.Document{
{Path: "a.md", Data: []byte("one")},
{Path: "b/c.md", Data: []byte("two")},
})
require.Equal(t, map[string][]byte{"a.md": []byte("one"), "b/c.md": []byte("two")}, got)
}
func TestExtractNeedsAnArchive(t *testing.T) {
_, err := Extract(nil, nil)
require.ErrorContains(t, err, "needs an archive")
}