Files
beads/cmd/bd/doctor_test.go
Ryan 08bfe133d0 Add 'bd doctor' command to sanity check installation (#189)
* Add bd doctor command for installation health checks

Implements a comprehensive health check command similar to claude doctor
that validates beads installation and provides actionable recommendations.

Features:
- Installation check (.beads/ directory exists)
- Database version verification (compares with CLI version)
- ID format detection (hash-based vs sequential)
- CLI version check (fetches latest from GitHub)
- Storage type detection (SQLite vs JSONL-only mode)
- Tree-style output with color-coded warnings
- JSON output for scripting (--json flag)
- Actionable fix recommendations for each issue

Implementation improvements:
- Status constants instead of magic strings
- Semantic version comparison (fixes 0.10.0 vs 0.9.9 edge case)
- Documented defer pattern for intentional error ignore
- Comprehensive test coverage including version comparison edge cases
- Clean integration using slices.Contains for command list

Usage:
  bd doctor              # Check current directory
  bd doctor /path/to/repo # Check specific repository
  bd doctor --json       # Machine-readable output

* Simplify bd doctor documentation in README

Reduce verbose health check section to 2 lines as requested.

* Fix bd doctor to handle JSONL-only mode for ID format check

When no SQLite database exists (JSONL-only mode), skip the ID format
check instead of showing an error. This prevents the confusing
'Unable to query issues' error when the installation is actually fine.
2025-10-31 11:41:13 -07:00

174 lines
4.6 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestDoctorNoBeadsDir(t *testing.T) {
// Create temporary directory
tmpDir := t.TempDir()
// Run diagnostics
result := runDiagnostics(tmpDir)
// Should fail overall
if result.OverallOK {
t.Error("Expected OverallOK to be false when .beads/ directory is missing")
}
// Check installation check failed
if len(result.Checks) == 0 {
t.Fatal("Expected at least one check")
}
installCheck := result.Checks[0]
if installCheck.Name != "Installation" {
t.Errorf("Expected first check to be Installation, got %s", installCheck.Name)
}
if installCheck.Status != "error" {
t.Errorf("Expected Installation status to be error, got %s", installCheck.Status)
}
if installCheck.Fix == "" {
t.Error("Expected Installation check to have a fix")
}
}
func TestDoctorWithBeadsDir(t *testing.T) {
// Create temporary directory with .beads
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
// Run diagnostics
result := runDiagnostics(tmpDir)
// Should have installation check passing
if len(result.Checks) == 0 {
t.Fatal("Expected at least one check")
}
installCheck := result.Checks[0]
if installCheck.Name != "Installation" {
t.Errorf("Expected first check to be Installation, got %s", installCheck.Name)
}
if installCheck.Status != "ok" {
t.Errorf("Expected Installation status to be ok, got %s", installCheck.Status)
}
}
func TestDoctorJSONOutput(t *testing.T) {
// Create temporary directory
tmpDir := t.TempDir()
// Run diagnostics
result := runDiagnostics(tmpDir)
// Marshal to JSON to verify structure
jsonBytes, err := json.Marshal(result)
if err != nil {
t.Fatalf("Failed to marshal result to JSON: %v", err)
}
// Unmarshal back to verify structure
var decoded doctorResult
if err := json.Unmarshal(jsonBytes, &decoded); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
// Verify key fields
if decoded.Path != result.Path {
t.Errorf("Path mismatch: %s != %s", decoded.Path, result.Path)
}
if decoded.CLIVersion != result.CLIVersion {
t.Errorf("CLIVersion mismatch: %s != %s", decoded.CLIVersion, result.CLIVersion)
}
if decoded.OverallOK != result.OverallOK {
t.Errorf("OverallOK mismatch: %v != %v", decoded.OverallOK, result.OverallOK)
}
if len(decoded.Checks) != len(result.Checks) {
t.Errorf("Checks length mismatch: %d != %d", len(decoded.Checks), len(result.Checks))
}
}
// Note: isHashID is tested in migrate_hash_ids_test.go
func TestCheckInstallation(t *testing.T) {
// Test with missing .beads directory
tmpDir := t.TempDir()
check := checkInstallation(tmpDir)
if check.Status != statusError {
t.Errorf("Expected error status, got %s", check.Status)
}
if check.Fix == "" {
t.Error("Expected fix to be provided")
}
// Test with existing .beads directory
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
check = checkInstallation(tmpDir)
if check.Status != statusOK {
t.Errorf("Expected ok status, got %s", check.Status)
}
}
func TestCheckDatabaseVersionJSONLMode(t *testing.T) {
// Create temporary directory with .beads but no database
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
// Create empty issues.jsonl to simulate --no-db mode
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
t.Fatal(err)
}
check := checkDatabaseVersion(tmpDir)
if check.Status != statusOK {
t.Errorf("Expected ok status for JSONL mode, got %s", check.Status)
}
if check.Message != "JSONL-only mode" {
t.Errorf("Expected JSONL-only mode message, got %s", check.Message)
}
if check.Detail == "" {
t.Error("Expected detail field to be set for JSONL mode")
}
}
func TestCompareVersions(t *testing.T) {
tests := []struct {
v1 string
v2 string
expected int
}{
{"0.20.1", "0.20.1", 0}, // Equal
{"0.20.1", "0.20.0", 1}, // v1 > v2
{"0.20.0", "0.20.1", -1}, // v1 < v2
{"0.10.0", "0.9.9", 1}, // Major.minor comparison
{"1.0.0", "0.99.99", 1}, // Major version difference
{"0.20.1", "0.3.0", 1}, // String comparison would fail this
{"1.2", "1.2.0", 0}, // Different length, equal
{"1.2.1", "1.2", 1}, // Different length, v1 > v2
}
for _, tc := range tests {
result := compareVersions(tc.v1, tc.v2)
if result != tc.expected {
t.Errorf("compareVersions(%q, %q) = %d, expected %d", tc.v1, tc.v2, result, tc.expected)
}
}
}