feat: Auto-migrate database on CLI version bump (bd-jgxi)

When CLI is upgraded (e.g., 0.24.0 → 0.24.1), the database version
is now automatically updated to match the CLI version during
PersistentPreRun. This fixes the recurring UX issue where bd doctor
shows version mismatch after every CLI upgrade.

Implementation:
- Added autoMigrateOnVersionBump() function in version_tracking.go
- Calls after trackBdVersion() in PersistentPreRun
- Best-effort and silent failures to avoid disrupting commands
- Only updates bd_version metadata field
- Includes comprehensive test coverage

Changes:
- cmd/bd/main.go: Call autoMigrateOnVersionBump() in PersistentPreRun
- cmd/bd/version_tracking.go: Implement auto-migration logic
- cmd/bd/version_tracking_test.go: Add tests for auto-migration

🤖 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:09:24 -08:00
parent e76c7bec7c
commit 7796f5c7f5
4 changed files with 275 additions and 1 deletions

View File

@@ -1,11 +1,14 @@
package main
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/configfile"
"github.com/steveyegge/beads/internal/storage/sqlite"
)
func TestGetVersionsSince(t *testing.T) {
@@ -308,3 +311,187 @@ func TestMaybeShowUpgradeNotification(t *testing.T) {
t.Error("Should not change acknowledged state on subsequent calls")
}
}
func TestAutoMigrateOnVersionBump_NoUpgrade(t *testing.T) {
// Save original state
origUpgradeDetected := versionUpgradeDetected
defer func() {
versionUpgradeDetected = origUpgradeDetected
}()
// Reset state - no upgrade detected
versionUpgradeDetected = false
// Should return early without doing anything
autoMigrateOnVersionBump()
// Test passes if no panic occurs
}
func TestAutoMigrateOnVersionBump_NoDatabase(t *testing.T) {
// Create temp .beads directory without a database
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)
}
// Save original state
origUpgradeDetected := versionUpgradeDetected
defer func() {
versionUpgradeDetected = origUpgradeDetected
}()
// Simulate version upgrade
versionUpgradeDetected = true
// Should handle gracefully when database doesn't exist
autoMigrateOnVersionBump()
// Test passes if no panic occurs
}
func TestAutoMigrateOnVersionBump_MigratesVersion(t *testing.T) {
// Create temp .beads 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)
}
// Create database with old version
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
ctx := context.Background()
store, err := sqlite.New(ctx, dbPath)
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
// Set old database version
oldVersion := "0.22.0"
if err := store.SetMetadata(ctx, "bd_version", oldVersion); err != nil {
t.Fatalf("Failed to set old version: %v", err)
}
_ = store.Close()
// Save original state
origUpgradeDetected := versionUpgradeDetected
defer func() {
versionUpgradeDetected = origUpgradeDetected
}()
// Simulate version upgrade
versionUpgradeDetected = true
// Call auto-migration
autoMigrateOnVersionBump()
// Verify database version was updated
store, err = sqlite.New(ctx, dbPath)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer store.Close()
newVersion, err := store.GetMetadata(ctx, "bd_version")
if err != nil {
t.Fatalf("Failed to read database version: %v", err)
}
if newVersion != Version {
t.Errorf("Database version not updated: got %q, want %q", newVersion, Version)
}
}
func TestAutoMigrateOnVersionBump_AlreadyMigrated(t *testing.T) {
// Create temp .beads 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)
}
// Create database with current version
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
ctx := context.Background()
store, err := sqlite.New(ctx, dbPath)
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
// Set current database version
if err := store.SetMetadata(ctx, "bd_version", Version); err != nil {
t.Fatalf("Failed to set version: %v", err)
}
_ = store.Close()
// Save original state
origUpgradeDetected := versionUpgradeDetected
defer func() {
versionUpgradeDetected = origUpgradeDetected
}()
// Simulate version upgrade
versionUpgradeDetected = true
// Call auto-migration - should be a no-op
autoMigrateOnVersionBump()
// Verify database version is still current
store, err = sqlite.New(ctx, dbPath)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer store.Close()
currentVersion, err := store.GetMetadata(ctx, "bd_version")
if err != nil {
t.Fatalf("Failed to read database version: %v", err)
}
if currentVersion != Version {
t.Errorf("Database version changed unexpectedly: got %q, want %q", currentVersion, Version)
}
}