This commit is contained in:
Steve Yegge
2025-11-23 18:15:29 -08:00
4 changed files with 938 additions and 664 deletions

File diff suppressed because one or more lines are too long

View File

@@ -302,6 +302,10 @@ 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{

View File

@@ -1,10 +1,14 @@
package main
import (
"context"
"fmt"
"os"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/configfile"
"github.com/steveyegge/beads/internal/debug"
"github.com/steveyegge/beads/internal/storage/sqlite"
)
// trackBdVersion checks if bd version has changed since last run and updates metadata.json.
@@ -112,3 +116,82 @@ func maybeShowUpgradeNotification() {
fmt.Println("💡 Run 'bd upgrade review' to see what changed")
fmt.Println()
}
// 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().
//
// bd-jgxi: Auto-migrate database on CLI version bump
func autoMigrateOnVersionBump() {
// 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
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
debug.Logf("auto-migrate: skipping migration, database does not exist: %s", dbPath)
return
}
// Open database to check current version
ctx := context.Background()
store, err := sqlite.New(ctx, dbPath)
if err != nil {
// Failed to open database - skip migration
debug.Logf("auto-migrate: failed to open database: %v", err)
return
}
// Get current database version
dbVersion, err := store.GetMetadata(ctx, "bd_version")
if err != nil {
// Failed to read version - skip migration
debug.Logf("auto-migrate: failed to read database version: %v", err)
_ = store.Close()
return
}
// Check if migration is needed
if dbVersion == Version {
// Database is already at current version
debug.Logf("auto-migrate: database already at version %s", Version)
_ = store.Close()
return
}
// Perform migration: update database version
debug.Logf("auto-migrate: migrating database from %s to %s", dbVersion, Version)
if err := store.SetMetadata(ctx, "bd_version", Version); err != nil {
// Migration failed - log and continue
debug.Logf("auto-migrate: failed to update database version: %v", err)
_ = store.Close()
return
}
// Close database
if err := store.Close(); err != nil {
debug.Logf("auto-migrate: warning: failed to close database: %v", err)
}
debug.Logf("auto-migrate: successfully migrated database to version %s", Version)
}

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)
}
}