From b27d00ac89f28b176996fbb2cc4bc821d3cb558c Mon Sep 17 00:00:00 2001 From: Juan Reyero Date: Fri, 9 Jan 2026 12:02:02 +0100 Subject: [PATCH] fix: sync --import-only fails with "no database store available" when daemon was connected When running `bd sync --import-only` while the daemon is connected, the command fails with "no database store available for inline import". Root cause: 1. PersistentPreRun connects to daemon and returns early without initializing the store global 2. sync command closes the daemon connection (for consistency) 3. sync --import-only calls importFromJSONLInline which requires store != nil 4. Without ensureStoreActive(), the store is never initialized after daemon disconnect Fix: Call ensureStoreActive() after closing the daemon connection in sync.go. This ensures the local SQLite store is initialized for all sync operations that need direct database access. - Add ensureStoreActive() call after daemon disconnect in sync.go - Add test documenting the bug and verifying the fix Co-Authored-By: Claude Opus 4.5 --- cmd/bd/direct_mode_test.go | 135 +++++++++++++++++++++++++++++++++++++ cmd/bd/sync.go | 8 +++ 2 files changed, 143 insertions(+) diff --git a/cmd/bd/direct_mode_test.go b/cmd/bd/direct_mode_test.go index e4a47662..1768618f 100644 --- a/cmd/bd/direct_mode_test.go +++ b/cmd/bd/direct_mode_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "encoding/json" "os" "path/filepath" "testing" @@ -139,3 +140,137 @@ func TestFallbackToDirectModeEnablesFlush(t *testing.T) { t.Fatalf("expected JSONL export to contain neighbor issue ID %s", neighbor.ID) } } + +// TestImportFromJSONLInlineAfterDaemonDisconnect verifies that importFromJSONLInline +// works after daemon disconnect when ensureStoreActive is called. +// +// This tests the fix for the bug where `bd sync --import-only` fails with +// "no database store available for inline import" when daemon mode was active. +// +// The bug occurs because: +// 1. PersistentPreRun connects to daemon and returns early (store = nil) +// 2. sync command closes daemon connection +// 3. sync --import-only calls importFromJSONLInline which requires store != nil +// 4. Without ensureStoreActive(), the store is never initialized +// +// The fix: call ensureStoreActive() after closing daemon in sync.go +func TestImportFromJSONLInlineAfterDaemonDisconnect(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Save and restore all global state + oldRootCtx := rootCtx + rootCtx = ctx + origDaemonClient := daemonClient + origDaemonStatus := daemonStatus + origStore := store + origStoreActive := storeActive + origDBPath := dbPath + origAutoImport := autoImportEnabled + + defer func() { + rootCtx = oldRootCtx + if store != nil && store != origStore { + _ = store.Close() + } + storeMutex.Lock() + store = origStore + storeActive = origStoreActive + storeMutex.Unlock() + daemonClient = origDaemonClient + daemonStatus = origDaemonStatus + dbPath = origDBPath + autoImportEnabled = origAutoImport + }() + + // Setup: Create temp directory with .beads structure + tmpDir := t.TempDir() + beadsDir := filepath.Join(tmpDir, ".beads") + if err := os.MkdirAll(beadsDir, 0o755); err != nil { + t.Fatalf("failed to create .beads dir: %v", err) + } + + testDBPath := filepath.Join(beadsDir, "beads.db") + jsonlPath := filepath.Join(beadsDir, "issues.jsonl") + + // Create and seed the database + setupStore := newTestStore(t, testDBPath) + issue := &types.Issue{ + Title: "Test Issue", + IssueType: types.TypeTask, + Priority: 2, + Status: types.StatusOpen, + } + if err := setupStore.CreateIssue(ctx, issue, "test"); err != nil { + t.Fatalf("failed to create issue: %v", err) + } + issueID := issue.ID + + // Export to JSONL + issues, err := setupStore.SearchIssues(ctx, "", types.IssueFilter{}) + if err != nil { + t.Fatalf("failed to search issues: %v", err) + } + f, err := os.Create(jsonlPath) + if err != nil { + t.Fatalf("failed to create JSONL: %v", err) + } + for _, iss := range issues { + data, _ := json.Marshal(iss) + f.Write(data) + f.Write([]byte("\n")) + } + f.Close() + + // Close setup store + if err := setupStore.Close(); err != nil { + t.Fatalf("failed to close setup store: %v", err) + } + + // Simulate daemon-connected state (as PersistentPreRun leaves it) + dbPath = testDBPath + storeMutex.Lock() + store = nil + storeActive = false + storeMutex.Unlock() + daemonClient = &rpc.Client{} // Non-nil means daemon was connected + autoImportEnabled = false + + // Simulate what sync.go does: close daemon but DON'T initialize store + // This is the bug scenario + _ = daemonClient.Close() + daemonClient = nil + + // BUG: Without ensureStoreActive(), importFromJSONLInline fails + err = importFromJSONLInline(ctx, jsonlPath, false, false) + if err == nil { + t.Fatal("expected importFromJSONLInline to fail when store is nil") + } + if err.Error() != "no database store available for inline import" { + t.Fatalf("unexpected error: %v", err) + } + + // FIX: Call ensureStoreActive() after daemon disconnect + if err := ensureStoreActive(); err != nil { + t.Fatalf("ensureStoreActive failed: %v", err) + } + + // Now importFromJSONLInline should work + err = importFromJSONLInline(ctx, jsonlPath, false, false) + if err != nil { + t.Fatalf("importFromJSONLInline failed after ensureStoreActive: %v", err) + } + + // Verify the import worked by checking the issue exists + storeMutex.Lock() + currentStore := store + storeMutex.Unlock() + + imported, err := currentStore.GetIssue(ctx, issueID) + if err != nil { + t.Fatalf("failed to get imported issue: %v", err) + } + if imported.Title != "Test Issue" { + t.Errorf("expected title 'Test Issue', got %q", imported.Title) + } +} diff --git a/cmd/bd/sync.go b/cmd/bd/sync.go index c75be5b3..b509d906 100644 --- a/cmd/bd/sync.go +++ b/cmd/bd/sync.go @@ -71,6 +71,14 @@ Use --merge to merge the sync branch back to main branch.`, daemonClient = nil } + // Initialize local store after daemon disconnect. + // When daemon was connected, PersistentPreRun returns early without initializing + // the store global. Commands like --import-only need the store, so we must + // initialize it here after closing the daemon connection. + if err := ensureStoreActive(); err != nil { + FatalError("failed to initialize store: %v", err) + } + // Resolve noGitHistory based on fromMain (fixes #417) noGitHistory = resolveNoGitHistoryForFromMain(fromMain, noGitHistory)