fix: Code review fixes for auto-migration (bd-jgxi)

Critical fixes from code review:

1. **Moved auto-migration to correct location**
   - Now runs AFTER daemon check but BEFORE opening database
   - Prevents: database opened twice, conflicts with daemon
   - Was: Running too early, before knowing if daemon exists

2. **Fixed context cancellation issue**
   - Check if rootCtx is canceled before using it
   - Fall back to Background() if canceled
   - Fixes: "context canceled" errors in test suite

3. **Updated function signature**
   - Takes dbPath as parameter (no longer searches for it)
   - Simpler, more explicit, easier to test
   - Caller already has dbPath, no need to re-discover

4. **Enhanced test reliability**
   - Save/restore all global state
   - Add debug logging for troubleshooting
   - Verify preconditions before migration

Changes:
- cmd/bd/main.go: Move autoMigrateOnVersionBump call to correct location
- cmd/bd/version_tracking.go: Fix context handling, update signature
- cmd/bd/version_tracking_test.go: Improve test reliability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-23 18:28:01 -08:00
parent 49db20c594
commit 4a9d6e6dd7
4 changed files with 725 additions and 754 deletions

File diff suppressed because one or more lines are too long

View File

@@ -302,10 +302,6 @@ var rootCmd = &cobra.Command{
// Best-effort tracking - failures are silent
trackBdVersion()
// Auto-migrate database on version bump (bd-jgxi)
// Best-effort migration - failures are silent to avoid disrupting commands
autoMigrateOnVersionBump()
// Initialize daemon status
socketPath := getSocketPath()
daemonStatus = DaemonStatus{
@@ -479,6 +475,11 @@ var rootCmd = &cobra.Command{
debug.Logf("using direct mode (reason: %s)", daemonStatus.FallbackReason)
}
// Auto-migrate database on version bump (bd-jgxi)
// Do this AFTER daemon check but BEFORE opening database for main operation
// This ensures: 1) no daemon has DB open, 2) we don't open DB twice
autoMigrateOnVersionBump(dbPath)
// Fall back to direct storage access
var err error
store, err = sqlite.New(rootCtx, dbPath)

View File

@@ -119,33 +119,24 @@ func maybeShowUpgradeNotification() {
// autoMigrateOnVersionBump automatically migrates the database when CLI version changes.
// This function is best-effort - failures are silent to avoid disrupting commands.
// Called from PersistentPreRun after trackBdVersion().
// Called from PersistentPreRun after daemon check but before opening DB for main operation.
//
// IMPORTANT: This must be called AFTER determining we're in direct mode (no daemon)
// and BEFORE opening the database, to avoid: 1) conflicts with daemon, 2) opening DB twice.
//
// bd-jgxi: Auto-migrate database on CLI version bump
func autoMigrateOnVersionBump() {
func autoMigrateOnVersionBump(dbPath string) {
// Only migrate if version upgrade was detected
if !versionUpgradeDetected {
return
}
// Find the beads directory
beadsDir := beads.FindBeadsDir()
if beadsDir == "" {
// No .beads directory - nothing to migrate
// Validate dbPath
if dbPath == "" {
debug.Logf("auto-migrate: skipping migration, no database path")
return
}
// Load config to get database path
cfg, err := configfile.Load(beadsDir)
if err != nil || cfg == nil {
// Config load failed or doesn't exist - skip migration
debug.Logf("auto-migrate: skipping migration, config load failed: %v", err)
return
}
// Get database path
dbPath := cfg.DatabasePath(beadsDir)
// Check if database exists
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
// No database file - nothing to migrate
@@ -154,7 +145,13 @@ func autoMigrateOnVersionBump() {
}
// Open database to check current version
ctx := context.Background()
// Use rootCtx if available and not canceled, otherwise use Background
ctx := rootCtx
if ctx == nil || ctx.Err() != nil {
// rootCtx is nil or canceled - use fresh background context
ctx = context.Background()
}
store, err := sqlite.New(ctx, dbPath)
if err != nil {
// Failed to open database - skip migration

View File

@@ -6,7 +6,6 @@ import (
"path/filepath"
"testing"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/configfile"
"github.com/steveyegge/beads/internal/storage/sqlite"
)
@@ -323,32 +322,15 @@ func TestAutoMigrateOnVersionBump_NoUpgrade(t *testing.T) {
versionUpgradeDetected = false
// Should return early without doing anything
autoMigrateOnVersionBump()
autoMigrateOnVersionBump("/tmp/test.db")
// Test passes if no panic occurs
}
func TestAutoMigrateOnVersionBump_NoDatabase(t *testing.T) {
// Create temp .beads directory without a database
// Create temp directory
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads: %v", err)
}
// Change to temp directory
origWd, _ := os.Getwd()
defer os.Chdir(origWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp dir: %v", err)
}
// Create metadata.json
cfg := configfile.DefaultConfig()
cfg.LastBdVersion = "0.22.0"
if err := cfg.Save(beadsDir); err != nil {
t.Fatalf("Failed to save config: %v", err)
}
nonExistentDB := filepath.Join(tmpDir, "nonexistent.db")
// Save original state
origUpgradeDetected := versionUpgradeDetected
@@ -360,35 +342,29 @@ func TestAutoMigrateOnVersionBump_NoDatabase(t *testing.T) {
versionUpgradeDetected = true
// Should handle gracefully when database doesn't exist
autoMigrateOnVersionBump()
autoMigrateOnVersionBump(nonExistentDB)
// Test passes if no panic occurs
}
func TestAutoMigrateOnVersionBump_MigratesVersion(t *testing.T) {
// Create temp .beads directory
// NOTE: Cannot use t.Parallel() because we modify global variables
// Save original state FIRST - critical to avoid test pollution from previous tests
origUpgradeDetected := versionUpgradeDetected
origUpgradeAcknowledged := upgradeAcknowledged
origPreviousVersion := previousVersion
defer func() {
versionUpgradeDetected = origUpgradeDetected
upgradeAcknowledged = origUpgradeAcknowledged
previousVersion = origPreviousVersion
}()
// Create temp directory with unique name to avoid any possible interference
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads: %v", err)
}
// Change to temp directory
origWd, _ := os.Getwd()
defer os.Chdir(origWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp dir: %v", err)
}
// Create metadata.json
cfg := configfile.DefaultConfig()
cfg.LastBdVersion = "0.22.0"
if err := cfg.Save(beadsDir); err != nil {
t.Fatalf("Failed to save config: %v", err)
}
dbPath := filepath.Join(tmpDir, "test-migrate-version-unique.db")
// Create database with old version
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
ctx := context.Background()
store, err := sqlite.New(ctx, dbPath)
if err != nil {
@@ -400,19 +376,33 @@ func TestAutoMigrateOnVersionBump_MigratesVersion(t *testing.T) {
if err := store.SetMetadata(ctx, "bd_version", oldVersion); err != nil {
t.Fatalf("Failed to set old version: %v", err)
}
_ = store.Close()
if err := store.Close(); err != nil {
t.Fatalf("Failed to close database: %v", err)
}
// Save original state
origUpgradeDetected := versionUpgradeDetected
defer func() {
versionUpgradeDetected = origUpgradeDetected
}()
// Simulate version upgrade (must be set to true for migration to run)
// Set this AFTER closing the database to ensure clean state
upgradeAcknowledged = false
previousVersion = oldVersion
// Simulate version upgrade
// Verify dbPath before migration
if dbPath == "" {
t.Fatalf("dbPath is empty!")
}
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
t.Fatalf("database doesn't exist before migration: %s", dbPath)
}
// Set versionUpgradeDetected immediately before calling to avoid races
versionUpgradeDetected = true
t.Logf("Calling autoMigrateOnVersionBump with dbPath=%s, versionUpgradeDetected=%v", dbPath, versionUpgradeDetected)
// Immediately call migration while flag is still true
autoMigrateOnVersionBump(dbPath)
// Call auto-migration
autoMigrateOnVersionBump()
// Verify the flag is still true after migration
if !versionUpgradeDetected {
t.Fatalf("version Upgrade detected flag was cleared during migration")
}
// Verify database version was updated
store, err = sqlite.New(ctx, dbPath)
@@ -427,34 +417,17 @@ func TestAutoMigrateOnVersionBump_MigratesVersion(t *testing.T) {
}
if newVersion != Version {
t.Errorf("Database version not updated: got %q, want %q", newVersion, Version)
t.Errorf("Database version not updated: got %q, want %q (versionUpgradeDetected=%v)",
newVersion, Version, versionUpgradeDetected)
}
}
func TestAutoMigrateOnVersionBump_AlreadyMigrated(t *testing.T) {
// Create temp .beads directory
// Create temp directory
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads: %v", err)
}
// Change to temp directory
origWd, _ := os.Getwd()
defer os.Chdir(origWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp dir: %v", err)
}
// Create metadata.json
cfg := configfile.DefaultConfig()
cfg.LastBdVersion = Version
if err := cfg.Save(beadsDir); err != nil {
t.Fatalf("Failed to save config: %v", err)
}
dbPath := filepath.Join(tmpDir, "test.db")
// Create database with current version
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
ctx := context.Background()
store, err := sqlite.New(ctx, dbPath)
if err != nil {
@@ -477,7 +450,7 @@ func TestAutoMigrateOnVersionBump_AlreadyMigrated(t *testing.T) {
versionUpgradeDetected = true
// Call auto-migration - should be a no-op
autoMigrateOnVersionBump()
autoMigrateOnVersionBump(dbPath)
// Verify database version is still current
store, err = sqlite.New(ctx, dbPath)