feat: Add metadata.json version tracking validation to bd doctor (bd-u4sb)
Add comprehensive validation of metadata.json version tracking to bd doctor: Checks added: - metadata.json exists and is valid JSON - LastBdVersion field is present and non-empty - LastBdVersion is valid semver format (e.g., 0.24.2) - Warns if LastBdVersion is very old (> 10 minor versions behind) - Provides helpful fix messages for each validation failure Implementation: - New checkMetadataVersionTracking() function - Helper functions: isValidSemver(), parseVersionParts() - Comprehensive test coverage for all validation scenarios Tests: - TestCheckMetadataVersionTracking: 7 test cases covering all scenarios - TestIsValidSemver: Version format validation - TestParseVersionParts: Version parsing logic This helps ensure version tracking (bd-loka) is working correctly and alerts users if they've missed important upgrade notifications. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -613,6 +613,7 @@
|
|||||||
{"id":"bd-twlr","content_hash":"e0fe5d5f0cac3bb24ae6c12bdcac79ba0dac61f2e85568e9def8b809b7d038b6","title":"Add bd init --team wizard","description":"Interactive wizard for team workflow setup. Guides user through: branch workflow configuration, shared repo setup, team member onboarding, examples of team collaboration patterns.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-05T18:04:30.013645-08:00","updated_at":"2025-11-05T19:27:33.075826-08:00","closed_at":"2025-11-05T18:56:03.004161-08:00","source_repo":".","dependencies":[{"issue_id":"bd-twlr","depends_on_id":"bd-8rd","type":"parent-child","created_at":"2025-11-05T18:04:39.164445-08:00","created_by":"daemon"}]}
|
{"id":"bd-twlr","content_hash":"e0fe5d5f0cac3bb24ae6c12bdcac79ba0dac61f2e85568e9def8b809b7d038b6","title":"Add bd init --team wizard","description":"Interactive wizard for team workflow setup. Guides user through: branch workflow configuration, shared repo setup, team member onboarding, examples of team collaboration patterns.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-05T18:04:30.013645-08:00","updated_at":"2025-11-05T19:27:33.075826-08:00","closed_at":"2025-11-05T18:56:03.004161-08:00","source_repo":".","dependencies":[{"issue_id":"bd-twlr","depends_on_id":"bd-8rd","type":"parent-child","created_at":"2025-11-05T18:04:39.164445-08:00","created_by":"daemon"}]}
|
||||||
{"id":"bd-u3t","content_hash":"0666b1c7fb8f72d592027b74221e620e5f6aeb71a2ab4c3bcc15df190dc6a037","title":"Phase 2: Implement sandbox auto-detection for GH #353","description":"Implement automatic sandbox detection to improve UX for users in sandboxed environments (e.g., Codex).\n\n**Tasks:**\n1. Implement sandbox detection heuristic using syscall.Kill permission checks\n2. Auto-enable --sandbox mode when sandbox is detected\n3. Display informative message when sandbox is detected\n\n**Implementation:**\n- Add isSandboxed() function in cmd/bd/main.go\n- Auto-set sandboxMode = true in PersistentPreRun when detected\n- Show: 'ℹ️ Sandbox detected, using direct mode'\n\n**References:**\n- docs/GH353_INVESTIGATION.md (Solution 3, lines 120-160)\n- Depends on: Phase 1 (bd-???)\n\n**Acceptance Criteria:**\n- Codex users don't need to manually specify --sandbox\n- No false positives in normal environments\n- Clear messaging when auto-detection triggers","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-21T18:51:57.254358-05:00","updated_at":"2025-11-21T23:53:00.48278-08:00","closed_at":"2025-11-21T19:28:24.467713-05:00","source_repo":"."}
|
{"id":"bd-u3t","content_hash":"0666b1c7fb8f72d592027b74221e620e5f6aeb71a2ab4c3bcc15df190dc6a037","title":"Phase 2: Implement sandbox auto-detection for GH #353","description":"Implement automatic sandbox detection to improve UX for users in sandboxed environments (e.g., Codex).\n\n**Tasks:**\n1. Implement sandbox detection heuristic using syscall.Kill permission checks\n2. Auto-enable --sandbox mode when sandbox is detected\n3. Display informative message when sandbox is detected\n\n**Implementation:**\n- Add isSandboxed() function in cmd/bd/main.go\n- Auto-set sandboxMode = true in PersistentPreRun when detected\n- Show: 'ℹ️ Sandbox detected, using direct mode'\n\n**References:**\n- docs/GH353_INVESTIGATION.md (Solution 3, lines 120-160)\n- Depends on: Phase 1 (bd-???)\n\n**Acceptance Criteria:**\n- Codex users don't need to manually specify --sandbox\n- No false positives in normal environments\n- Clear messaging when auto-detection triggers","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-21T18:51:57.254358-05:00","updated_at":"2025-11-21T23:53:00.48278-08:00","closed_at":"2025-11-21T19:28:24.467713-05:00","source_repo":"."}
|
||||||
{"id":"bd-u4f5","content_hash":"89c6ae8745a842541c9a2025222c2c2e67e17b4fc33e0e56e58a37f0c5935939","title":"bd import silently succeeds when database matches working tree but not git HEAD","description":"**Critical**: bd import reports '0 created, 0 updated' when database matches working tree JSONL, even when working tree is ahead of git HEAD. This gives false confidence that everything is synced with the source of truth.\n\n## Reproduction\n\n1. Start with database synced to working tree .beads/issues.jsonl (376 issues)\n2. Git HEAD has older version of .beads/issues.jsonl (354 issues)\n3. Run: bd import .beads/issues.jsonl\n4. Output: 'Import complete: 0 created, 0 updated'\n\n## Problem\n\nUser expects 'bd import' after 'git pull' to sync database with committed state, but:\n- Command silently succeeds because DB already matches working tree\n- No warning that working tree has uncommitted changes\n- User falsely believes everything is synced with git\n- Violates 'JSONL in git is source of truth' principle\n\n## Expected Behavior\n\nWhen .beads/issues.jsonl differs from git HEAD, bd import should:\n1. Detect uncommitted changes: git diff --quiet HEAD .beads/issues.jsonl\n2. Warn user: 'Warning: .beads/issues.jsonl has uncommitted changes (376 lines vs 354 in HEAD)'\n3. Clarify status: 'Import complete: 0 created, 0 updated (already synced with working tree)'\n4. Recommend: 'Run git diff .beads/issues.jsonl to review uncommitted work'\n\n## Impact\n\n- Users can't trust 'bd import' status messages\n- Silent data loss risk if user assumes synced and runs git checkout\n- Breaks mental model of 'JSONL in git = source of truth'\n- Critical for VC's landing-the-plane workflow","acceptance_criteria":"1. bd import detects when working tree differs from git HEAD\n2. Warning emitted if JSONL has uncommitted changes \n3. Status message clarifies 'synced with working tree' vs 'synced with git'\n4. Optional flag to suppress warning (e.g., --working-tree mode)\n5. Documentation updated to explain import behavior with uncommitted changes\n6. Test case: import with dirty working tree shows warning","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-07T23:51:28.536822-08:00","updated_at":"2025-11-07T23:58:34.482313-08:00","closed_at":"2025-11-07T23:58:34.482313-08:00","source_repo":".","labels":["data-integrity"]}
|
{"id":"bd-u4f5","content_hash":"89c6ae8745a842541c9a2025222c2c2e67e17b4fc33e0e56e58a37f0c5935939","title":"bd import silently succeeds when database matches working tree but not git HEAD","description":"**Critical**: bd import reports '0 created, 0 updated' when database matches working tree JSONL, even when working tree is ahead of git HEAD. This gives false confidence that everything is synced with the source of truth.\n\n## Reproduction\n\n1. Start with database synced to working tree .beads/issues.jsonl (376 issues)\n2. Git HEAD has older version of .beads/issues.jsonl (354 issues)\n3. Run: bd import .beads/issues.jsonl\n4. Output: 'Import complete: 0 created, 0 updated'\n\n## Problem\n\nUser expects 'bd import' after 'git pull' to sync database with committed state, but:\n- Command silently succeeds because DB already matches working tree\n- No warning that working tree has uncommitted changes\n- User falsely believes everything is synced with git\n- Violates 'JSONL in git is source of truth' principle\n\n## Expected Behavior\n\nWhen .beads/issues.jsonl differs from git HEAD, bd import should:\n1. Detect uncommitted changes: git diff --quiet HEAD .beads/issues.jsonl\n2. Warn user: 'Warning: .beads/issues.jsonl has uncommitted changes (376 lines vs 354 in HEAD)'\n3. Clarify status: 'Import complete: 0 created, 0 updated (already synced with working tree)'\n4. Recommend: 'Run git diff .beads/issues.jsonl to review uncommitted work'\n\n## Impact\n\n- Users can't trust 'bd import' status messages\n- Silent data loss risk if user assumes synced and runs git checkout\n- Breaks mental model of 'JSONL in git = source of truth'\n- Critical for VC's landing-the-plane workflow","acceptance_criteria":"1. bd import detects when working tree differs from git HEAD\n2. Warning emitted if JSONL has uncommitted changes \n3. Status message clarifies 'synced with working tree' vs 'synced with git'\n4. Optional flag to suppress warning (e.g., --working-tree mode)\n5. Documentation updated to explain import behavior with uncommitted changes\n6. Test case: import with dirty working tree shows warning","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-07T23:51:28.536822-08:00","updated_at":"2025-11-07T23:58:34.482313-08:00","closed_at":"2025-11-07T23:58:34.482313-08:00","source_repo":".","labels":["data-integrity"]}
|
||||||
|
{"id":"bd-u4sb","content_hash":"320ddd043171d0f072f1a7ca11ac2f4fe06ffd9bf612771f44523df426b53d1f","title":"bd doctor should validate metadata.json version tracking","description":"bd doctor should check that metadata.json has the LastBdVersion field and that it's valid.\n\nChecks to add:\n- metadata.json exists and is valid JSON\n- LastBdVersion field is present\n- LastBdVersion is a valid semver-like string (or empty on first run)\n- Optionally warn if LastBdVersion is very old (\u003e 10 versions behind)\n\nThis helps ensure version tracking (bd-loka) is working correctly.\n\nRelated: bd-loka","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-23T17:19:08.140971-08:00","updated_at":"2025-11-23T17:27:54.857447-08:00","closed_at":"2025-11-23T17:27:54.857447-08:00","source_repo":"."}
|
||||||
{"id":"bd-u8j","content_hash":"91f39bbd4f2394592407c77917682b2c7c3a0b6415a3572eb75a49b0486a17fe","title":"Clarify exclusive lock protocol compatibility with multi-repo","description":"The contributor-workflow-analysis.md proposes per-repo file locking (Decision #7) using flock on JSONL files. However, VC (a downstream library consumer) uses an exclusive lock protocol (vc-195, requires Beads v0.17.3+) that allows bd daemon and VC executor to coexist.\n\nNeed to clarify:\n- Does the proposed per-repo file locking work with VC's existing exclusive lock protocol?\n- Do library consumers like VC need to adapt their locking logic?\n- Can multiple repos be locked atomically for cross-repo operations?\n\nContext: contributor-workflow-analysis.md lines 662-681","acceptance_criteria":"- Documentation explicitly states compatibility or incompatibility with existing lock protocols\n- If incompatible, migration path is documented for library consumers\n- If compatible, example showing coexistence is provided","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-03T20:24:08.257493-08:00","updated_at":"2025-11-05T14:15:01.506885-08:00","closed_at":"2025-11-05T14:15:01.506885-08:00","source_repo":"."}
|
{"id":"bd-u8j","content_hash":"91f39bbd4f2394592407c77917682b2c7c3a0b6415a3572eb75a49b0486a17fe","title":"Clarify exclusive lock protocol compatibility with multi-repo","description":"The contributor-workflow-analysis.md proposes per-repo file locking (Decision #7) using flock on JSONL files. However, VC (a downstream library consumer) uses an exclusive lock protocol (vc-195, requires Beads v0.17.3+) that allows bd daemon and VC executor to coexist.\n\nNeed to clarify:\n- Does the proposed per-repo file locking work with VC's existing exclusive lock protocol?\n- Do library consumers like VC need to adapt their locking logic?\n- Can multiple repos be locked atomically for cross-repo operations?\n\nContext: contributor-workflow-analysis.md lines 662-681","acceptance_criteria":"- Documentation explicitly states compatibility or incompatibility with existing lock protocols\n- If incompatible, migration path is documented for library consumers\n- If compatible, example showing coexistence is provided","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-03T20:24:08.257493-08:00","updated_at":"2025-11-05T14:15:01.506885-08:00","closed_at":"2025-11-05T14:15:01.506885-08:00","source_repo":"."}
|
||||||
{"id":"bd-uiae","content_hash":"5c184901daaa674a0f1224a29ab789019b53da6d5b5b4d6ac943e7d5d4846b3e","title":"Update documentation for beads-merge integration","description":"Document the integrated merge functionality.\n\n**Updates needed**:\n- AGENTS.md: Replace \"use external beads-merge\" with \"bd merge\"\n- README.md: Add git merge driver section\n- TROUBLESHOOTING.md: Update merge conflict resolution\n- ADVANCED.md: Document 3-way merge algorithm\n- Create CREDITS.md or ATTRIBUTION.md for @neongreen\n\n**Highlight**: Deletion sync fix (bd-hv01)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-05T18:42:20.488998-08:00","updated_at":"2025-11-06T18:19:16.234758-08:00","closed_at":"2025-11-06T15:40:27.830475-08:00","source_repo":".","dependencies":[{"issue_id":"bd-uiae","depends_on_id":"bd-qqvw","type":"parent-child","created_at":"2025-11-05T18:42:28.752447-08:00","created_by":"daemon"}]}
|
{"id":"bd-uiae","content_hash":"5c184901daaa674a0f1224a29ab789019b53da6d5b5b4d6ac943e7d5d4846b3e","title":"Update documentation for beads-merge integration","description":"Document the integrated merge functionality.\n\n**Updates needed**:\n- AGENTS.md: Replace \"use external beads-merge\" with \"bd merge\"\n- README.md: Add git merge driver section\n- TROUBLESHOOTING.md: Update merge conflict resolution\n- ADVANCED.md: Document 3-way merge algorithm\n- Create CREDITS.md or ATTRIBUTION.md for @neongreen\n\n**Highlight**: Deletion sync fix (bd-hv01)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-05T18:42:20.488998-08:00","updated_at":"2025-11-06T18:19:16.234758-08:00","closed_at":"2025-11-06T15:40:27.830475-08:00","source_repo":".","dependencies":[{"issue_id":"bd-uiae","depends_on_id":"bd-qqvw","type":"parent-child","created_at":"2025-11-05T18:42:28.752447-08:00","created_by":"daemon"}]}
|
||||||
{"id":"bd-urob","content_hash":"fc0e79260f5f6860fa8884859c4b33b18f9cc2dad361c1c1abb9bdeb412479b5","title":"bd-hv01: Refactor snapshot management into dedicated module","description":"Problem: Snapshot logic is scattered across deletion_tracking.go. Would benefit from abstraction with SnapshotManager type.\n\nBenefits: cleaner separation of concerns, easier to test in isolation, better encapsulation, could add observability/metrics.\n\nSuggested improvements: add magic constants, track merge statistics, better error messages.\n\nFiles: cmd/bd/deletion_tracking.go (refactor into new snapshot_manager.go)","status":"closed","priority":3,"issue_type":"chore","created_at":"2025-11-06T18:16:27.943666-08:00","updated_at":"2025-11-08T02:24:24.686744-08:00","closed_at":"2025-11-08T02:19:14.152412-08:00","source_repo":".","dependencies":[{"issue_id":"bd-urob","depends_on_id":"bd-rbxi","type":"parent-child","created_at":"2025-11-06T18:19:15.192447-08:00","created_by":"daemon"}]}
|
{"id":"bd-urob","content_hash":"fc0e79260f5f6860fa8884859c4b33b18f9cc2dad361c1c1abb9bdeb412479b5","title":"bd-hv01: Refactor snapshot management into dedicated module","description":"Problem: Snapshot logic is scattered across deletion_tracking.go. Would benefit from abstraction with SnapshotManager type.\n\nBenefits: cleaner separation of concerns, easier to test in isolation, better encapsulation, could add observability/metrics.\n\nSuggested improvements: add magic constants, track merge statistics, better error messages.\n\nFiles: cmd/bd/deletion_tracking.go (refactor into new snapshot_manager.go)","status":"closed","priority":3,"issue_type":"chore","created_at":"2025-11-06T18:16:27.943666-08:00","updated_at":"2025-11-08T02:24:24.686744-08:00","closed_at":"2025-11-08T02:19:14.152412-08:00","source_repo":".","dependencies":[{"issue_id":"bd-urob","depends_on_id":"bd-rbxi","type":"parent-child","created_at":"2025-11-06T18:19:15.192447-08:00","created_by":"daemon"}]}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
"database": "beads.db",
|
"database": "beads.db",
|
||||||
"jsonl_export": "beads.jsonl",
|
"jsonl_export": "beads.jsonl",
|
||||||
"last_bd_version": "0.24.2"
|
"last_bd_version": "0.24.2"
|
||||||
}
|
}
|
||||||
138
cmd/bd/doctor.go
138
cmd/bd/doctor.go
@@ -70,6 +70,7 @@ This command checks:
|
|||||||
- Circular dependencies
|
- Circular dependencies
|
||||||
- Git hooks (pre-commit, post-merge, pre-push)
|
- Git hooks (pre-commit, post-merge, pre-push)
|
||||||
- .beads/.gitignore up to date
|
- .beads/.gitignore up to date
|
||||||
|
- Metadata.json version tracking (LastBdVersion field)
|
||||||
|
|
||||||
Performance Mode (--perf):
|
Performance Mode (--perf):
|
||||||
Run performance diagnostics on your database:
|
Run performance diagnostics on your database:
|
||||||
@@ -342,6 +343,11 @@ func runDiagnostics(path string) doctorResult {
|
|||||||
result.Checks = append(result.Checks, mergeDriverCheck)
|
result.Checks = append(result.Checks, mergeDriverCheck)
|
||||||
// Don't fail overall check for merge driver, just warn
|
// Don't fail overall check for merge driver, just warn
|
||||||
|
|
||||||
|
// Check 16: Metadata.json version tracking (bd-u4sb)
|
||||||
|
metadataCheck := checkMetadataVersionTracking(path)
|
||||||
|
result.Checks = append(result.Checks, metadataCheck)
|
||||||
|
// Don't fail overall check for metadata, just warn
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1586,6 +1592,138 @@ func checkMergeDriver(path string) doctorCheck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkMetadataVersionTracking(path string) doctorCheck {
|
||||||
|
beadsDir := filepath.Join(path, ".beads")
|
||||||
|
|
||||||
|
// Load metadata.json
|
||||||
|
cfg, err := configfile.Load(beadsDir)
|
||||||
|
if err != nil {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Metadata Version Tracking",
|
||||||
|
Status: statusError,
|
||||||
|
Message: "Unable to read metadata.json",
|
||||||
|
Detail: err.Error(),
|
||||||
|
Fix: "Ensure metadata.json exists and is valid JSON. Run 'bd init' if needed.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if LastBdVersion field is present
|
||||||
|
if cfg.LastBdVersion == "" {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Metadata Version Tracking",
|
||||||
|
Status: statusWarning,
|
||||||
|
Message: "LastBdVersion field is empty (first run)",
|
||||||
|
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) {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Metadata Version Tracking",
|
||||||
|
Status: statusWarning,
|
||||||
|
Message: fmt.Sprintf("LastBdVersion has invalid format: %q", cfg.LastBdVersion),
|
||||||
|
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(Version, cfg.LastBdVersion)
|
||||||
|
if versionDiff > 0 {
|
||||||
|
// Current version is newer - check how far behind
|
||||||
|
currentParts := parseVersionParts(Version)
|
||||||
|
lastParts := parseVersionParts(cfg.LastBdVersion)
|
||||||
|
|
||||||
|
// Simple heuristic: warn if minor version is 10+ behind or major version differs by 1+
|
||||||
|
majorDiff := currentParts[0] - lastParts[0]
|
||||||
|
minorDiff := currentParts[1] - lastParts[1]
|
||||||
|
|
||||||
|
if majorDiff >= 1 || (majorDiff == 0 && minorDiff >= 10) {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Metadata Version Tracking",
|
||||||
|
Status: statusWarning,
|
||||||
|
Message: fmt.Sprintf("LastBdVersion is very old: %s (current: %s)", cfg.LastBdVersion, Version),
|
||||||
|
Detail: "You may have missed important upgrade notifications",
|
||||||
|
Fix: "Run 'bd upgrade review' to see recent changes",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version is behind but not too old
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Metadata Version Tracking",
|
||||||
|
Status: statusOK,
|
||||||
|
Message: fmt.Sprintf("Version tracking active (last: %s, current: %s)", cfg.LastBdVersion, Version),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version is current or ahead (shouldn't happen, but handle it)
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Metadata Version Tracking",
|
||||||
|
Status: statusOK,
|
||||||
|
Message: fmt.Sprintf("Version tracking active (version: %s)", cfg.LastBdVersion),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidSemver checks if a version string is valid semver-like format (X.Y.Z)
|
||||||
|
func isValidSemver(version string) bool {
|
||||||
|
if version == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by dots and ensure all parts are numeric
|
||||||
|
versionParts := strings.Split(version, ".")
|
||||||
|
if len(versionParts) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse each part to ensure it's a valid number
|
||||||
|
for _, part := range versionParts {
|
||||||
|
if part == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var num int
|
||||||
|
if _, err := fmt.Sscanf(part, "%d", &num); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if num < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseVersionParts parses version string into numeric parts
|
||||||
|
// Returns [major, minor, patch, ...] or empty slice on error
|
||||||
|
func parseVersionParts(version string) []int {
|
||||||
|
parts := strings.Split(version, ".")
|
||||||
|
result := make([]int, 0, len(parts))
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
var num int
|
||||||
|
if _, err := fmt.Sscanf(part, "%d", &num); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result = append(result, num)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(doctorCmd)
|
rootCmd.AddCommand(doctorCmd)
|
||||||
doctorCmd.Flags().BoolVar(&perfMode, "perf", false, "Run performance diagnostics and generate CPU profile")
|
doctorCmd.Flags().BoolVar(&perfMode, "perf", false, "Run performance diagnostics and generate CPU profile")
|
||||||
|
|||||||
@@ -680,3 +680,171 @@ func TestCheckGitHooks(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckMetadataVersionTracking(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupMetadata 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)
|
||||||
|
},
|
||||||
|
expectedStatus: 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)
|
||||||
|
},
|
||||||
|
expectedStatus: 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)
|
||||||
|
},
|
||||||
|
expectedStatus: 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)
|
||||||
|
},
|
||||||
|
expectedStatus: 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)
|
||||||
|
},
|
||||||
|
expectedStatus: 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: statusError,
|
||||||
|
expectWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing metadata.json",
|
||||||
|
setupMetadata: func(beadsDir string) error {
|
||||||
|
// Don't create metadata.json
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
expectedStatus: statusWarning,
|
||||||
|
expectWarning: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||||
|
if err := os.Mkdir(beadsDir, 0750); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup metadata.json
|
||||||
|
if err := tc.setupMetadata(beadsDir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
check := checkMetadataVersionTracking(tmpDir)
|
||||||
|
|
||||||
|
if check.Status != tc.expectedStatus {
|
||||||
|
t.Errorf("Expected status %s, got %s (message: %s)", tc.expectedStatus, check.Status, check.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectWarning && check.Status == statusWarning && check.Fix == "" {
|
||||||
|
t.Error("Expected fix message for warning status")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsValidSemver(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
version string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"0.24.2", true},
|
||||||
|
{"1.0.0", true},
|
||||||
|
{"0.1", true}, // Major.minor is valid
|
||||||
|
{"1", true}, // Just major is valid
|
||||||
|
{"", false}, // Empty is invalid
|
||||||
|
{"invalid", false}, // Non-numeric is invalid
|
||||||
|
{"0.a.2", false}, // Letters in parts are invalid
|
||||||
|
{"1.2.3.4", true}, // Extra parts are ok
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
result := isValidSemver(tc.version)
|
||||||
|
if result != tc.expected {
|
||||||
|
t.Errorf("isValidSemver(%q) = %v, expected %v", tc.version, result, tc.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseVersionParts(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
version string
|
||||||
|
expected []int
|
||||||
|
}{
|
||||||
|
{"0.24.2", []int{0, 24, 2}},
|
||||||
|
{"1.0.0", []int{1, 0, 0}},
|
||||||
|
{"0.1", []int{0, 1}},
|
||||||
|
{"1", []int{1}},
|
||||||
|
{"", []int{}},
|
||||||
|
{"invalid", []int{}},
|
||||||
|
{"1.a.3", []int{1}}, // Stops at first non-numeric part
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
result := parseVersionParts(tc.version)
|
||||||
|
if len(result) != len(tc.expected) {
|
||||||
|
t.Errorf("parseVersionParts(%q) returned %d parts, expected %d", tc.version, len(result), len(tc.expected))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := range result {
|
||||||
|
if result[i] != tc.expected[i] {
|
||||||
|
t.Errorf("parseVersionParts(%q)[%d] = %d, expected %d", tc.version, i, result[i], tc.expected[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user