Files
beads/cmd/bd/version_tracking_test.go
Steve Yegge 7796f5c7f5 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>
2025-11-23 18:09:24 -08:00

498 lines
13 KiB
Go

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) {
tests := []struct {
name string
sinceVersion string
expectedCount int
description string
}{
{
name: "empty version returns all",
sinceVersion: "",
expectedCount: len(versionChanges),
description: "Should return all versions when sinceVersion is empty",
},
{
name: "version not in changelog",
sinceVersion: "0.1.0",
expectedCount: len(versionChanges),
description: "Should return all versions when sinceVersion not found",
},
{
name: "oldest version in changelog",
sinceVersion: "0.21.0",
expectedCount: 3, // 0.22.0, 0.22.1, 0.23.0
description: "Should return versions newer than oldest",
},
{
name: "middle version returns newer versions",
sinceVersion: "0.22.0",
expectedCount: 2, // 0.22.1 and 0.23.0
description: "Should return versions newer than specified",
},
{
name: "latest version returns empty",
sinceVersion: "0.23.0",
expectedCount: 0,
description: "Should return empty slice when already on latest in changelog",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getVersionsSince(tt.sinceVersion)
if len(result) != tt.expectedCount {
t.Errorf("getVersionsSince(%q) returned %d versions, want %d: %s",
tt.sinceVersion, len(result), tt.expectedCount, tt.description)
}
})
}
}
func TestGetVersionsSinceOrder(t *testing.T) {
// Test that versions are returned in chronological order (oldest first)
// versionChanges array is newest-first, but getVersionsSince returns oldest-first
result := getVersionsSince("0.21.0")
if len(result) != 3 {
t.Fatalf("Expected 3 versions after 0.21.0, got %d", len(result))
}
// Verify chronological order by checking dates increase
// result should be [0.22.0, 0.22.1, 0.23.0]
for i := 1; i < len(result); i++ {
prev := result[i-1]
curr := result[i]
// Simple date comparison (YYYY-MM-DD format)
if curr.Date < prev.Date {
t.Errorf("Versions not in chronological order: %s (%s) should come before %s (%s)",
prev.Version, prev.Date, curr.Version, curr.Date)
}
}
// Check specific order
expectedVersions := []string{"0.22.0", "0.22.1", "0.23.0"}
for i, expected := range expectedVersions {
if result[i].Version != expected {
t.Errorf("Version at index %d = %s, want %s", i, result[i].Version, expected)
}
}
}
func TestTrackBdVersion_NoBeadsDir(t *testing.T) {
// Save original state
origUpgradeDetected := versionUpgradeDetected
origPreviousVersion := previousVersion
defer func() {
versionUpgradeDetected = origUpgradeDetected
previousVersion = origPreviousVersion
}()
// Change to temp directory with no .beads
tmpDir := t.TempDir()
origWd, _ := os.Getwd()
defer os.Chdir(origWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp dir: %v", err)
}
// trackBdVersion should silently succeed
trackBdVersion()
// Should not detect upgrade when no .beads dir exists
if versionUpgradeDetected {
t.Error("Expected no upgrade detection when .beads directory doesn't exist")
}
}
func TestTrackBdVersion_FirstRun(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)
}
// Save original state
origUpgradeDetected := versionUpgradeDetected
origPreviousVersion := previousVersion
defer func() {
versionUpgradeDetected = origUpgradeDetected
previousVersion = origPreviousVersion
}()
// Reset state
versionUpgradeDetected = false
previousVersion = ""
// trackBdVersion should create metadata.json
trackBdVersion()
// Should not detect upgrade on first run
if versionUpgradeDetected {
t.Error("Expected no upgrade detection on first run")
}
// Should have created metadata.json with current version
cfg, err := configfile.Load(beadsDir)
if err != nil {
t.Fatalf("Failed to load config after tracking: %v", err)
}
if cfg.LastBdVersion != Version {
t.Errorf("LastBdVersion = %q, want %q", cfg.LastBdVersion, Version)
}
}
func TestTrackBdVersion_UpgradeDetection(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 with old version
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
origPreviousVersion := previousVersion
defer func() {
versionUpgradeDetected = origUpgradeDetected
previousVersion = origPreviousVersion
}()
// Reset state
versionUpgradeDetected = false
previousVersion = ""
// trackBdVersion should detect upgrade
trackBdVersion()
// Should detect upgrade
if !versionUpgradeDetected {
t.Error("Expected upgrade detection when version changed")
}
if previousVersion != "0.22.0" {
t.Errorf("previousVersion = %q, want %q", previousVersion, "0.22.0")
}
// Should have updated metadata.json to current version
cfg, err := configfile.Load(beadsDir)
if err != nil {
t.Fatalf("Failed to load config after tracking: %v", err)
}
if cfg.LastBdVersion != Version {
t.Errorf("LastBdVersion = %q, want %q", cfg.LastBdVersion, Version)
}
}
func TestTrackBdVersion_SameVersion(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 with current version
cfg := configfile.DefaultConfig()
cfg.LastBdVersion = Version
if err := cfg.Save(beadsDir); err != nil {
t.Fatalf("Failed to save config: %v", err)
}
// Save original state
origUpgradeDetected := versionUpgradeDetected
origPreviousVersion := previousVersion
defer func() {
versionUpgradeDetected = origUpgradeDetected
previousVersion = origPreviousVersion
}()
// Reset state
versionUpgradeDetected = false
previousVersion = ""
// trackBdVersion should not detect upgrade
trackBdVersion()
// Should not detect upgrade
if versionUpgradeDetected {
t.Error("Expected no upgrade detection when version is the same")
}
}
func TestMaybeShowUpgradeNotification(t *testing.T) {
// Save original state
origUpgradeDetected := versionUpgradeDetected
origPreviousVersion := previousVersion
origUpgradeAcknowledged := upgradeAcknowledged
defer func() {
versionUpgradeDetected = origUpgradeDetected
previousVersion = origPreviousVersion
upgradeAcknowledged = origUpgradeAcknowledged
}()
// Test: No upgrade detected - should not modify acknowledged flag
versionUpgradeDetected = false
upgradeAcknowledged = false
previousVersion = ""
maybeShowUpgradeNotification()
if upgradeAcknowledged {
t.Error("Should not set acknowledged flag when no upgrade detected")
}
// Test: Upgrade detected but already acknowledged - should not change state
versionUpgradeDetected = true
upgradeAcknowledged = true
previousVersion = "0.22.0"
maybeShowUpgradeNotification()
if !upgradeAcknowledged {
t.Error("Should keep acknowledged flag when already acknowledged")
}
// Test: Upgrade detected and not acknowledged - should set acknowledged flag
versionUpgradeDetected = true
upgradeAcknowledged = false
previousVersion = "0.22.0"
maybeShowUpgradeNotification()
if !upgradeAcknowledged {
t.Error("Should mark as acknowledged after showing notification")
}
// Calling again should keep acknowledged flag set
prevAck := upgradeAcknowledged
maybeShowUpgradeNotification()
if upgradeAcknowledged != prevAck {
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)
}
}