fix(doctor): check .local_version instead of deprecated LastBdVersion (fixes #662)

The doctor's version tracking check was looking at metadata.json:LastBdVersion,
but version tracking was moved to .local_version file (gitignored). This caused
the "LastBdVersion field is empty" warning to persist even after running bd
commands, because those commands update .local_version but not metadata.json.

- Update CheckMetadataVersionTracking to read from .local_version
- Rename check from "Metadata Version Tracking" to "Version Tracking"
- Update tests to use .local_version instead of metadata.json
- Remove unused configfile import

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-20 14:41:13 -08:00
parent e778b3f648
commit 5b2a516aca
2 changed files with 66 additions and 88 deletions

View File

@@ -5,11 +5,10 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/steveyegge/beads/internal/configfile"
) )
// CheckCLIVersion checks if the CLI version is up to date. // CheckCLIVersion checks if the CLI version is up to date.
@@ -53,62 +52,73 @@ func CheckCLIVersion(cliVersion string) DoctorCheck {
} }
} }
// CheckMetadataVersionTracking checks if metadata.json has proper version tracking. // localVersionFile is the gitignored file that stores the last bd version used locally.
// Must match the constant in version_tracking.go.
const localVersionFile = ".local_version"
// CheckMetadataVersionTracking checks if version tracking is properly configured.
// Version tracking uses .local_version file (gitignored) to track the last bd version used.
//
// GH#662: This was updated to check .local_version instead of metadata.json:LastBdVersion,
// which is now deprecated.
func CheckMetadataVersionTracking(path string, currentVersion string) DoctorCheck { func CheckMetadataVersionTracking(path string, currentVersion string) DoctorCheck {
beadsDir := filepath.Join(path, ".beads") beadsDir := filepath.Join(path, ".beads")
localVersionPath := filepath.Join(beadsDir, localVersionFile)
// Load metadata.json // Read .local_version file
cfg, err := configfile.Load(beadsDir) // #nosec G304 - path is constructed from controlled beadsDir + constant
data, err := os.ReadFile(localVersionPath)
if err != nil { if err != nil {
if os.IsNotExist(err) {
// File doesn't exist yet - will be created on next bd command
return DoctorCheck{ return DoctorCheck{
Name: "Metadata Version Tracking", Name: "Version Tracking",
Status: StatusWarning,
Message: "Version tracking not initialized",
Detail: "The .local_version file will be created on next bd command",
Fix: "Run any bd command (e.g., 'bd ready') to initialize version tracking",
}
}
// Other error reading file
return DoctorCheck{
Name: "Version Tracking",
Status: StatusError, Status: StatusError,
Message: "Unable to read metadata.json", Message: "Unable to read .local_version file",
Detail: err.Error(), Detail: err.Error(),
Fix: "Ensure metadata.json exists and is valid JSON. Run 'bd init' if needed.", Fix: "Check file permissions on .beads/.local_version",
} }
} }
// Check if metadata.json exists lastVersion := strings.TrimSpace(string(data))
if cfg == nil {
return DoctorCheck{
Name: "Metadata Version Tracking",
Status: StatusWarning,
Message: "metadata.json not found",
Fix: "Run any bd command to create metadata.json with version tracking",
}
}
// Check if LastBdVersion field is present // Check if file is empty
if cfg.LastBdVersion == "" { if lastVersion == "" {
return DoctorCheck{ return DoctorCheck{
Name: "Metadata Version Tracking", Name: "Version Tracking",
Status: StatusWarning, Status: StatusWarning,
Message: "LastBdVersion field is empty (first run)", Message: ".local_version file is empty",
Detail: "Version tracking will be initialized on next command", Detail: "Version tracking will be initialized on next command",
Fix: "Run any bd command to initialize version tracking", Fix: "Run any bd command to initialize version tracking",
} }
} }
// Validate that LastBdVersion is a valid semver-like string // Validate that version is a valid semver-like string
// Simple validation: should be X.Y.Z format where X, Y, Z are numbers if !IsValidSemver(lastVersion) {
if !IsValidSemver(cfg.LastBdVersion) {
return DoctorCheck{ return DoctorCheck{
Name: "Metadata Version Tracking", Name: "Version Tracking",
Status: StatusWarning, Status: StatusWarning,
Message: fmt.Sprintf("LastBdVersion has invalid format: %q", cfg.LastBdVersion), Message: fmt.Sprintf("Invalid version format in .local_version: %q", lastVersion),
Detail: "Expected semver format like '0.24.2'", Detail: "Expected semver format like '0.24.2'",
Fix: "Run any bd command to reset version tracking to current version", Fix: "Run any bd command to reset version tracking to current version",
} }
} }
// Check if LastBdVersion is very old (> 10 versions behind) // Check if version is very old (> 10 versions behind)
// Calculate version distance versionDiff := CompareVersions(currentVersion, lastVersion)
versionDiff := CompareVersions(currentVersion, cfg.LastBdVersion)
if versionDiff > 0 { if versionDiff > 0 {
// Current version is newer - check how far behind // Current version is newer - check how far behind
currentParts := ParseVersionParts(currentVersion) currentParts := ParseVersionParts(currentVersion)
lastParts := ParseVersionParts(cfg.LastBdVersion) lastParts := ParseVersionParts(lastVersion)
// Simple heuristic: warn if minor version is 10+ behind or major version differs by 1+ // Simple heuristic: warn if minor version is 10+ behind or major version differs by 1+
majorDiff := currentParts[0] - lastParts[0] majorDiff := currentParts[0] - lastParts[0]
@@ -116,27 +126,27 @@ func CheckMetadataVersionTracking(path string, currentVersion string) DoctorChec
if majorDiff >= 1 || (majorDiff == 0 && minorDiff >= 10) { if majorDiff >= 1 || (majorDiff == 0 && minorDiff >= 10) {
return DoctorCheck{ return DoctorCheck{
Name: "Metadata Version Tracking", Name: "Version Tracking",
Status: StatusWarning, Status: StatusWarning,
Message: fmt.Sprintf("LastBdVersion is very old: %s (current: %s)", cfg.LastBdVersion, currentVersion), Message: fmt.Sprintf("Last recorded version is very old: %s (current: %s)", lastVersion, currentVersion),
Detail: "You may have missed important upgrade notifications", Detail: "You may have missed important upgrade notifications",
Fix: "Run 'bd upgrade review' to see recent changes", Fix: "Run 'bd upgrade review' to see recent changes",
} }
} }
// Version is behind but not too old // Version is behind but not too old - this is normal after upgrade
return DoctorCheck{ return DoctorCheck{
Name: "Metadata Version Tracking", Name: "Version Tracking",
Status: StatusOK, Status: StatusOK,
Message: fmt.Sprintf("Version tracking active (last: %s, current: %s)", cfg.LastBdVersion, currentVersion), Message: fmt.Sprintf("Version tracking active (last: %s, current: %s)", lastVersion, currentVersion),
} }
} }
// Version is current or ahead (shouldn't happen, but handle it) // Version is current or ahead
return DoctorCheck{ return DoctorCheck{
Name: "Metadata Version Tracking", Name: "Version Tracking",
Status: StatusOK, Status: StatusOK,
Message: fmt.Sprintf("Version tracking active (version: %s)", cfg.LastBdVersion), Message: fmt.Sprintf("Version tracking active (version: %s)", lastVersion),
} }
} }

View File

@@ -877,89 +877,57 @@ func TestGetClaudePluginVersion(t *testing.T) {
} }
func TestCheckMetadataVersionTracking(t *testing.T) { func TestCheckMetadataVersionTracking(t *testing.T) {
// GH#662: Tests updated to use .local_version file instead of metadata.json:LastBdVersion
tests := []struct { tests := []struct {
name string name string
setupMetadata func(beadsDir string) error setupVersion func(beadsDir string) error
expectedStatus string expectedStatus string
expectWarning bool expectWarning bool
}{ }{
{ {
name: "valid current version", name: "valid current version",
setupMetadata: func(beadsDir string) error { setupVersion: func(beadsDir string) error {
cfg := map[string]string{ return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte(Version+"\n"), 0644)
"database": "beads.db",
"last_bd_version": Version,
}
data, _ := json.Marshal(cfg)
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644)
}, },
expectedStatus: doctor.StatusOK, expectedStatus: doctor.StatusOK,
expectWarning: false, expectWarning: false,
}, },
{ {
name: "slightly outdated version", name: "slightly outdated version",
setupMetadata: func(beadsDir string) error { setupVersion: func(beadsDir string) error {
cfg := map[string]string{ return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("0.24.0\n"), 0644)
"database": "beads.db",
"last_bd_version": "0.24.0",
}
data, _ := json.Marshal(cfg)
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644)
}, },
expectedStatus: doctor.StatusOK, expectedStatus: doctor.StatusOK,
expectWarning: false, expectWarning: false,
}, },
{ {
name: "very old version", name: "very old version",
setupMetadata: func(beadsDir string) error { setupVersion: func(beadsDir string) error {
cfg := map[string]string{ return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("0.14.0\n"), 0644)
"database": "beads.db",
"last_bd_version": "0.14.0",
}
data, _ := json.Marshal(cfg)
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644)
}, },
expectedStatus: doctor.StatusWarning, expectedStatus: doctor.StatusWarning,
expectWarning: true, expectWarning: true,
}, },
{ {
name: "empty version field", name: "empty version file",
setupMetadata: func(beadsDir string) error { setupVersion: func(beadsDir string) error {
cfg := map[string]string{ return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte(""), 0644)
"database": "beads.db",
"last_bd_version": "",
}
data, _ := json.Marshal(cfg)
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644)
}, },
expectedStatus: doctor.StatusWarning, expectedStatus: doctor.StatusWarning,
expectWarning: true, expectWarning: true,
}, },
{ {
name: "invalid version format", name: "invalid version format",
setupMetadata: func(beadsDir string) error { setupVersion: func(beadsDir string) error {
cfg := map[string]string{ return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("invalid-version\n"), 0644)
"database": "beads.db",
"last_bd_version": "invalid-version",
}
data, _ := json.Marshal(cfg)
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644)
}, },
expectedStatus: doctor.StatusWarning, expectedStatus: doctor.StatusWarning,
expectWarning: true, expectWarning: true,
}, },
{ {
name: "corrupted metadata.json", name: "missing .local_version file",
setupMetadata: func(beadsDir string) error { setupVersion: func(beadsDir string) error {
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), []byte("{invalid json}"), 0644) // Don't create .local_version
},
expectedStatus: doctor.StatusError,
expectWarning: false,
},
{
name: "missing metadata.json",
setupMetadata: func(beadsDir string) error {
// Don't create metadata.json
return nil return nil
}, },
expectedStatus: doctor.StatusWarning, expectedStatus: doctor.StatusWarning,
@@ -975,8 +943,8 @@ func TestCheckMetadataVersionTracking(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// Setup metadata.json // Setup .local_version file
if err := tc.setupMetadata(beadsDir); err != nil { if err := tc.setupVersion(beadsDir); err != nil {
t.Fatal(err) t.Fatal(err)
} }