Fix bd-166: Prevent duplicate issues with wrong prefix

Critical fix for silent data corruption where database created 173
duplicate issues with wrong prefix (beads- instead of bd-).

Root cause: When issue_prefix config was missing, CreateIssue fell
back to deriving prefix from database filename (beads.db → 'beads'),
while auto-import imported bd- issues from git with SkipPrefixValidation.
This created duplicates.

Changes:
1. Removed derivePrefixFromPath() - never derive prefix from filename
2. CreateIssue/CreateIssues now REJECT if issue_prefix config missing
   - Fail-fast with clear error message
3. Auto-import now SETS issue_prefix from first imported issue if missing
   - Handles fresh clone scenario safely
4. Added newTestStore() helper that sets issue_prefix for tests
5. Updated test setup in multiple files to prevent test failures

Follow-ups filed: bd-167, bd-168, bd-169

Closes bd-166

Amp-Thread-ID: https://ampcode.com/threads/T-b2ee0738-b90b-40ef-ae44-f2d93729842c
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-26 21:55:01 -07:00
parent 669c054332
commit 09e881022e
11 changed files with 108 additions and 40 deletions

View File

@@ -20,6 +20,10 @@ func setupTestStorage(t *testing.T) *sqlite.SQLiteStorage {
}
ctx := context.Background()
// CRITICAL (bd-166): Set issue_prefix to prevent "database not initialized" errors
if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
t.Fatalf("failed to set issue_prefix: %v", err)
}
if err := store.SetConfig(ctx, "compact_tier1_days", "0"); err != nil {
t.Fatalf("failed to set config: %v", err)
}

View File

@@ -38,6 +38,14 @@ func setupTestServer(t *testing.T) (*Server, *Client, func()) {
t.Fatalf("Failed to create store: %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()
os.RemoveAll(tmpDir)
t.Fatalf("Failed to set issue_prefix: %v", err)
}
server := NewServer(socketPath, store, tmpDir, dbPath)
ctx, cancel := context.WithCancel(context.Background())

View File

@@ -572,21 +572,9 @@ func (s *SQLiteStorage) SyncAllCounters(ctx context.Context) error {
return nil
}
// derivePrefixFromPath derives the issue prefix from the database file path
// Database file is named like ".beads/wy-.db" -> prefix should be "wy"
func derivePrefixFromPath(dbPath string) string {
dbFileName := filepath.Base(dbPath)
// Strip ".db" extension
dbFileName = strings.TrimSuffix(dbFileName, ".db")
// Strip trailing hyphen (if any)
prefix := strings.TrimSuffix(dbFileName, "-")
// Fallback if filename is weird
if prefix == "" {
prefix = "bd"
}
return prefix
}
// REMOVED (bd-166): derivePrefixFromPath was causing duplicate issues with wrong prefix
// The database should ALWAYS have issue_prefix config set explicitly (by 'bd init' or auto-import)
// Never derive prefix from filename - it leads to silent data corruption
// CreateIssue creates a new issue
func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, actor string) error {
@@ -635,8 +623,9 @@ func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, act
var prefix string
err := conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&prefix)
if err == sql.ErrNoRows || prefix == "" {
// Config not set - derive prefix from database filename
prefix = derivePrefixFromPath(s.dbPath)
// CRITICAL: Reject operation if issue_prefix config is missing (bd-166)
// This prevents duplicate issues with wrong prefix
return fmt.Errorf("database not initialized: issue_prefix config is missing (run 'bd init --prefix <prefix>' first)")
} else if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}
@@ -770,8 +759,8 @@ func generateBatchIDs(ctx context.Context, conn *sql.Conn, issues []*types.Issue
var prefix string
err := conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, "issue_prefix").Scan(&prefix)
if err == sql.ErrNoRows || prefix == "" {
// Config not set - derive prefix from database filename
prefix = derivePrefixFromPath(dbPath)
// CRITICAL: Reject operation if issue_prefix config is missing (bd-166)
return fmt.Errorf("database not initialized: issue_prefix config is missing (run 'bd init --prefix <prefix>' first)")
} else if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}

View File

@@ -27,6 +27,14 @@ func setupTestDB(t *testing.T) (*SQLiteStorage, func()) {
t.Fatalf("failed to create storage: %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()
os.RemoveAll(tmpDir)
t.Fatalf("failed to set issue_prefix: %v", err)
}
cleanup := func() {
store.Close()
os.RemoveAll(tmpDir)