feat: integrate migration detection into bd doctor (bd-7l27)
Add a consolidated "Pending Migrations" check to bd doctor that: - Detects sequential ID usage (suggests bd migrate hash-ids) - Detects legacy deletions.jsonl (suggests bd migrate tombstones) - Detects missing sync-branch config (suggests bd migrate sync) - Detects database version mismatches (suggests bd migrate) Also updates existing checks to use correct modern commands: - CheckIDFormat: bd migrate hash-ids (was bd migrate --to-hash-ids) - CheckDeletionsManifest: bd migrate tombstones (was bd migrate-tombstones) - CheckSyncBranchConfig: bd migrate sync beads-sync (was config.yaml edit) Removes TODO(bd-7l27) comments from migrate_*.go files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
188
cmd/bd/doctor/migration_test.go
Normal file
188
cmd/bd/doctor/migration_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckPendingMigrations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T, dir string)
|
||||
wantStatus string
|
||||
wantMessage string
|
||||
wantMigrations int
|
||||
}{
|
||||
{
|
||||
name: "no beads directory",
|
||||
setup: func(t *testing.T, dir string) {},
|
||||
wantStatus: StatusOK,
|
||||
wantMessage: "None required",
|
||||
},
|
||||
{
|
||||
name: "empty beads directory",
|
||||
setup: func(t *testing.T, dir string) {
|
||||
if err := os.MkdirAll(filepath.Join(dir, ".beads"), 0755); err != nil {
|
||||
t.Fatalf("failed to create .beads: %v", err)
|
||||
}
|
||||
},
|
||||
wantStatus: StatusOK,
|
||||
wantMessage: "None required",
|
||||
},
|
||||
{
|
||||
name: "deletions.jsonl exists with entries",
|
||||
setup: func(t *testing.T, dir string) {
|
||||
beadsDir := filepath.Join(dir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .beads: %v", err)
|
||||
}
|
||||
// Create deletions.jsonl with an entry
|
||||
content := `{"id":"bd-test","ts":"2024-01-01T00:00:00Z","by":"test"}`
|
||||
if err := os.WriteFile(filepath.Join(beadsDir, "deletions.jsonl"), []byte(content), 0644); err != nil {
|
||||
t.Fatalf("failed to create deletions.jsonl: %v", err)
|
||||
}
|
||||
},
|
||||
wantStatus: StatusWarning,
|
||||
wantMigrations: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
tt.setup(t, tmpDir)
|
||||
|
||||
check := CheckPendingMigrations(tmpDir)
|
||||
|
||||
if check.Status != tt.wantStatus {
|
||||
t.Errorf("status = %q, want %q", check.Status, tt.wantStatus)
|
||||
}
|
||||
|
||||
if tt.wantMessage != "" && check.Message != tt.wantMessage {
|
||||
t.Errorf("message = %q, want %q", check.Message, tt.wantMessage)
|
||||
}
|
||||
|
||||
if check.Category != CategoryMaintenance {
|
||||
t.Errorf("category = %q, want %q", check.Category, CategoryMaintenance)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectPendingMigrations(t *testing.T) {
|
||||
t.Run("no beads directory returns empty", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
migrations := DetectPendingMigrations(tmpDir)
|
||||
if len(migrations) != 0 {
|
||||
t.Errorf("expected 0 migrations, got %d", len(migrations))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty beads directory returns empty", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(tmpDir, ".beads"), 0755); err != nil {
|
||||
t.Fatalf("failed to create .beads: %v", err)
|
||||
}
|
||||
|
||||
migrations := DetectPendingMigrations(tmpDir)
|
||||
if len(migrations) != 0 {
|
||||
t.Errorf("expected 0 migrations, got %d", len(migrations))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("deletions.jsonl triggers tombstones migration", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .beads: %v", err)
|
||||
}
|
||||
|
||||
// Create deletions.jsonl with an entry
|
||||
content := `{"id":"bd-test","ts":"2024-01-01T00:00:00Z","by":"test"}`
|
||||
if err := os.WriteFile(filepath.Join(beadsDir, "deletions.jsonl"), []byte(content), 0644); err != nil {
|
||||
t.Fatalf("failed to create deletions.jsonl: %v", err)
|
||||
}
|
||||
|
||||
migrations := DetectPendingMigrations(tmpDir)
|
||||
if len(migrations) != 1 {
|
||||
t.Errorf("expected 1 migration, got %d", len(migrations))
|
||||
return
|
||||
}
|
||||
|
||||
if migrations[0].Name != "tombstones" {
|
||||
t.Errorf("migration name = %q, want %q", migrations[0].Name, "tombstones")
|
||||
}
|
||||
|
||||
if migrations[0].Command != "bd migrate tombstones" {
|
||||
t.Errorf("migration command = %q, want %q", migrations[0].Command, "bd migrate tombstones")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNeedsTombstonesMigration(t *testing.T) {
|
||||
t.Run("no deletions.jsonl returns false", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if needsTombstonesMigration(tmpDir) {
|
||||
t.Error("expected false for non-existent deletions.jsonl")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty deletions.jsonl returns false", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "deletions.jsonl"), []byte(""), 0644); err != nil {
|
||||
t.Fatalf("failed to create deletions.jsonl: %v", err)
|
||||
}
|
||||
|
||||
if needsTombstonesMigration(tmpDir) {
|
||||
t.Error("expected false for empty deletions.jsonl")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("deletions.jsonl with entries returns true", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-doctor-migration-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
content := `{"id":"bd-test","ts":"2024-01-01T00:00:00Z","by":"test"}`
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "deletions.jsonl"), []byte(content), 0644); err != nil {
|
||||
t.Fatalf("failed to create deletions.jsonl: %v", err)
|
||||
}
|
||||
|
||||
if !needsTombstonesMigration(tmpDir) {
|
||||
t.Error("expected true for deletions.jsonl with entries")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user