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:
@@ -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{
|
||||||
|
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{
|
return DoctorCheck{
|
||||||
Name: "Metadata Version Tracking",
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user