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#-", 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") }