diff --git a/.beads/bd.jsonl b/.beads/bd.jsonl index 459e3dc4..d4c921ba 100644 --- a/.beads/bd.jsonl +++ b/.beads/bd.jsonl @@ -73,10 +73,11 @@ {"id":"bd-164","title":"Add migration tooling for database upgrades","description":"Create bd migrate command and auto-migration logic for version upgrades.\n\nImplementation:\n- bd migrate command (or bd init --migrate)\n- Auto-run on first command after daemon version upgrade\n- Detection logic:\n - Find all .db files in .beads/\n - Check schema version in each\n - Prompt to migrate/rename/delete\n- Migration operations:\n - Rename old database to beads.db\n - Update schema version metadata\n - Remove stale databases (with confirmation)\n- Could be part of daemon auto-start logic\n\nUser experience:\n$ bd ready\nDatabase schema mismatch detected.\n Found: vc.db (schema v0.16.0)\n Expected: beads.db (schema v0.17.5)\n \nRun 'bd migrate' to migrate automatically.\n\nBenefits:\n- Smooth upgrade path\n- Prevents confusion on version changes\n- Clean up stale databases\n\nDepends on:\n- Canonical naming (bd-160)\n- Schema versioning (bd-161)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-26T18:06:07.571855-07:00","updated_at":"2025-10-26T19:04:02.023089-07:00","closed_at":"2025-10-26T19:04:02.023089-07:00","dependencies":[{"issue_id":"bd-164","depends_on_id":"bd-159","type":"parent-child","created_at":"2025-10-26T18:06:07.573546-07:00","created_by":"daemon"},{"issue_id":"bd-164","depends_on_id":"bd-162","type":"blocks","created_at":"2025-10-26T18:06:17.327717-07:00","created_by":"daemon"},{"issue_id":"bd-164","depends_on_id":"bd-160","type":"blocks","created_at":"2025-10-26T18:06:17.351768-07:00","created_by":"daemon"}]} {"id":"bd-165","title":"Enforce canonical database name (beads.db)","description":"Always use beads.db as the canonical database name. Never auto-detect from multiple .db files.\n\nImplementation:\n- bd init always creates/uses beads.db\n- bd init detects and migrates old databases (vc.db → beads.db, bd.db → beads.db)\n- Daemon refuses to start if multiple .db files exist in .beads/ (exit with ambiguity error)\n- Update database discovery logic to prefer beads.db, error on ambiguity\n\nBenefits:\n- Prevents accidental use of stale databases\n- Clear single source of truth\n- Migration path for existing users","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-26T18:06:18.33827-07:00","updated_at":"2025-10-26T18:10:34.132537-07:00","closed_at":"2025-10-26T18:10:34.132537-07:00","dependencies":[{"issue_id":"bd-165","depends_on_id":"bd-159","type":"parent-child","created_at":"2025-10-26T18:06:18.339465-07:00","created_by":"daemon"}]} {"id":"bd-166","title":"bd import/sync created 173 duplicate issues with wrong prefix","description":"## What Happened\nDuring corruption recovery investigation (beads-173), discovered the database contained 338 issues instead of expected 165:\n- 165 issues with correct `bd-` prefix \n- 173 duplicate issues with `beads-` prefix (bd-1 → beads-1, etc.)\n- Database config was set to `beads` prefix instead of `bd`\n\n## Root Cause\nSome bd operation (likely import or sync) created duplicate issues with the wrong prefix. The database should have rejected or warned about prefix mismatch, but instead:\n1. Silently created duplicates with wrong prefix\n2. Changed database prefix config from `bd` to `beads`\n\n## Impact\n- **Data integrity violation**: Duplicate issues with different IDs\n- **Silent corruption**: No error or warning during creation \n- **Wrong prefix**: Database config changed without user consent\n- **Confusion**: Users see double the issues, dependencies broken\n\n## Recovery \nHad to manually fix the `issue_prefix` config key (not `prefix` as initially thought):\n```bash\nsqlite3 .beads/beads.db \"UPDATE config SET value = 'bd' WHERE key = 'issue_prefix';\"\nsqlite3 .beads/beads.db \"DELETE FROM issues WHERE id LIKE 'beads-%';\"\n```\n\n## What Should Happen\n1. **Reject prefix mismatch**: If importing issues with different prefix than configured, error or require `--rename-on-import`\n2. **Never auto-change prefix**: Database prefix should only change via explicit `bd rename-prefix` command \n3. **Validate on import**: Check imported issue IDs match configured prefix before creating\n4. **Warn on duplicate IDs**: Even with different prefixes, detect potential duplicates\n\n## Related\n- Discovered during beads-173 (database corruption investigation)\n- Similar to existing prefix validation in `bd sync` (bd-21)","notes":"## Root Cause Analysis\n\nFound the smoking gun! The bug is a combination of three factors:\n\n**1. Database filename-based prefix fallback (sqlite.go:577-589)**\n```go\nfunc derivePrefixFromPath(dbPath string) string {\n dbFileName := filepath.Base(dbPath)\n dbFileName = strings.TrimSuffix(dbFileName, \".db\")\n prefix := strings.TrimSuffix(dbFileName, \"-\")\n if prefix == \"\" {\n prefix = \"bd\"\n }\n return prefix\n}\n```\nWhen `issue_prefix` config is missing, CreateIssue uses database filename (beads.db → \"beads\" prefix).\n\n**2. Auto-import skips prefix validation (autoimport.go:175)**\n```go\nopts := ImportOptions{\n ResolveCollisions: true,\n SkipPrefixValidation: true, // Auto-import is lenient about prefixes\n}\n```\nThis allows importing bd- issues without error even when config says \"beads\".\n\n**3. Missing issue_prefix config**\nSomehow the database lost its `issue_prefix = 'bd'` config (should be set by `bd init --prefix bd`), triggering the fallback.\n\n**Timeline:**\n1. Database exists with bd- issues\n2. `issue_prefix` config gets lost (corruption? manual deletion? reinit?)\n3. Auto-import brings in bd- issues from git (SkipPrefixValidation allows this)\n4. New issue creation falls back to filename → uses \"beads\" prefix\n5. Result: 165 bd- issues + 173 beads- duplicates\n\n**Fixes implemented:**\n1. ✅ Removed derivePrefixFromPath - never derive prefix from filename\n2. ✅ CreateIssue now REJECTS operations if issue_prefix is missing (hard error)\n3. ✅ Auto-import now SETS issue_prefix from first imported issue if missing\n4. ✅ Updated all test helpers to set issue_prefix during database initialization\n\nThis prevents silent corruption - if issue_prefix is missing, operations fail loudly instead of creating duplicate issues.","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-10-26T21:38:39.096165-07:00","updated_at":"2025-10-26T21:51:44.171474-07:00","closed_at":"2025-10-26T21:51:44.171474-07:00"} -{"id":"bd-167","title":"Complete test fixes for bd-166 (missing issue_prefix)","description":"Several test files still fail with \"database not initialized: issue_prefix config is missing\" error:\n\n- cmd/bd/comments_test.go\n- cmd/bd/export_test.go \n- cmd/bd/git_sync_test.go\n- cmd/bd/label_test.go\n- cmd/bd/list_test.go\n- cmd/bd/reopen_test.go\n- cmd/bd/direct_mode_test.go\n\nAll need to either:\n1. Use the new `newTestStore(t, dbPath)` helper, or\n2. Call `store.SetConfig(ctx, \"issue_prefix\", \"bd\")` after `sqlite.New()`\n\nPattern to fix:\n```go\n// Old:\nstore, err := sqlite.New(dbPath)\n\n// New:\nstore := newTestStore(t, dbPath)\n```","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-26T21:54:36.625952-07:00","updated_at":"2025-10-26T21:54:36.625952-07:00","dependencies":[{"issue_id":"bd-167","depends_on_id":"bd-166","type":"discovered-from","created_at":"2025-10-26T21:54:41.993277-07:00","created_by":"daemon"}]} +{"id":"bd-167","title":"Complete test fixes for bd-166 (missing issue_prefix)","description":"Several test files still fail with \"database not initialized: issue_prefix config is missing\" error:\n\n- cmd/bd/comments_test.go\n- cmd/bd/export_test.go \n- cmd/bd/git_sync_test.go\n- cmd/bd/label_test.go\n- cmd/bd/list_test.go\n- cmd/bd/reopen_test.go\n- cmd/bd/direct_mode_test.go\n\nAll need to either:\n1. Use the new `newTestStore(t, dbPath)` helper, or\n2. Call `store.SetConfig(ctx, \"issue_prefix\", \"bd\")` after `sqlite.New()`\n\nPattern to fix:\n```go\n// Old:\nstore, err := sqlite.New(dbPath)\n\n// New:\nstore := newTestStore(t, dbPath)\n```","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-26T21:54:36.625952-07:00","updated_at":"2025-10-26T22:07:15.853197-07:00","closed_at":"2025-10-26T22:07:15.853197-07:00","dependencies":[{"issue_id":"bd-167","depends_on_id":"bd-166","type":"discovered-from","created_at":"2025-10-26T21:54:41.993277-07:00","created_by":"daemon"}]} {"id":"bd-168","title":"Add bd doctor command to detect/fix missing issue_prefix","description":"Create a `bd doctor` command that validates database health and offers to fix common issues.\n\n**Phase 1: Detect missing issue_prefix**\n```bash\nbd doctor\n# Output:\n# ✗ issue_prefix not configured\n# Database has 165 issues with prefix \"bd\"\n# Run: bd doctor --fix-prefix bd\n```\n\n**Phase 2: Auto-fix**\n```bash\nbd doctor --fix-prefix bd\n# Sets issue_prefix config to \"bd\"\n# Or derive from existing issues if --fix-prefix not specified\n```\n\n**Phase 3: Other checks (future)**\n- Orphaned dependencies\n- Counter mismatches \n- Corrupted timestamps\n- Duplicate issues with different prefixes\n\nThis helps users recover from bd-166 corruption without manual SQL.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-26T21:54:36.634246-07:00","updated_at":"2025-10-26T21:54:36.634246-07:00","dependencies":[{"issue_id":"bd-168","depends_on_id":"bd-166","type":"discovered-from","created_at":"2025-10-26T21:54:41.994194-07:00","created_by":"daemon"}]} {"id":"bd-169","title":"Add test for CreateIssue with missing issue_prefix","description":"Add explicit test case that verifies CreateIssue fails correctly when issue_prefix config is missing.\n\n**Test:**\n```go\nfunc TestCreateIssue_MissingPrefix(t *testing.T) {\n store, cleanup := setupTestDB(t)\n defer cleanup()\n \n ctx := context.Background()\n \n // Clear the issue_prefix config\n err := store.SetConfig(ctx, \"issue_prefix\", \"\")\n require.NoError(t, err)\n \n // Attempt to create issue should fail\n issue := \u0026types.Issue{\n Title: \"Test issue\",\n Status: types.StatusOpen,\n Priority: 1,\n IssueType: types.TypeTask,\n }\n \n err = store.CreateIssue(ctx, issue, \"test\")\n require.Error(t, err)\n assert.Contains(t, err.Error(), \"database not initialized\")\n assert.Contains(t, err.Error(), \"issue_prefix config is missing\")\n}\n```\n\nThis ensures the fix for bd-166 doesn't regress.","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-26T21:54:36.63521-07:00","updated_at":"2025-10-26T21:54:36.63521-07:00","dependencies":[{"issue_id":"bd-169","depends_on_id":"bd-166","type":"discovered-from","created_at":"2025-10-26T21:54:41.995525-07:00","created_by":"daemon"}]} {"id":"bd-17","title":"Update EXTENDING.md with UnderlyingDB() usage and best practices","description":"EXTENDING.md currently shows how to use direct sql.Open() to access the database, but doesn't mention the new UnderlyingDB() method that's the recommended way for extensions.\n\n**Update needed:**\n1. Add section showing UnderlyingDB() usage:\n ```go\n store, err := beads.NewSQLiteStorage(dbPath)\n db := store.UnderlyingDB()\n // Create extension tables using db\n ```\n\n2. Document when to use UnderlyingDB() vs direct sql.Open():\n - Use UnderlyingDB() when you want to share the storage connection\n - Use sql.Open() when you need independent connection management\n\n3. Add safety warnings (cross-reference from UnderlyingDB() docs):\n - Don't close the DB\n - Don't modify pool settings\n - Keep transactions short\n\n4. Update the VC example to show UnderlyingDB() pattern\n\n5. Explain beads.Storage.UnderlyingDB() in the API section","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-22T17:07:56.820056-07:00","updated_at":"2025-10-25T23:15:33.478579-07:00","closed_at":"2025-10-22T19:41:19.895847-07:00","dependencies":[{"issue_id":"bd-17","depends_on_id":"bd-10","type":"discovered-from","created_at":"2025-10-24T13:17:40.32522-07:00","created_by":"renumber"}]} +{"id":"bd-170","title":"Clean up beads-* duplicate issues and review corrupt backup for missing issues","description":"## Current State\n- Database has 3 duplicate beads-* issues (beads-2, beads-3, beads-4) that should be deleted\n- Have corrupt backup: `.beads/beads.db.corrupt-backup` (4.4MB) from bd-166 corruption incident\n- Current clean DB has 172 issues (155 closed, 14 open, 3 in_progress)\n\n## Tasks\n1. **Delete beads-* duplicates** - these are corrupted duplicates from bd-166\n ```bash\n sqlite3 .beads/beads.db \"DELETE FROM issues WHERE id LIKE 'beads-%';\"\n ```\n\n2. **Review corrupt backup for missing issues**\n - Open corrupt backup: `sqlite3 .beads/beads.db.corrupt-backup`\n - Compare issue counts: backup had ~338 issues (165 bd- + 173 beads- duplicates)\n - Check if any ~8 issues exist in backup that are NOT in current DB\n - Cherry-pick any legitimate issues that were lost during cleanup\n\n3. **Verification**\n - Compare issue IDs between corrupt backup and current DB\n - Identify any missing issues worth recovering\n - Document findings\n\n## Why P0\nThis blocks clean database state and may contain lost work from the corruption incident.","status":"closed","priority":0,"issue_type":"task","created_at":"2025-10-26T22:30:00.126524-07:00","updated_at":"2025-10-26T22:30:35.01995-07:00","closed_at":"2025-10-26T22:30:35.01995-07:00"} {"id":"bd-18","title":"Consider adding UnderlyingConn(ctx) for safer scoped DB access","description":"Currently UnderlyingDB() returns *sql.DB which is correct for most uses, but for extension migrations/DDL, a scoped connection might be safer.\n\n**Proposal:** Add optional UnderlyingConn(ctx) (*sql.Conn, error) method that:\n- Returns a scoped connection via s.db.Conn(ctx)\n- Encourages lifetime-bounded usage\n- Reduces temptation to tune global pool settings\n- Better for one-time DDL operations like CREATE TABLE\n\n**Implementation:**\n```go\n// UnderlyingConn returns a single connection from the pool for scoped use\n// Useful for migrations and DDL. Close the connection when done.\nfunc (s *SQLiteStorage) UnderlyingConn(ctx context.Context) (*sql.Conn, error) {\n return s.db.Conn(ctx)\n}\n```\n\n**Benefits:**\n- Safer for migrations (explicit scope)\n- Complements UnderlyingDB() for different use cases\n- Low implementation cost\n\n**Trade-off:** Adds another method to maintain, but Oracle considers this balanced compromise between safety and flexibility.\n\n**Decision:** This is optional - evaluate based on VC's actual usage patterns.","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-10-22T17:07:56.832638-07:00","updated_at":"2025-10-25T23:15:33.479496-07:00","closed_at":"2025-10-22T22:02:18.479512-07:00","dependencies":[{"issue_id":"bd-18","depends_on_id":"bd-10","type":"related","created_at":"2025-10-24T13:17:40.325463-07:00","created_by":"renumber"}]} {"id":"bd-19","title":"MCP close tool method signature error - takes 1 positional argument but 2 were given","description":"The close approval routing fix in beads-mcp v0.11.0 works correctly and successfully routes update(status=\"closed\") calls to close() tool. However, the close() tool has a Python method signature bug that prevents execution.\n\nImpact: All MCP-based close operations are broken. Workaround: Use bd CLI directly.\n\nError: BdDaemonClient.close() takes 1 positional argument but 2 were given\n\nRoot cause: BdDaemonClient.close() only accepts self, but MCP tool passes issue_id and reason.\n\nAdditional issue: CLI close has FOREIGN KEY constraint error when recording reason parameter.\n\nSee GitHub issue #107 for full details.","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-10-22T17:25:34.67056-07:00","updated_at":"2025-10-25T23:15:33.480292-07:00","closed_at":"2025-10-22T17:36:55.463445-07:00"} {"id":"bd-2","title":"Improve error handling in dependency removal during remapping","description":"In updateDependencyReferences(), RemoveDependency errors are caught and ignored with continue (line 392). Comment says 'if dependency doesn't exist' but this catches ALL errors including real failures. Should check error type with errors.Is(err, ErrDependencyNotFound) and only ignore not-found errors, returning other errors properly.","status":"closed","priority":3,"issue_type":"bug","created_at":"2025-10-21T23:53:44.31362-07:00","updated_at":"2025-10-25T23:15:33.462194-07:00","closed_at":"2025-10-18T09:41:18.209717-07:00"} @@ -167,6 +168,3 @@ {"id":"bd-97","title":"Counter not synced after import on existing DB with populated issue_counters table","description":"The counter sync fix in counter_sync_test.go only syncs during initial migration when issue_counters table is empty (migrateIssueCountersTable checks count==0). For existing databases with stale counters:\n\n- Import doesn't resync the counter\n- Delete doesn't update counter \n- Renumber doesn't fix counter\n- Counter remains stuck at old high value\n\nExample from today:\n- Had 49 issues after clean import\n- Counter stuck at 4106 from previous test pollution\n- Next issue would be bd-4107 instead of bd-12\n- Even after renumber, counter stayed at 4106\n\nRoot cause: Migration only syncs if table is empty (line 182 in sqlite.go). Once populated, never resyncs.\n\nFix needed: \n1. Sync counter after import operations (not just empty table)\n2. Add counter resync after renumber\n3. Daemon caches counter value - needs to reload after external changes\n\nRelated: bd-8 (original counter sync fix), bd-9 (daemon cache staleness)","notes":"## Investigation Results\n\nAfter thorough code review, all the fixes mentioned in the issue description have ALREADY been implemented:\n\n### ✅ Fixes Already in Place:\n\n1. **Import DOES resync counters**\n - `cmd/bd/import_shared.go:253` calls `SyncAllCounters()` after batch import\n - Verified with new test `TestCounterSyncAfterImport`\n\n2. **Delete DOES update counters**\n - `internal/storage/sqlite/sqlite.go:1424` calls `SyncAllCounters()` after deletion\n - Both single delete and batch delete sync properly\n - Verified with existing tests: `TestCounterSyncAfterDelete`, `TestCounterSyncAfterBatchDelete`\n\n3. **Renumber DOES fix counters**\n - `cmd/bd/renumber.go:298-304` calls `ResetCounter()` then `SyncAllCounters()`\n - Forces counter to actual max ID (not just MAX with stale value)\n\n4. **Daemon cache DOES detect external changes**\n - `internal/rpc/server.go:1466-1487` checks file mtime and evicts stale cache\n - When DB file changes externally, cached storage is evicted and reopened\n\n### Tests Added:\n\n- `TestCounterSyncAfterImport`: Confirms import syncs counters from stale value (4106) to actual max (49)\n- `TestCounterNotSyncedWithoutExplicitSync`: Documents what would happen without the fix (bd-4107 instead of bd-12)\n\n### Conclusion:\n\nThe issue described in bd-12 has been **fully resolved**. All operations (import, delete, renumber) now properly sync counters. The daemon correctly detects external DB changes via file modification time.\n\nThe root cause (migration only syncing empty tables) was fixed by adding explicit `SyncAllCounters()` calls after import, delete, and renumber operations.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-10-24T13:35:23.110118-07:00","updated_at":"2025-10-25T23:15:33.522231-07:00","closed_at":"2025-10-22T00:03:46.697918-07:00"} {"id":"bd-98","title":"Re-land TestDatabaseReinitialization after fixing Windows/Nix issues","description":"TestDatabaseReinitialization test was reverted due to CI failures:\n- Windows: JSON parse errors, missing files \n- Nix: git not available in build environment\n\nNeed to fix and re-land:\n1. Make test work on Windows (path separators, file handling)\n2. Skip test in Nix environment or mock git\n3. Fix JSON parsing issues","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-24T15:06:27.385396-07:00","updated_at":"2025-10-25T23:15:33.508271-07:00","closed_at":"2025-10-25T17:48:52.214323-07:00"} {"id":"bd-99","title":"Feature: Use external_ref as primary matching key for import updates","description":"Implement external_ref-based matching for imports to enable hybrid workflows with external systems (Jira, GitHub, Linear).\n\n## Problem\nCurrent import collision detection treats any content change as a collision, preventing users from syncing updates from external systems without creating duplicates.\n\n## Solution\nUse external_ref field as primary matching key during imports. When an incoming issue has external_ref set:\n- Search for existing issue with same external_ref\n- If found, UPDATE (not collision)\n- If not found, create new issue\n- Never match local issues (without external_ref)\n\n## Use Cases\n- Jira integration: Import backlog, add local tasks, re-sync updates\n- GitHub integration: Import issues, track with local subtasks, sync status\n- Linear integration: Team coordination with local breakdown\n\n## Reference\nGitHub issue #142: https://github.com/steveyegge/beads/issues/142","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-10-24T22:10:24.862547-07:00","updated_at":"2025-10-25T23:15:33.508456-07:00","closed_at":"2025-10-25T10:17:33.543504-07:00"} -{"id":"beads-2","title":"bd import/sync created 173 duplicate issues with wrong prefix (beads- instead of bd-)","description":"## What Happened\nDuring corruption recovery investigation (beads-173), discovered the database contained 338 issues instead of expected 165:\n- 165 issues with correct `bd-` prefix\n- 173 duplicate issues with `beads-` prefix (bd-1 → beads-1, etc.)\n- Database config was set to `beads-` prefix instead of `bd-`\n\n## Root Cause\nSome bd operation (likely import or sync) created duplicate issues with the wrong prefix. The database should have rejected or warned about prefix mismatch, but instead:\n1. Silently created duplicates with wrong prefix\n2. Changed database prefix config from `bd-` to `beads-`\n\n## Impact\n- **Data integrity violation**: Duplicate issues with different IDs\n- **Silent corruption**: No error or warning during creation\n- **Wrong prefix**: Database config changed without user consent\n- **Confusion**: Users see double the issues, dependencies broken\n\n## Recovery\nHad to manually:\n```bash\n# Delete duplicates\nsqlite3 .beads/beads.db \"DELETE FROM dependencies WHERE issue_id LIKE 'beads-%' OR depends_on_id LIKE 'beads-%'; DELETE FROM issues WHERE id LIKE 'beads-%';\"\n\n# Fix prefix config\nsqlite3 .beads/beads.db \"UPDATE config SET value = 'bd-' WHERE key = 'prefix';\"\n```\n\n## What Should Happen\n1. **Reject prefix mismatch**: If importing issues with different prefix than configured, error or require `--rename-on-import`\n2. **Never auto-change prefix**: Database prefix should only change via explicit `bd rename-prefix` command\n3. **Validate on import**: Check imported issue IDs match configured prefix before creating\n4. **Warn on duplicate IDs**: Even with different prefixes, detect potential duplicates\n\n## Related\n- Discovered during beads-173 (database corruption investigation)\n- Similar to existing prefix validation in `bd sync` (bd-21)","status":"open","priority":0,"issue_type":"bug","created_at":"2025-10-26T21:38:17.31308-07:00","updated_at":"2025-10-26T21:38:17.31308-07:00"} -{"id":"beads-3","title":"bd import/sync created 173 duplicate issues with wrong prefix (beads- instead of bd-)","description":"","status":"open","priority":0,"issue_type":"bug","created_at":"2025-10-26T21:38:24.300198-07:00","updated_at":"2025-10-26T21:38:24.300198-07:00"} -{"id":"beads-4","title":"bd import/sync created 173 duplicate issues with wrong prefix","description":"","status":"open","priority":0,"issue_type":"bug","created_at":"2025-10-26T21:38:33.52121-07:00","updated_at":"2025-10-26T21:38:33.52121-07:00"} diff --git a/.beads/beads.db.corrupt-backup b/.beads/beads.db.corrupt-backup deleted file mode 100644 index eb8eb9ef..00000000 Binary files a/.beads/beads.db.corrupt-backup and /dev/null differ diff --git a/cmd/bd/comments_test.go b/cmd/bd/comments_test.go index 7abf3942..09eee453 100644 --- a/cmd/bd/comments_test.go +++ b/cmd/bd/comments_test.go @@ -7,7 +7,6 @@ import ( "path/filepath" "testing" - "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/types" ) @@ -21,10 +20,7 @@ func TestCommentsCommand(t *testing.T) { defer os.RemoveAll(tmpDir) testDB := filepath.Join(tmpDir, "test.db") - s, err := sqlite.New(testDB) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } + s := newTestStore(t, testDB) defer s.Close() ctx := context.Background() diff --git a/cmd/bd/direct_mode_test.go b/cmd/bd/direct_mode_test.go index 25c12b6a..4b1758a4 100644 --- a/cmd/bd/direct_mode_test.go +++ b/cmd/bd/direct_mode_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/steveyegge/beads/internal/rpc" - "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/types" ) @@ -67,10 +66,7 @@ func TestFallbackToDirectModeEnablesFlush(t *testing.T) { testDBPath := filepath.Join(beadsDir, "test.db") // Seed database with issues - setupStore, err := sqlite.New(testDBPath) - if err != nil { - t.Fatalf("failed to create seed store: %v", err) - } + setupStore := newTestStore(t, testDBPath) ctx := context.Background() target := &types.Issue{ diff --git a/cmd/bd/export_test.go b/cmd/bd/export_test.go index fd7d7493..49e6b3a7 100644 --- a/cmd/bd/export_test.go +++ b/cmd/bd/export_test.go @@ -8,7 +8,6 @@ import ( "path/filepath" "testing" - "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/types" ) @@ -22,10 +21,7 @@ func TestExportCommand(t *testing.T) { defer os.RemoveAll(tmpDir) testDB := filepath.Join(tmpDir, "test.db") - s, err := sqlite.New(testDB) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } + s := newTestStore(t, testDB) defer s.Close() ctx := context.Background() @@ -228,10 +224,7 @@ func TestExportCommand(t *testing.T) { // Create empty database emptyDBPath := filepath.Join(tmpDir, "empty.db") - emptyStore, err := sqlite.New(emptyDBPath) - if err != nil { - t.Fatalf("Failed to create empty store: %v", err) - } + emptyStore := newTestStore(t, emptyDBPath) defer emptyStore.Close() // Test using exportToJSONLWithStore directly (daemon code path) diff --git a/cmd/bd/git_sync_test.go b/cmd/bd/git_sync_test.go index f2cf0de7..9cb1436a 100644 --- a/cmd/bd/git_sync_test.go +++ b/cmd/bd/git_sync_test.go @@ -45,10 +45,7 @@ func TestGitPullSyncIntegration(t *testing.T) { } clone1DBPath := filepath.Join(clone1BeadsDir, "test.db") - clone1Store, err := sqlite.New(clone1DBPath) - if err != nil { - t.Fatalf("Failed to create clone1 database: %v", err) - } + clone1Store := newTestStore(t, clone1DBPath) defer clone1Store.Close() ctx := context.Background() @@ -94,10 +91,7 @@ func TestGitPullSyncIntegration(t *testing.T) { // Initialize empty database in clone2 clone2BeadsDir := filepath.Join(clone2Dir, ".beads") clone2DBPath := filepath.Join(clone2BeadsDir, "test.db") - clone2Store, err := sqlite.New(clone2DBPath) - if err != nil { - t.Fatalf("Failed to create clone2 database: %v", err) - } + clone2Store := newTestStore(t, clone2DBPath) defer clone2Store.Close() if err := clone2Store.SetMetadata(ctx, "issue_prefix", "test"); err != nil { @@ -141,10 +135,7 @@ func TestGitPullSyncIntegration(t *testing.T) { // In real usage, auto-import would trigger on next bd command // For this test, we'll manually import to simulate that behavior - newStore, err := sqlite.New(clone2DBPath) - if err != nil { - t.Fatalf("Failed to reopen database: %v", err) - } + newStore := newTestStore(t, clone2DBPath) // Don't defer close - we'll reassign to clone2Store for the next test // Manually import to simulate auto-import behavior diff --git a/cmd/bd/label_test.go b/cmd/bd/label_test.go index 59135639..4800e059 100644 --- a/cmd/bd/label_test.go +++ b/cmd/bd/label_test.go @@ -123,10 +123,7 @@ func TestLabelCommands(t *testing.T) { defer os.RemoveAll(tmpDir) testDB := filepath.Join(tmpDir, "test.db") - s, err := sqlite.New(testDB) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } + s := newTestStore(t, testDB) defer s.Close() ctx := context.Background() diff --git a/cmd/bd/list_test.go b/cmd/bd/list_test.go index 99ed1402..6353aa34 100644 --- a/cmd/bd/list_test.go +++ b/cmd/bd/list_test.go @@ -98,10 +98,7 @@ func TestListCommand(t *testing.T) { defer os.RemoveAll(tmpDir) testDB := filepath.Join(tmpDir, "test.db") - s, err := sqlite.New(testDB) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } + s := newTestStore(t, testDB) defer s.Close() h := newListTestHelper(t, s) diff --git a/cmd/bd/reopen_test.go b/cmd/bd/reopen_test.go index a5b3def8..c6210cbb 100644 --- a/cmd/bd/reopen_test.go +++ b/cmd/bd/reopen_test.go @@ -101,10 +101,7 @@ func TestReopenCommand(t *testing.T) { defer os.RemoveAll(tmpDir) testDB := filepath.Join(tmpDir, "test.db") - s, err := sqlite.New(testDB) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } + s := newTestStore(t, testDB) defer s.Close() ctx := context.Background() diff --git a/internal/config/config_test.go b/internal/config/config_test.go index d74ffd89..51310b28 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -37,7 +37,7 @@ func TestDefaults(t *testing.T) { {"no-auto-import", false, func(k string) interface{} { return GetBool(k) }}, {"db", "", func(k string) interface{} { return GetString(k) }}, {"actor", "", func(k string) interface{} { return GetString(k) }}, - {"flush-debounce", 5 * time.Second, func(k string) interface{} { return GetDuration(k) }}, + {"flush-debounce", 30 * time.Second, func(k string) interface{} { return GetDuration(k) }}, {"auto-start-daemon", true, func(k string) interface{} { return GetBool(k) }}, } diff --git a/internal/rpc/rpc_test.go b/internal/rpc/rpc_test.go index 8a94a858..8dc9842b 100644 --- a/internal/rpc/rpc_test.go +++ b/internal/rpc/rpc_test.go @@ -435,10 +435,7 @@ func TestDatabaseHandshake(t *testing.T) { os.MkdirAll(beadsDir1, 0750) dbPath1 := filepath.Join(beadsDir1, "db1.db") socketPath1 := filepath.Join(beadsDir1, "bd.sock") - store1, err := sqlitestorage.New(dbPath1) - if err != nil { - t.Fatalf("Failed to create store 1: %v", err) - } + store1 := newTestStore(t, dbPath1) defer store1.Close() server1 := NewServer(socketPath1, store1, tmpDir1, dbPath1) @@ -453,10 +450,7 @@ func TestDatabaseHandshake(t *testing.T) { os.MkdirAll(beadsDir2, 0750) dbPath2 := filepath.Join(beadsDir2, "db2.db") socketPath2 := filepath.Join(beadsDir2, "bd.sock") - store2, err := sqlitestorage.New(dbPath2) - if err != nil { - t.Fatalf("Failed to create store 2: %v", err) - } + store2 := newTestStore(t, dbPath2) defer store2.Close() server2 := NewServer(socketPath2, store2, tmpDir2, dbPath2) diff --git a/internal/rpc/test_helpers.go b/internal/rpc/test_helpers.go new file mode 100644 index 00000000..f32da07e --- /dev/null +++ b/internal/rpc/test_helpers.go @@ -0,0 +1,28 @@ +package rpc + +import ( + "context" + "testing" + + "github.com/steveyegge/beads/internal/storage/sqlite" +) + +// newTestStore creates a SQLite store with issue_prefix configured (bd-166) +// This prevents "database not initialized" errors in tests +func newTestStore(t *testing.T, dbPath string) *sqlite.SQLiteStorage { + t.Helper() + + store, err := sqlite.New(dbPath) + if err != nil { + t.Fatalf("Failed to create test database: %v", err) + } + + // CRITICAL (bd-166): Set issue_prefix to prevent "database not initialized" errors + ctx := context.Background() + if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil { + store.Close() + t.Fatalf("Failed to set issue_prefix: %v", err) + } + + return store +} diff --git a/internal/rpc/version_test.go b/internal/rpc/version_test.go index 7ff490b2..a2f5c8dc 100644 --- a/internal/rpc/version_test.go +++ b/internal/rpc/version_test.go @@ -85,10 +85,7 @@ func TestVersionCompatibility(t *testing.T) { tmpDir, _, dbPath, socketPath, cleanup := setupTestServerIsolated(t) defer cleanup() - store, err := sqlitestorage.New(dbPath) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } + store := newTestStore(t, dbPath) defer store.Close() // Override server version diff --git a/internal/storage/sqlite/sqlite_test.go b/internal/storage/sqlite/sqlite_test.go index 701b3d1b..fbd26904 100644 --- a/internal/storage/sqlite/sqlite_test.go +++ b/internal/storage/sqlite/sqlite_test.go @@ -1301,6 +1301,11 @@ func TestInMemoryDatabase(t *testing.T) { } defer store.Close() + // Set issue_prefix (bd-166) + if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil { + t.Fatalf("failed to set issue_prefix: %v", err) + } + // Verify we can create and retrieve an issue issue := &types.Issue{ Title: "Test in-memory issue", @@ -1344,6 +1349,11 @@ func TestInMemorySharedCache(t *testing.T) { } defer store1.Close() + // Set issue_prefix (bd-166) + if err := store1.SetConfig(ctx, "issue_prefix", "bd"); err != nil { + t.Fatalf("failed to set issue_prefix: %v", err) + } + // Create an issue in the first connection issue := &types.Issue{ Title: "Shared memory test", diff --git a/internal/storage/sqlite/test_helpers.go b/internal/storage/sqlite/test_helpers.go new file mode 100644 index 00000000..60a4bcf9 --- /dev/null +++ b/internal/storage/sqlite/test_helpers.go @@ -0,0 +1,26 @@ +package sqlite + +import ( + "context" + "testing" +) + +// newTestStore creates a SQLiteStorage with issue_prefix configured (bd-166) +// This prevents "database not initialized" errors in tests +func newTestStore(t *testing.T, dbPath string) *SQLiteStorage { + t.Helper() + + store, err := New(dbPath) + if err != nil { + t.Fatalf("Failed to create test database: %v", err) + } + + // CRITICAL (bd-166): Set issue_prefix to prevent "database not initialized" errors + ctx := context.Background() + if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil { + store.Close() + t.Fatalf("Failed to set issue_prefix: %v", err) + } + + return store +} diff --git a/internal/storage/sqlite/underlying_db_test.go b/internal/storage/sqlite/underlying_db_test.go index d0a388f3..448a2a27 100644 --- a/internal/storage/sqlite/underlying_db_test.go +++ b/internal/storage/sqlite/underlying_db_test.go @@ -20,10 +20,7 @@ func TestUnderlyingDB_BasicAccess(t *testing.T) { defer os.RemoveAll(tmpDir) dbPath := filepath.Join(tmpDir, "test.db") - store, err := New(dbPath) - if err != nil { - t.Fatalf("Failed to create storage: %v", err) - } + store := newTestStore(t, dbPath) defer store.Close() // Get underlying DB @@ -53,10 +50,7 @@ func TestUnderlyingDB_CreateExtensionTable(t *testing.T) { defer os.RemoveAll(tmpDir) dbPath := filepath.Join(tmpDir, "test.db") - store, err := New(dbPath) - if err != nil { - t.Fatalf("Failed to create storage: %v", err) - } + store := newTestStore(t, dbPath) defer store.Close() ctx := context.Background() @@ -145,10 +139,7 @@ func TestUnderlyingDB_ConcurrentAccess(t *testing.T) { defer os.RemoveAll(tmpDir) dbPath := filepath.Join(tmpDir, "test.db") - store, err := New(dbPath) - if err != nil { - t.Fatalf("Failed to create storage: %v", err) - } + store := newTestStore(t, dbPath) defer store.Close() ctx := context.Background() @@ -243,10 +234,7 @@ func TestUnderlyingDB_LongTxDoesNotDeadlock(t *testing.T) { defer os.RemoveAll(tmpDir) dbPath := filepath.Join(tmpDir, "test.db") - store, err := New(dbPath) - if err != nil { - t.Fatalf("Failed to create storage: %v", err) - } + store := newTestStore(t, dbPath) defer store.Close() ctx := context.Background() @@ -297,10 +285,7 @@ func TestUnderlyingConn_BasicAccess(t *testing.T) { defer os.RemoveAll(tmpDir) dbPath := filepath.Join(tmpDir, "test.db") - store, err := New(dbPath) - if err != nil { - t.Fatalf("Failed to create storage: %v", err) - } + store := newTestStore(t, dbPath) defer store.Close() ctx := context.Background() @@ -333,10 +318,7 @@ func TestUnderlyingConn_DDLOperations(t *testing.T) { defer os.RemoveAll(tmpDir) dbPath := filepath.Join(tmpDir, "test.db") - store, err := New(dbPath) - if err != nil { - t.Fatalf("Failed to create storage: %v", err) - } + store := newTestStore(t, dbPath) defer store.Close() ctx := context.Background() @@ -417,10 +399,7 @@ func TestUnderlyingConn_ContextCancellation(t *testing.T) { defer os.RemoveAll(tmpDir) dbPath := filepath.Join(tmpDir, "test.db") - store, err := New(dbPath) - if err != nil { - t.Fatalf("Failed to create storage: %v", err) - } + store := newTestStore(t, dbPath) defer store.Close() // Create a context that's already canceled @@ -444,10 +423,7 @@ func TestUnderlyingConn_MultipleConnections(t *testing.T) { defer os.RemoveAll(tmpDir) dbPath := filepath.Join(tmpDir, "test.db") - store, err := New(dbPath) - if err != nil { - t.Fatalf("Failed to create storage: %v", err) - } + store := newTestStore(t, dbPath) defer store.Close() ctx := context.Background()