diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07f8f4f..b3fc067 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -118,7 +118,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -a "v${{ github.event.inputs.version }}" -m "Release v${{ github.event.inputs.version }}" + git tag -a "v${{ github.event.inputs.version }}" -m "v${{ github.event.inputs.version }}" git push origin "v${{ github.event.inputs.version }}" - name: Create GitHub Release @@ -127,4 +127,4 @@ jobs: tag_name: v${{ github.event.inputs.version }} name: knowledge-mcp v${{ github.event.inputs.version }} generate_release_notes: true - files: release/* \ No newline at end of file + files: release/* diff --git a/main.go b/main.go index b05124e..8dc8eff 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,6 @@ import ( "github.com/renderorange/knowledge-mcp/hook" "github.com/renderorange/knowledge-mcp/install" - "github.com/renderorange/knowledge-mcp/knowledge" "github.com/renderorange/knowledge-mcp/projects" "github.com/renderorange/knowledge-mcp/search" "github.com/renderorange/knowledge-mcp/tools" @@ -66,6 +65,7 @@ func main() { globalPath := flag.String("global", "", "Path to a global knowledge store shared across all projects") indexOverride := flag.String("index", "", "Override the search index location") storeDir := flag.String("store", "", "Central directory for all knowledge stores; in-tree .agents/ is ignored when set") + noIndexOnStartup := flag.Bool("no-index-on-startup", false, "Skip indexing on startup; index may be stale or empty") flag.Var(&roots, "root", "Org root whose immediate children are projects (repeatable)") flag.Var(&projs, "project", "Single project root (repeatable)") flag.Parse() @@ -100,7 +100,9 @@ func main() { } defer idx.Close() - indexAll(resolver, idx) + if !*noIndexOnStartup { + go idx.IndexAll(resolver) + } // Create MCP server s := server.NewMCPServer( @@ -129,70 +131,6 @@ func main() { } } -// indexAll indexes every ref known to the resolver. -func indexAll(res *projects.Resolver, idx *search.Index) { - for _, ref := range res.Snapshot() { - switch ref.Kind { - case projects.KindProject: - indexProjectKnowledge(res.AgentsDir(ref), ref.Address, idx) - case projects.KindOrg: - indexOrgKnowledge(res.OrgKnowledgeDir(ref), ref.Name, idx) - case projects.KindGlobal: - indexProjectKnowledge(res.AgentsDir(ref), ref.Address, idx) - } - } -} - -// indexProjectKnowledge indexes all knowledge files under an agents dir -// under the given addressing name. -func indexProjectKnowledge(agentsDir, projectName string, idx *search.Index) { - for _, cat := range knowledge.ValidCategories() { - catPath := knowledge.CategoryFilePath(agentsDir, cat) - kf, err := knowledge.Load(catPath) - if err != nil { - continue - } - for _, entry := range kf.Entries { - doc := search.SearchDocument{ - Summary: entry.Summary, - Detail: entry.Detail, - Rule: entry.Rule, - Category: cat, - Project: projectName, - } - if addErr := idx.Add(projectName+"/"+entry.ID, doc); addErr != nil { - log.Printf("warning: failed to index %s/%s: %v", projectName, entry.ID, addErr) - } - } - } -} - -// indexOrgKnowledge indexes an org's knowledge files as individual markdown -// sections, so queries return only the relevant section instead of the -// entire document. -func indexOrgKnowledge(knowledgeDir, orgName string, idx *search.Index) { - for _, catFile := range []string{"architecture.md", "review.md"} { - filePath := filepath.Join(knowledgeDir, catFile) - data, err := os.ReadFile(filePath) - if err != nil { - continue - } - for _, sec := range knowledge.SplitSections(string(data)) { - heading, body := sec[0], sec[1] - doc := search.SearchDocument{ - Summary: fmt.Sprintf("%s: %s", catFile, heading), - Detail: body, - Category: "conventions", - Project: orgName, - } - id := fmt.Sprintf("%s/org-%s::%s", orgName, catFile, heading) - if addErr := idx.Add(id, doc); addErr != nil { - log.Printf("warning: failed to index %s: %v", id, addErr) - } - } - } -} - // runHook executes hook-augment against the recorded install config. func runHook() { if err := hook.Run(os.Stdin, os.Stdout); err != nil { diff --git a/search/index.go b/search/index.go index 840d80f..68fc373 100644 --- a/search/index.go +++ b/search/index.go @@ -9,10 +9,13 @@ import ( "slices" "sort" "strings" + "sync" "github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2/mapping" "github.com/blevesearch/bleve/v2/search/query" + "github.com/renderorange/knowledge-mcp/knowledge" + "github.com/renderorange/knowledge-mcp/projects" ) // metaFileVersion is the on-disk key-format version. A mismatch forces an index rebuild. @@ -54,6 +57,8 @@ type SearchResult struct { type Index struct { indexPath string index bleve.Index + ready chan struct{} // closed when indexing completes or stale data exists + readyOnce sync.Once // guards close of ready to prevent double-close panic } // NewIndex opens or creates a bleve index at the given path. indexNames is the @@ -97,7 +102,14 @@ func NewIndex(indexPath string, indexNames []string) (*Index, error) { return nil, fmt.Errorf("write index meta: %w", marshalErr) } - return &Index{indexPath: indexPath, index: idx}, nil + ki := &Index{indexPath: indexPath, index: idx, ready: make(chan struct{})} + + // If index has data, close ready immediately (stale data available) + if ki.indexHasData() { + ki.CloseReady() + } + + return ki, nil } func metaFilePath(indexPath string) string { @@ -196,6 +208,9 @@ func (i *Index) Add(id string, doc SearchDocument) error { // Query searches the index, scoped to one project, with full-text // search and optional filters. func (i *Index) Query(project, q, category string, limit int) ([]SearchResult, error) { + // Block until indexing completes if no stale data available + <-i.ready + if limit <= 0 { limit = 10 } @@ -272,6 +287,92 @@ func (i *Index) Close() error { return i.index.Close() } +// CloseReady closes the ready channel exactly once, preventing double-close panics. +// This is used by tests that add documents directly without IndexAll. +func (i *Index) CloseReady() { + i.readyOnce.Do(func() { close(i.ready) }) +} + +// indexHasData reports whether the index contains any documents. +func (i *Index) indexHasData() bool { + req := bleve.NewSearchRequest(bleve.NewMatchAllQuery()) + req.Size = 0 + result, err := i.index.Search(req) + if err != nil { + return false + } + return result.Total > 0 +} + +// WaitReady blocks until the index has data available for queries. +func (i *Index) WaitReady() { + <-i.ready +} + +// IndexAll indexes all projects known to the resolver in the background. +// It closes the ready channel when complete. +func (i *Index) IndexAll(res *projects.Resolver) { + defer i.CloseReady() + + for _, ref := range res.Snapshot() { + switch ref.Kind { + case projects.KindProject: + i.indexProjectKnowledge(res.AgentsDir(ref), ref.Address) + case projects.KindOrg: + i.indexOrgKnowledge(res.OrgKnowledgeDir(ref), ref.Name) + case projects.KindGlobal: + i.indexProjectKnowledge(res.AgentsDir(ref), ref.Address) + } + } +} + +// indexProjectKnowledge indexes all knowledge files under an agents dir. +func (i *Index) indexProjectKnowledge(agentsDir, projectName string) { + for _, cat := range knowledge.ValidCategories() { + catPath := knowledge.CategoryFilePath(agentsDir, cat) + kf, err := knowledge.Load(catPath) + if err != nil { + continue + } + for _, entry := range kf.Entries { + doc := SearchDocument{ + Summary: entry.Summary, + Detail: entry.Detail, + Rule: entry.Rule, + Category: cat, + Project: projectName, + } + if addErr := i.Add(projectName+"/"+entry.ID, doc); addErr != nil { + log.Printf("warning: failed to index %s/%s: %v", projectName, entry.ID, addErr) + } + } + } +} + +// indexOrgKnowledge indexes an org's knowledge files as sections. +func (i *Index) indexOrgKnowledge(knowledgeDir, orgName string) { + for _, catFile := range []string{"architecture.md", "review.md"} { + filePath := filepath.Join(knowledgeDir, catFile) + data, err := os.ReadFile(filePath) + if err != nil { + continue + } + for _, sec := range knowledge.SplitSections(string(data)) { + heading, body := sec[0], sec[1] + doc := SearchDocument{ + Summary: fmt.Sprintf("%s: %s", catFile, heading), + Detail: body, + Category: "conventions", + Project: orgName, + } + id := fmt.Sprintf("%s/org-%s::%s", orgName, catFile, heading) + if addErr := i.Add(id, doc); addErr != nil { + log.Printf("warning: failed to index %s: %v", id, addErr) + } + } + } +} + // DeleteIndex removes a bleve index directory. func DeleteIndex(indexPath string) error { return os.RemoveAll(indexPath) diff --git a/search/index_test.go b/search/index_test.go index def9ef7..1c6c490 100644 --- a/search/index_test.go +++ b/search/index_test.go @@ -5,6 +5,9 @@ import ( "os" "path/filepath" "testing" + "time" + + "github.com/renderorange/knowledge-mcp/projects" ) func newIndex(t *testing.T, path string, names []string) *Index { @@ -16,9 +19,18 @@ func newIndex(t *testing.T, path string, names []string) *Index { return idx } +// newIndexReady creates an index and closes the ready channel immediately, +// for unit tests that add documents directly and query without IndexAll. +func newIndexReady(t *testing.T, path string, names []string) *Index { + t.Helper() + idx := newIndex(t, path, names) + idx.CloseReady() + return idx +} + func TestIndexCreateAndQuery(t *testing.T) { dir := t.TempDir() - idx := newIndex(t, filepath.Join(dir, "test.bleve"), []string{"test"}) + idx := newIndexReady(t, filepath.Join(dir, "test.bleve"), []string{"test"}) defer idx.Close() docs := []struct { @@ -108,7 +120,7 @@ func TestIndexCreateAndQuery(t *testing.T) { func TestProjectScopingNoCrossProjectLeakage(t *testing.T) { dir := t.TempDir() indexPath := filepath.Join(dir, "test.bleve") - idx := newIndex(t, indexPath, []string{"alpha", "beta"}) + idx := newIndexReady(t, indexPath, []string{"alpha", "beta"}) defer idx.Close() // Same bare entry ID in two projects — must not overwrite each other. @@ -195,7 +207,7 @@ func TestIndexRebuildOnNameSetChange(t *testing.T) { idx2.Close() // Different name set: rebuild, old docs gone. - idx3 := newIndex(t, indexPath, []string{"alpha", "beta"}) + idx3 := newIndexReady(t, indexPath, []string{"alpha", "beta"}) defer idx3.Close() results, err = idx3.Query("alpha", "", "", 10) if err != nil { @@ -208,7 +220,7 @@ func TestIndexRebuildOnNameSetChange(t *testing.T) { func TestIndexEmptyQuery(t *testing.T) { dir := t.TempDir() - idx := newIndex(t, filepath.Join(dir, "test.bleve"), []string{"test"}) + idx := newIndexReady(t, filepath.Join(dir, "test.bleve"), []string{"test"}) defer idx.Close() if err := idx.Add("test/test-001", SearchDocument{ @@ -228,7 +240,7 @@ func TestIndexEmptyQuery(t *testing.T) { func TestQueryEscapesSpecialCharacters(t *testing.T) { dir := t.TempDir() - idx := newIndex(t, filepath.Join(dir, "test.bleve"), []string{"test"}) + idx := newIndexReady(t, filepath.Join(dir, "test.bleve"), []string{"test"}) defer idx.Close() if err := idx.Add("test/conv-001", SearchDocument{ @@ -295,7 +307,7 @@ func TestIndexRecoverFromCorruption(t *testing.T) { } // Opening should recover from corruption, not fail - idx2 := newIndex(t, indexPath, []string{"test"}) + idx2 := newIndexReady(t, indexPath, []string{"test"}) defer idx2.Close() // Old data is gone (rebuilt), but server should work @@ -322,6 +334,255 @@ func TestIndexRecoverFromCorruption(t *testing.T) { } } +func TestIndexHasData(t *testing.T) { + dir := t.TempDir() + indexPath := filepath.Join(dir, "test.bleve") + + idx := newIndex(t, indexPath, []string{"test"}) + defer idx.Close() + if idx.indexHasData() { + t.Error("empty index should not have data") + } + + if err := idx.Add("test/conv-001", SearchDocument{Summary: "test", Project: "test"}); err != nil { + t.Fatalf("Add() error: %v", err) + } + + if !idx.indexHasData() { + t.Error("index with data should have data") + } +} + +func TestWaitReadyBlocksWhenEmpty(t *testing.T) { + dir := t.TempDir() + indexPath := filepath.Join(dir, "test.bleve") + + idx := newIndex(t, indexPath, []string{"test"}) + defer idx.Close() + + // ready channel should be open (blocking) for empty index + select { + case <-idx.ready: + t.Error("ready channel should not be closed for empty index") + default: + // expected: channel is open (blocking) + } +} + +func TestWaitReadyReturnsImmediatelyWithData(t *testing.T) { + dir := t.TempDir() + indexPath := filepath.Join(dir, "test.bleve") + + idx := newIndex(t, indexPath, []string{"test"}) + if err := idx.Add("test/conv-001", SearchDocument{Summary: "test", Project: "test"}); err != nil { + t.Fatalf("Add() error: %v", err) + } + idx.Close() + + // Reopen — index has data, ready should be closed immediately + idx2 := newIndex(t, indexPath, []string{"test"}) + defer idx2.Close() + + select { + case <-idx2.ready: + // expected: channel is closed (stale data available) + default: + t.Error("ready channel should be closed for index with stale data") + } +} + +func TestIndexAllBackground(t *testing.T) { + dir := t.TempDir() + indexPath := filepath.Join(dir, "test.bleve") + + idx := newIndex(t, indexPath, []string{"test-project"}) + defer idx.Close() + + // Create a mock knowledge file + agentsDir := filepath.Join(dir, "test-project", ".agents") + if err := os.MkdirAll(agentsDir, 0755); err != nil { + t.Fatalf("MkdirAll() error: %v", err) + } + conventionsPath := filepath.Join(agentsDir, "conventions.yaml") + if err := os.WriteFile(conventionsPath, []byte(`entries: +- id: conv-001 + summary: Test convention + detail: Test detail +`), 0644); err != nil { + t.Fatalf("WriteFile() error: %v", err) + } + + // Create a resolver with the test project + resolver, _, err := projects.BuildWithStore(nil, []string{filepath.Join(dir, "test-project")}, "", "") + if err != nil { + t.Fatalf("BuildWithStore() error: %v", err) + } + + // IndexAll should complete and close ready channel + go idx.IndexAll(resolver) + + // Wait for indexing to complete (with timeout) + select { + case <-idx.ready: + // success + case <-time.After(5 * time.Second): + t.Fatal("IndexAll did not complete within timeout") + } + + // Query should find the indexed document + results, err := idx.Query("test-project", "convention", "", 10) + if err != nil { + t.Fatalf("Query() error: %v", err) + } + if len(results) != 1 { + t.Fatalf("len(results) = %d, want 1", len(results)) + } + if results[0].Summary != "Test convention" { + t.Errorf("summary = %q, want %q", results[0].Summary, "Test convention") + } +} + +func TestIndexAllWithExistingDataNoDoubleClosePanic(t *testing.T) { + dir := t.TempDir() + indexPath := filepath.Join(dir, "test.bleve") + + // Create index and add a document, then close. + idx := newIndex(t, indexPath, []string{"test-project"}) + if err := idx.Add("test-project/conv-001", SearchDocument{ + Summary: "persisted doc", Project: "test-project", + }); err != nil { + t.Fatalf("Add() error: %v", err) + } + idx.Close() + + // Reopen — NewIndex will close ready because index has data. + idx2 := newIndex(t, indexPath, []string{"test-project"}) + defer idx2.Close() + + // Verify ready is already closed. + select { + case <-idx2.ready: + // expected + default: + t.Fatal("ready channel should be closed for index with stale data") + } + + // Create a resolver so IndexAll can run. + agentsDir := filepath.Join(dir, "test-project", ".agents") + if err := os.MkdirAll(agentsDir, 0755); err != nil { + t.Fatalf("MkdirAll() error: %v", err) + } + conventionsPath := filepath.Join(agentsDir, "conventions.yaml") + if err := os.WriteFile(conventionsPath, []byte(`entries: +- id: conv-001 + summary: Test convention + detail: Test detail +`), 0644); err != nil { + t.Fatalf("WriteFile() error: %v", err) + } + + resolver, _, err := projects.BuildWithStore(nil, []string{filepath.Join(dir, "test-project")}, "", "") + if err != nil { + t.Fatalf("BuildWithStore() error: %v", err) + } + + // IndexAll must NOT panic — it should handle the already-closed ready channel. + done := make(chan struct{}) + go func() { + defer close(done) + idx2.IndexAll(resolver) + }() + + select { + case <-done: + // success: no panic + case <-time.After(5 * time.Second): + t.Fatal("IndexAll did not complete within timeout") + } + + results, err := idx2.Query("test-project", "convention", "", 10) + if err != nil { + t.Fatalf("Query() error: %v", err) + } + if len(results) != 1 { + t.Fatalf("len(results) = %d, want 1", len(results)) + } +} + +func TestBackgroundIndexingIntegration(t *testing.T) { + dir := t.TempDir() + indexPath := filepath.Join(dir, "test.bleve") + + // Create index with no data (ready channel should be open) + idx := newIndex(t, indexPath, []string{"test"}) + + // Verify ready is blocking + select { + case <-idx.ready: + t.Fatal("ready should not be closed for empty index") + default: + // expected + } + + // Create knowledge files + agentsDir := filepath.Join(dir, "project1", ".agents") + if err := os.MkdirAll(agentsDir, 0755); err != nil { + t.Fatalf("MkdirAll() error: %v", err) + } + if err := os.WriteFile(filepath.Join(agentsDir, "conventions.yaml"), []byte(`entries: +- id: conv-001 + summary: First convention + detail: Detail 1 +- id: conv-002 + summary: Second convention + detail: Detail 2 +`), 0644); err != nil { + t.Fatalf("WriteFile() error: %v", err) + } + + // Build resolver + resolver, _, err := projects.BuildWithStore(nil, []string{filepath.Join(dir, "project1")}, "", "") + if err != nil { + t.Fatalf("BuildWithStore() error: %v", err) + } + + // Start background indexing + go idx.IndexAll(resolver) + + // WaitReady should block until IndexAll completes + done := make(chan struct{}) + go func() { + defer close(done) + idx.WaitReady() + }() + + select { + case <-done: + // success: WaitReady returned after indexing + case <-time.After(5 * time.Second): + t.Fatal("WaitReady did not return within timeout") + } + + // Verify ready is now closed + select { + case <-idx.ready: + // expected + default: + t.Fatal("ready should be closed after IndexAll completes") + } + + // Query should return the indexed documents + results, err := idx.Query("project1", "convention", "", 10) + if err != nil { + t.Fatalf("Query() error: %v", err) + } + if len(results) != 2 { + t.Fatalf("len(results) = %d, want 2", len(results)) + } + + idx.Close() +} + func TestIndexRecoverFromSilentCorruption(t *testing.T) { dir := t.TempDir() indexPath := filepath.Join(dir, "test.bleve") @@ -345,7 +606,7 @@ func TestIndexRecoverFromSilentCorruption(t *testing.T) { } // Opening should detect the unhealthy index and rebuild - idx2 := newIndex(t, indexPath, []string{"test"}) + idx2 := newIndexReady(t, indexPath, []string{"test"}) defer idx2.Close() // Old data is gone (rebuilt), but server should work diff --git a/tools/integration_test.go b/tools/integration_test.go index 9e2315c..76d84d9 100644 --- a/tools/integration_test.go +++ b/tools/integration_test.go @@ -25,10 +25,7 @@ func TestEndToEnd(t *testing.T) { } indexPath := filepath.Join(dir, ".index") - idx, err := search.NewIndex(indexPath, []string{projectName}) - if err != nil { - t.Fatalf("NewIndex() error: %v", err) - } + idx := newIndexReady(t, indexPath, []string{projectName}) defer idx.Close() // Step 1: Init @@ -301,10 +298,7 @@ func TestFullWorkflow(t *testing.T) { t.Fatalf("projects.Build() error: %v", err) } - idx, err := search.NewIndex(filepath.Join(orgRoot, ".agents", ".index"), []string{"app", "lib"}) - if err != nil { - t.Fatalf("NewIndex() error: %v", err) - } + idx := newIndexReady(t, filepath.Join(orgRoot, ".agents", ".index"), []string{"app", "lib"}) defer idx.Close() // 1. List projects @@ -388,10 +382,7 @@ func TestStoreModeEndToEnd(t *testing.T) { t.Errorf("expected in-tree ignore warning, got %v", warnings) } - idx, err := search.NewIndex(filepath.Join(store, ".index"), []string{"proj"}) - if err != nil { - t.Fatalf("NewIndex() error: %v", err) - } + idx := newIndexReady(t, filepath.Join(store, ".index"), []string{"proj"}) defer idx.Close() // Init writes into the store. diff --git a/tools/tools_test.go b/tools/tools_test.go index feec35c..4cfc030 100644 --- a/tools/tools_test.go +++ b/tools/tools_test.go @@ -15,6 +15,18 @@ import ( "github.com/renderorange/knowledge-mcp/search" ) +// newIndexReady creates an index and closes the ready channel immediately, +// for unit tests that add documents directly and query without IndexAll. +func newIndexReady(t *testing.T, path string, names []string) *search.Index { + t.Helper() + idx, err := search.NewIndex(path, names) + if err != nil { + t.Fatalf("search.NewIndex() error: %v", err) + } + idx.CloseReady() + return idx +} + func TestInitHandler(t *testing.T) { dir := t.TempDir() @@ -210,7 +222,7 @@ func TestQueryHandler(t *testing.T) { knowledge.Save(knowledge.CategoryFilePath(agentsDir, "conventions"), kf) indexPath := filepath.Join(dir, ".index") - idx, _ := search.NewIndex(indexPath, []string{"test"}) + idx := newIndexReady(t, indexPath, []string{"test"}) defer idx.Close() resolver, _, err := projects.Build([]string{root}, nil, "") @@ -290,10 +302,7 @@ func TestListHandler(t *testing.T) { } indexPath := filepath.Join(root, ".index") - idx, err := search.NewIndex(indexPath, []string{"test"}) - if err != nil { - t.Fatalf("search.NewIndex() error: %v", err) - } + idx := newIndexReady(t, indexPath, []string{"test"}) defer idx.Close() handler := ListHandler(resolver, idx) @@ -701,10 +710,7 @@ func TestQueryProjectIsolation(t *testing.T) { t.Fatalf("projects.Build() error: %v", err) } - idx, err := search.NewIndex(filepath.Join(orgDir, ".index"), []string{"projectA", "projectB"}) - if err != nil { - t.Fatalf("NewIndex() error: %v", err) - } + idx := newIndexReady(t, filepath.Join(orgDir, ".index"), []string{"projectA", "projectB"}) defer idx.Close() write := WriteHandler(resolver, idx) @@ -792,10 +798,7 @@ func TestQueryWithGlobalMerge(t *testing.T) { } indexPath := filepath.Join(root, ".index") - idx, err := search.NewIndex(indexPath, []string{"myproj", "_global"}) - if err != nil { - t.Fatalf("search.NewIndex() error: %v", err) - } + idx := newIndexReady(t, indexPath, []string{"myproj", "_global"}) defer idx.Close() // Write to project @@ -874,10 +877,7 @@ func TestQueryWithoutGlobal(t *testing.T) { } indexPath := filepath.Join(root, ".index") - idx, err := search.NewIndex(indexPath, []string{"myproj"}) - if err != nil { - t.Fatalf("search.NewIndex() error: %v", err) - } + idx := newIndexReady(t, indexPath, []string{"myproj"}) defer idx.Close() write := WriteHandler(resolver, idx) @@ -914,7 +914,7 @@ func TestWriteWithRule(t *testing.T) { knowledge.Save(knowledge.CategoryFilePath(agentsDir, "conventions"), kf) indexPath := filepath.Join(dir, ".index") - idx, _ := search.NewIndex(indexPath, []string{"test"}) + idx := newIndexReady(t, indexPath, []string{"test"}) defer idx.Close() resolver, _, err := projects.Build([]string{root}, nil, "") @@ -1063,7 +1063,7 @@ func TestListGlobalProvenance(t *testing.T) { } indexPath := filepath.Join(root, ".index") - idx, _ := search.NewIndex(indexPath, []string{"myproj", "_global"}) + idx := newIndexReady(t, indexPath, []string{"myproj", "_global"}) defer idx.Close() handler := ListHandler(resolver, idx) @@ -1097,7 +1097,7 @@ func TestListOrgGroupedByFile(t *testing.T) { rootName := filepath.Base(root) indexPath := filepath.Join(root, ".index") - idx, _ := search.NewIndex(indexPath, []string{rootName}) + idx := newIndexReady(t, indexPath, []string{rootName}) defer idx.Close() for _, sec := range []struct{ file, heading, detail string }{ @@ -1150,7 +1150,7 @@ func TestListConstraintsSeparation(t *testing.T) { } indexPath := filepath.Join(root, ".index") - idx, _ := search.NewIndex(indexPath, []string{"test"}) + idx := newIndexReady(t, indexPath, []string{"test"}) defer idx.Close() handler := ListHandler(resolver, idx)