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"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/steveyegge/beads/internal/configfile"
)
// 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 {
beadsDir := filepath.Join(path, ".beads")
localVersionPath := filepath.Join(beadsDir, localVersionFile)
// Load metadata.json
cfg, err := configfile.Load(beadsDir)
// Read .local_version file
// #nosec G304 - path is constructed from controlled beadsDir + constant
data, err := os.ReadFile(localVersionPath)
if err != nil {
if os.IsNotExist(err) {
// File doesn't exist yet - will be created on next bd command
return DoctorCheck{
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: "Metadata Version Tracking",
Name: "Version Tracking",
Status: StatusError,
Message: "Unable to read metadata.json",
Message: "Unable to read .local_version file",
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
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",
}
}
lastVersion := strings.TrimSpace(string(data))
// Check if LastBdVersion field is present
if cfg.LastBdVersion == "" {
// Check if file is empty
if lastVersion == "" {
return DoctorCheck{
Name: "Metadata Version Tracking",
Name: "Version Tracking",
Status: StatusWarning,
Message: "LastBdVersion field is empty (first run)",
Message: ".local_version file is empty",
Detail: "Version tracking will be initialized on next command",
Fix: "Run any bd command to initialize version tracking",
}
}
// Validate that LastBdVersion is a valid semver-like string
// Simple validation: should be X.Y.Z format where X, Y, Z are numbers
if !IsValidSemver(cfg.LastBdVersion) {
// Validate that version is a valid semver-like string
if !IsValidSemver(lastVersion) {
return DoctorCheck{
Name: "Metadata Version Tracking",
Name: "Version Tracking",
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'",
Fix: "Run any bd command to reset version tracking to current version",
}
}
// Check if LastBdVersion is very old (> 10 versions behind)
// Calculate version distance
versionDiff := CompareVersions(currentVersion, cfg.LastBdVersion)
// Check if version is very old (> 10 versions behind)
versionDiff := CompareVersions(currentVersion, lastVersion)
if versionDiff > 0 {
// Current version is newer - check how far behind
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+
majorDiff := currentParts[0] - lastParts[0]
@@ -116,27 +126,27 @@ func CheckMetadataVersionTracking(path string, currentVersion string) DoctorChec
if majorDiff >= 1 || (majorDiff == 0 && minorDiff >= 10) {
return DoctorCheck{
Name: "Metadata Version Tracking",
Name: "Version Tracking",
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",
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{
Name: "Metadata Version Tracking",
Name: "Version Tracking",
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{
Name: "Metadata Version Tracking",
Name: "Version Tracking",
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) {
// GH#662: Tests updated to use .local_version file instead of metadata.json:LastBdVersion
tests := []struct {
name string
setupMetadata func(beadsDir string) error
setupVersion func(beadsDir string) error
expectedStatus string
expectWarning bool
}{
{
name: "valid current version",
setupMetadata: func(beadsDir string) error {
cfg := map[string]string{
"database": "beads.db",
"last_bd_version": Version,
}
data, _ := json.Marshal(cfg)
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644)
setupVersion: func(beadsDir string) error {
return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte(Version+"\n"), 0644)
},
expectedStatus: doctor.StatusOK,
expectWarning: false,
},
{
name: "slightly outdated version",
setupMetadata: func(beadsDir string) error {
cfg := map[string]string{
"database": "beads.db",
"last_bd_version": "0.24.0",
}
data, _ := json.Marshal(cfg)
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644)
setupVersion: func(beadsDir string) error {
return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("0.24.0\n"), 0644)
},
expectedStatus: doctor.StatusOK,
expectWarning: false,
},
{
name: "very old version",
setupMetadata: func(beadsDir string) error {
cfg := map[string]string{
"database": "beads.db",
"last_bd_version": "0.14.0",
}
data, _ := json.Marshal(cfg)
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644)
setupVersion: func(beadsDir string) error {
return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("0.14.0\n"), 0644)
},
expectedStatus: doctor.StatusWarning,
expectWarning: true,
},
{
name: "empty version field",
setupMetadata: func(beadsDir string) error {
cfg := map[string]string{
"database": "beads.db",
"last_bd_version": "",
}
data, _ := json.Marshal(cfg)
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644)
name: "empty version file",
setupVersion: func(beadsDir string) error {
return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte(""), 0644)
},
expectedStatus: doctor.StatusWarning,
expectWarning: true,
},
{
name: "invalid version format",
setupMetadata: func(beadsDir string) error {
cfg := map[string]string{
"database": "beads.db",
"last_bd_version": "invalid-version",
}
data, _ := json.Marshal(cfg)
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), data, 0644)
setupVersion: func(beadsDir string) error {
return os.WriteFile(filepath.Join(beadsDir, ".local_version"), []byte("invalid-version\n"), 0644)
},
expectedStatus: doctor.StatusWarning,
expectWarning: true,
},
{
name: "corrupted metadata.json",
setupMetadata: func(beadsDir string) error {
return os.WriteFile(filepath.Join(beadsDir, "metadata.json"), []byte("{invalid json}"), 0644)
},
expectedStatus: doctor.StatusError,
expectWarning: false,
},
{
name: "missing metadata.json",
setupMetadata: func(beadsDir string) error {
// Don't create metadata.json
name: "missing .local_version file",
setupVersion: func(beadsDir string) error {
// Don't create .local_version
return nil
},
expectedStatus: doctor.StatusWarning,
@@ -975,8 +943,8 @@ func TestCheckMetadataVersionTracking(t *testing.T) {
t.Fatal(err)
}
// Setup metadata.json
if err := tc.setupMetadata(beadsDir); err != nil {
// Setup .local_version file
if err := tc.setupVersion(beadsDir); err != nil {
t.Fatal(err)
}