diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 9462125..4a314ae 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -3,6 +3,7 @@ package testutil import ( "context" "fmt" + "path/filepath" "strings" "testing" @@ -26,6 +27,24 @@ func OpenTestDB(t *testing.T) *store.Store { return st } +// OpenTestDBFile is OpenTestDB on a file-backed store, returning the db path for reopen/stat tests. +func OpenTestDBFile(t *testing.T) (*store.Store, string) { + t.Helper() + path := filepath.Join(t.TempDir(), "test.db") + + st, err := store.Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + + if err := st.Migrate(context.Background()); err != nil { + t.Fatalf("Migrate: %v", err) + } + + t.Cleanup(func() { _ = st.Close() }) + return st, path +} + func LogTree(t *testing.T, st *store.Store) { t.Helper() ctx := context.Background() diff --git a/pkg/doctor/doctor_test.go b/pkg/doctor/doctor_test.go index 7850406..d105239 100644 --- a/pkg/doctor/doctor_test.go +++ b/pkg/doctor/doctor_test.go @@ -5,33 +5,14 @@ import ( "context" "database/sql" "encoding/json" - "path/filepath" "strings" "testing" - "github.com/radimsem/remindb/pkg/store" + "github.com/radimsem/remindb/internal/testutil" ) -func newCleanStore(t *testing.T) (*store.Store, string) { - t.Helper() - - dir := t.TempDir() - path := filepath.Join(dir, "doctor.db") - - st, err := store.Open(path) - if err != nil { - t.Fatalf("Open: %v", err) - } - t.Cleanup(func() { _ = st.Close() }) - - if err := st.Migrate(context.Background()); err != nil { - t.Fatalf("Migrate: %v", err) - } - return st, path -} - func TestRunCleanDB(t *testing.T) { - st, _ := newCleanStore(t) + st, _ := testutil.OpenTestDBFile(t) report := Run(context.Background(), st) @@ -66,7 +47,7 @@ func TestRunCleanDB(t *testing.T) { } func TestHealFixesBrokenFTS(t *testing.T) { - st, path := newCleanStore(t) + st, path := testutil.OpenTestDBFile(t) insertSampleNodes(t, path) @@ -99,7 +80,7 @@ func TestHealFixesBrokenFTS(t *testing.T) { } func TestHealIsIdempotent(t *testing.T) { - st, _ := newCleanStore(t) + st, _ := testutil.OpenTestDBFile(t) first := Heal(context.Background(), st) second := Heal(context.Background(), st) @@ -116,7 +97,7 @@ func TestHealIsIdempotent(t *testing.T) { } func TestReportJSONShape(t *testing.T) { - st, _ := newCleanStore(t) + st, _ := testutil.OpenTestDBFile(t) report := Run(context.Background(), st) @@ -151,7 +132,7 @@ func TestReportJSONShape(t *testing.T) { } func TestReportTextShape(t *testing.T) { - st, _ := newCleanStore(t) + st, _ := testutil.OpenTestDBFile(t) report := Run(context.Background(), st) @@ -254,7 +235,7 @@ func TestWriteTextHeader(t *testing.T) { } func TestWriteTextHeaderHealsToHealthy(t *testing.T) { - st, path := newCleanStore(t) + st, path := testutil.OpenTestDBFile(t) insertSampleNodes(t, path) if err := breakFTSSync(path); err != nil { diff --git a/pkg/inspect/inspect_test.go b/pkg/inspect/inspect_test.go index 0d820d7..66038c2 100644 --- a/pkg/inspect/inspect_test.go +++ b/pkg/inspect/inspect_test.go @@ -5,26 +5,11 @@ import ( "strings" "testing" + "github.com/radimsem/remindb/internal/testutil" "github.com/radimsem/remindb/pkg/inspect" "github.com/radimsem/remindb/pkg/store" ) -func openTestStore(t *testing.T) *store.Store { - t.Helper() - - st, err := store.Open(":memory:") - if err != nil { - t.Fatalf("Open: %v", err) - } - - if err := st.Migrate(context.Background()); err != nil { - t.Fatalf("Migrate: %v", err) - } - - t.Cleanup(func() { _ = st.Close() }) - return st -} - func testNode(id, parent, nodeType string) *store.Node { return &store.Node{ ID: id, ParentID: parent, @@ -35,7 +20,7 @@ func testNode(id, parent, nodeType string) *store.Node { } func TestCollect_Empty(t *testing.T) { - st := openTestStore(t) + st := testutil.OpenTestDB(t) ctx := context.Background() s, err := inspect.Collect(ctx, st) @@ -52,7 +37,7 @@ func TestCollect_Empty(t *testing.T) { } func TestCollect_PopulatesAllFields(t *testing.T) { - st := openTestStore(t) + st := testutil.OpenTestDB(t) ctx := context.Background() n1 := testNode("aaaaaaaa", "", "heading") @@ -113,7 +98,7 @@ func TestCollect_PopulatesAllFields(t *testing.T) { } func TestCollect_RelationCountIncludesPending(t *testing.T) { - st := openTestStore(t) + st := testutil.OpenTestDB(t) ctx := context.Background() for _, n := range []*store.Node{ diff --git a/pkg/mcp/resources/doctor_test.go b/pkg/mcp/resources/doctor_test.go index efa97aa..d04d66f 100644 --- a/pkg/mcp/resources/doctor_test.go +++ b/pkg/mcp/resources/doctor_test.go @@ -9,6 +9,7 @@ import ( "reflect" "testing" + "github.com/radimsem/remindb/internal/testutil" "github.com/radimsem/remindb/pkg/doctor" "github.com/radimsem/remindb/pkg/store" ) @@ -60,7 +61,7 @@ func assertCLIParity(t *testing.T, st *store.Store, got map[string]any, wantStat } func TestHandleDoctor_PassParity(t *testing.T) { - st := openGraphStore(t) + st := testutil.OpenTestDB(t) d := &Deps{Store: st} got := readDoctor(t, d) @@ -68,18 +69,7 @@ func TestHandleDoctor_PassParity(t *testing.T) { } func TestHandleDoctor_WarnParity(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "doctor.db") - - st, err := store.Open(path) - if err != nil { - t.Fatalf("Open: %v", err) - } - - t.Cleanup(func() { _ = st.Close() }) - if err := st.Migrate(context.Background()); err != nil { - t.Fatalf("Migrate: %v", err) - } + st, path := testutil.OpenTestDBFile(t) // A snapshot whose compile_root no longer exists trips stale_compile_root → warn. db, err := sql.Open("sqlite", path) @@ -88,7 +78,7 @@ func TestHandleDoctor_WarnParity(t *testing.T) { } defer func() { _ = db.Close() }() - gone := filepath.Join(dir, "vanished") + gone := filepath.Join(filepath.Dir(path), "vanished") if _, err := db.Exec( `INSERT INTO snapshots (cursor_hash, parent_id, message, compile_root) VALUES ('h', NULL, 'm', ?)`, gone, diff --git a/pkg/mcp/resources/graph_test.go b/pkg/mcp/resources/graph_test.go index ced6721..c98668b 100644 --- a/pkg/mcp/resources/graph_test.go +++ b/pkg/mcp/resources/graph_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "testing" + "github.com/radimsem/remindb/internal/testutil" "github.com/radimsem/remindb/pkg/store" ) @@ -16,25 +17,9 @@ func graphNode_(id string) *store.Node { } } -func openGraphStore(t *testing.T) *store.Store { - t.Helper() - - st, err := store.Open(":memory:") - if err != nil { - t.Fatalf("Open: %v", err) - } - - if err := st.Migrate(context.Background()); err != nil { - t.Fatalf("Migrate: %v", err) - } - t.Cleanup(func() { _ = st.Close() }) - - return st -} - // Fixture: parsed n1→n2, manual n2→n3, pending n1→"Unresolved"; n4 is orphan. func TestHandleGraph_ParsedManualPending(t *testing.T) { - st := openGraphStore(t) + st := testutil.OpenTestDB(t) ctx := context.Background() for _, id := range []string{"aaaaaaaaaa1", "bbbbbbbbbb1", "cccccccccc1", "dddddddddd1"} { @@ -107,7 +92,7 @@ func TestHandleGraph_ParsedManualPending(t *testing.T) { } func TestHandleGraph_EmptyDBStableShape(t *testing.T) { - st := openGraphStore(t) + st := testutil.OpenTestDB(t) d := &Deps{Store: st} body, err := d.graphBody(context.Background()) diff --git a/pkg/mcp/tools/tools_test.go b/pkg/mcp/tools/tools_test.go index f4e88d5..8dc0dfa 100644 --- a/pkg/mcp/tools/tools_test.go +++ b/pkg/mcp/tools/tools_test.go @@ -12,6 +12,7 @@ import ( gomcp "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/radimsem/remindb/internal/redaction" + "github.com/radimsem/remindb/internal/testutil" "github.com/radimsem/remindb/pkg/compiler" "github.com/radimsem/remindb/pkg/config" "github.com/radimsem/remindb/pkg/query" @@ -23,15 +24,7 @@ import ( func setup(t *testing.T) (*Deps, *store.Store) { t.Helper() - st, err := store.Open(":memory:") - if err != nil { - t.Fatalf("Open: %v", err) - } - - if err := st.Migrate(context.Background()); err != nil { - t.Fatalf("Migrate: %v", err) - } - t.Cleanup(func() { _ = st.Close() }) + st := testutil.OpenTestDB(t) tracker, err := temperature.NewTracker(st, "", temperature.DefaultConfig(), nil) if err != nil { diff --git a/pkg/relations/resolver_test.go b/pkg/relations/resolver_test.go index 033077c..6af595b 100644 --- a/pkg/relations/resolver_test.go +++ b/pkg/relations/resolver_test.go @@ -4,26 +4,11 @@ import ( "context" "testing" + "github.com/radimsem/remindb/internal/testutil" "github.com/radimsem/remindb/pkg/parser" "github.com/radimsem/remindb/pkg/store" ) -func openTestDB(t *testing.T) *store.Store { - t.Helper() - - st, err := store.Open(":memory:") - if err != nil { - t.Fatalf("Open: %v", err) - } - - if err := st.Migrate(context.Background()); err != nil { - t.Fatalf("Migrate: %v", err) - } - t.Cleanup(func() { _ = st.Close() }) - - return st -} - func mustHeading(t *testing.T, st *store.Store, id, sourceFile, label string, depth int) *store.Node { t.Helper() @@ -45,7 +30,7 @@ func mustHeading(t *testing.T, st *store.Store, id, sourceFile, label string, de } func TestResolve_ByIDHint_Hit(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) r := New(st) ctx := context.Background() @@ -62,7 +47,7 @@ func TestResolve_ByIDHint_Hit(t *testing.T) { // Per spec: missing IDHint does NOT fall back to label. func TestResolve_ByIDHint_MissNoFallback(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) r := New(st) ctx := context.Background() @@ -81,7 +66,7 @@ func TestResolve_ByIDHint_MissNoFallback(t *testing.T) { } func TestResolve_ByLabel(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) r := New(st) ctx := context.Background() @@ -97,7 +82,7 @@ func TestResolve_ByLabel(t *testing.T) { } func TestResolve_ByLabel_CaseInsensitive(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) r := New(st) ctx := context.Background() @@ -115,7 +100,7 @@ func TestResolve_ByLabel_CaseInsensitive(t *testing.T) { } func TestResolve_ByLabel_OnlyHeadings(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) r := New(st) ctx := context.Background() @@ -145,7 +130,7 @@ func TestResolve_ByLabel_OnlyHeadings(t *testing.T) { } func TestResolve_BySourceAndLabel(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) r := New(st) ctx := context.Background() @@ -166,7 +151,7 @@ func TestResolve_BySourceAndLabel(t *testing.T) { // Suffix match: user provides "docs/x.md" and source_file is the absolute "/abs/docs/x.md". func TestResolve_BySourceQual_SuffixMatch(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) r := New(st) ctx := context.Background() @@ -181,7 +166,7 @@ func TestResolve_BySourceQual_SuffixMatch(t *testing.T) { } func TestResolve_BySourceQual_MissNoFallback(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) r := New(st) ctx := context.Background() @@ -199,7 +184,7 @@ func TestResolve_BySourceQual_MissNoFallback(t *testing.T) { } func TestResolve_EmptyRefReturnsEmpty(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) r := New(st) got, err := r.Resolve(context.Background(), parser.WikilinkRef{}) @@ -213,7 +198,7 @@ func TestResolve_EmptyRefReturnsEmpty(t *testing.T) { // Disambiguation: same label in multiple files picks the lowest source_file first. func TestResolve_ByLabel_DisambiguationBySourceFile(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) r := New(st) ctx := context.Background() @@ -229,7 +214,7 @@ func TestResolve_ByLabel_DisambiguationBySourceFile(t *testing.T) { // End-to-end: Run() phase 1 emits a relation row for a resolved ref. func TestRun_Phase1_Hit(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) ctx := context.Background() src := mustHeading(t, st, "src11111111", "x.md", "Source", 1) @@ -259,7 +244,7 @@ func TestRun_Phase1_Hit(t *testing.T) { // Phase 1 miss → pending row gets inserted with origin=parsed. func TestRun_Phase1_Miss_GoesPending(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) ctx := context.Background() src := mustHeading(t, st, "src11111111", "x.md", "Source", 1) @@ -284,7 +269,7 @@ func TestRun_Phase1_Miss_GoesPending(t *testing.T) { // Recompiling the same source clears stale parsed pending entries first. func TestRun_Phase1_ClearsStaleParsedPending(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) ctx := context.Background() src := mustHeading(t, st, "src11111111", "x.md", "Source", 1) @@ -323,7 +308,7 @@ func TestRun_Phase1_ClearsStaleParsedPending(t *testing.T) { // Phase 2: a previously-pending row resolves on the next compile. func TestRun_Phase2_RetriesPendingAndMoves(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) ctx := context.Background() src := mustHeading(t, st, "src11111111", "x.md", "Source", 1) @@ -362,7 +347,7 @@ func TestRun_Phase2_RetriesPendingAndMoves(t *testing.T) { // Manual pending rows survive phase 1 (which only clears parsed pending). func TestRun_Phase1_DoesNotTouchManualPending(t *testing.T) { - st := openTestDB(t) + st := testutil.OpenTestDB(t) ctx := context.Background() src := mustHeading(t, st, "src11111111", "x.md", "Source", 1) diff --git a/pkg/store/doctor_test.go b/pkg/store/doctor_test.go index 94236ea..2895e9c 100644 --- a/pkg/store/doctor_test.go +++ b/pkg/store/doctor_test.go @@ -8,21 +8,6 @@ import ( "testing" ) -func newDoctorStore(t *testing.T) *Store { - t.Helper() - - st, err := Open(":memory:") - if err != nil { - t.Fatalf("Open: %v", err) - } - t.Cleanup(func() { _ = st.Close() }) - - if err := st.Migrate(context.Background()); err != nil { - t.Fatalf("Migrate: %v", err) - } - return st -} - func insertNodeRaw(t *testing.T, st *Store, id, parentID string) { t.Helper() @@ -41,7 +26,7 @@ func insertNodeRaw(t *testing.T, st *Store, id, parentID string) { } func TestCountsOnFreshDB(t *testing.T) { - st := newDoctorStore(t) + st := openTestDB(t) ctx := context.Background() for _, tc := range []struct { @@ -65,7 +50,7 @@ func TestCountsOnFreshDB(t *testing.T) { } func TestFTSIntegrityCheckPassesOnFreshDB(t *testing.T) { - st := newDoctorStore(t) + st := openTestDB(t) ctx := context.Background() insertNodeRaw(t, st, "n0000000001", "") @@ -77,7 +62,7 @@ func TestFTSIntegrityCheckPassesOnFreshDB(t *testing.T) { } func TestRebuildFTSResolvesIntegrityFailure(t *testing.T) { - st := newDoctorStore(t) + st := openTestDB(t) ctx := context.Background() insertNodeRaw(t, st, "n0000000001", "") @@ -105,7 +90,7 @@ func TestRebuildFTSResolvesIntegrityFailure(t *testing.T) { } func TestPromoteOrphansToRoots(t *testing.T) { - st := newDoctorStore(t) + st := openTestDB(t) ctx := context.Background() if _, err := st.db.Exec(`PRAGMA foreign_keys=OFF`); err != nil { @@ -140,7 +125,7 @@ func TestPromoteOrphansToRoots(t *testing.T) { } func TestDeleteDanglingDiffs(t *testing.T) { - st := newDoctorStore(t) + st := openTestDB(t) ctx := context.Background() if _, err := st.db.Exec(`PRAGMA foreign_keys=OFF`); err != nil { @@ -173,7 +158,7 @@ func TestDeleteDanglingDiffs(t *testing.T) { } func TestRepointHeadCursorEmptySnapshots(t *testing.T) { - st := newDoctorStore(t) + st := openTestDB(t) ctx := context.Background() if _, err := st.db.Exec(`PRAGMA foreign_keys=OFF`); err != nil { @@ -206,7 +191,7 @@ func TestRepointHeadCursorEmptySnapshots(t *testing.T) { } func TestRepointHeadCursorWithSnapshots(t *testing.T) { - st := newDoctorStore(t) + st := openTestDB(t) ctx := context.Background() if _, err := st.db.Exec(`INSERT INTO snapshots (cursor_hash) VALUES ('hash1'), ('hash2'), ('hash3')`); err != nil { @@ -253,7 +238,7 @@ func TestEmbeddedMigrationVersions(t *testing.T) { } func TestAppliedMatchesEmbedded(t *testing.T) { - st := newDoctorStore(t) + st := openTestDB(t) ctx := context.Background() applied, err := st.AppliedMigrationVersions(ctx)