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.
This commit is contained in:
501
cmd/bd/doctor.go
Normal file
501
cmd/bd/doctor.go
Normal file
@@ -0,0 +1,501 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Status constants for doctor checks
|
||||
const (
|
||||
statusOK = "ok"
|
||||
statusWarning = "warning"
|
||||
statusError = "error"
|
||||
)
|
||||
|
||||
type doctorCheck struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"` // statusOK, statusWarning, or statusError
|
||||
Message string `json:"message"`
|
||||
Detail string `json:"detail,omitempty"` // Additional detail like storage type
|
||||
Fix string `json:"fix,omitempty"`
|
||||
}
|
||||
|
||||
type doctorResult struct {
|
||||
Path string `json:"path"`
|
||||
Checks []doctorCheck `json:"checks"`
|
||||
OverallOK bool `json:"overall_ok"`
|
||||
CLIVersion string `json:"cli_version"`
|
||||
}
|
||||
|
||||
var doctorCmd = &cobra.Command{
|
||||
Use: "doctor [path]",
|
||||
Short: "Check beads installation health",
|
||||
Long: `Sanity check the beads installation for the current directory or specified path.
|
||||
|
||||
This command checks:
|
||||
- If .beads/ directory exists
|
||||
- Database version and schema compatibility
|
||||
- Whether using hash-based vs sequential IDs
|
||||
- If CLI version is current (checks GitHub releases)
|
||||
|
||||
Examples:
|
||||
bd doctor # Check current directory
|
||||
bd doctor /path/to/repo # Check specific repository
|
||||
bd doctor --json # Machine-readable output`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Get json flag from command
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
// Determine path to check
|
||||
checkPath := "."
|
||||
if len(args) > 0 {
|
||||
checkPath = args[0]
|
||||
}
|
||||
|
||||
// Convert to absolute path
|
||||
absPath, err := filepath.Abs(checkPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to resolve path: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Run diagnostics
|
||||
result := runDiagnostics(absPath)
|
||||
|
||||
// Output results
|
||||
if jsonOutput {
|
||||
outputJSON(result)
|
||||
} else {
|
||||
printDiagnostics(result)
|
||||
}
|
||||
|
||||
// Exit with error if any checks failed
|
||||
if !result.OverallOK {
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func runDiagnostics(path string) doctorResult {
|
||||
result := doctorResult{
|
||||
Path: path,
|
||||
CLIVersion: Version,
|
||||
OverallOK: true,
|
||||
}
|
||||
|
||||
// Check 1: Installation (.beads/ directory)
|
||||
installCheck := checkInstallation(path)
|
||||
result.Checks = append(result.Checks, installCheck)
|
||||
if installCheck.Status != statusOK {
|
||||
result.OverallOK = false
|
||||
// If no .beads/, skip other checks
|
||||
return result
|
||||
}
|
||||
|
||||
// Check 2: Database version
|
||||
dbCheck := checkDatabaseVersion(path)
|
||||
result.Checks = append(result.Checks, dbCheck)
|
||||
if dbCheck.Status == statusError {
|
||||
result.OverallOK = false
|
||||
}
|
||||
|
||||
// Check 3: ID format (hash vs sequential)
|
||||
idCheck := checkIDFormat(path)
|
||||
result.Checks = append(result.Checks, idCheck)
|
||||
if idCheck.Status == statusWarning {
|
||||
result.OverallOK = false
|
||||
}
|
||||
|
||||
// Check 4: CLI version (GitHub)
|
||||
versionCheck := checkCLIVersion()
|
||||
result.Checks = append(result.Checks, versionCheck)
|
||||
// Don't fail overall check for outdated CLI, just warn
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func checkInstallation(path string) doctorCheck {
|
||||
beadsDir := filepath.Join(path, ".beads")
|
||||
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||
// Auto-detect prefix from directory name
|
||||
prefix := filepath.Base(path)
|
||||
prefix = strings.TrimRight(prefix, "-")
|
||||
|
||||
return doctorCheck{
|
||||
Name: "Installation",
|
||||
Status: statusError,
|
||||
Message: "No .beads/ directory found",
|
||||
Fix: fmt.Sprintf("Run 'bd init --prefix %s' to initialize beads", prefix),
|
||||
}
|
||||
}
|
||||
|
||||
return doctorCheck{
|
||||
Name: "Installation",
|
||||
Status: statusOK,
|
||||
Message: ".beads/ directory found",
|
||||
}
|
||||
}
|
||||
|
||||
func checkDatabaseVersion(path string) doctorCheck {
|
||||
beadsDir := filepath.Join(path, ".beads")
|
||||
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
|
||||
// Check if database file exists
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
// Check if JSONL exists (--no-db mode)
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
if _, err := os.Stat(jsonlPath); err == nil {
|
||||
return doctorCheck{
|
||||
Name: "Database",
|
||||
Status: statusOK,
|
||||
Message: "JSONL-only mode",
|
||||
Detail: "Using issues.jsonl (no SQLite database)",
|
||||
}
|
||||
}
|
||||
|
||||
return doctorCheck{
|
||||
Name: "Database",
|
||||
Status: statusError,
|
||||
Message: "No beads.db found",
|
||||
Fix: "Run 'bd init' to create database",
|
||||
}
|
||||
}
|
||||
|
||||
// Get database version
|
||||
dbVersion := getDatabaseVersionFromPath(dbPath)
|
||||
|
||||
if dbVersion == "unknown" {
|
||||
return doctorCheck{
|
||||
Name: "Database",
|
||||
Status: statusError,
|
||||
Message: "Unable to read database version",
|
||||
Detail: "Storage: SQLite",
|
||||
Fix: "Database may be corrupted. Try 'bd migrate'",
|
||||
}
|
||||
}
|
||||
|
||||
if dbVersion == "pre-0.17.5" {
|
||||
return doctorCheck{
|
||||
Name: "Database",
|
||||
Status: statusWarning,
|
||||
Message: fmt.Sprintf("version %s (very old)", dbVersion),
|
||||
Detail: "Storage: SQLite",
|
||||
Fix: "Run 'bd migrate' to upgrade database schema",
|
||||
}
|
||||
}
|
||||
|
||||
if dbVersion != Version {
|
||||
return doctorCheck{
|
||||
Name: "Database",
|
||||
Status: statusWarning,
|
||||
Message: fmt.Sprintf("version %s (CLI: %s)", dbVersion, Version),
|
||||
Detail: "Storage: SQLite",
|
||||
Fix: "Run 'bd migrate' to sync database with CLI version",
|
||||
}
|
||||
}
|
||||
|
||||
return doctorCheck{
|
||||
Name: "Database",
|
||||
Status: statusOK,
|
||||
Message: fmt.Sprintf("version %s", dbVersion),
|
||||
Detail: "Storage: SQLite",
|
||||
}
|
||||
}
|
||||
|
||||
func checkIDFormat(path string) doctorCheck {
|
||||
beadsDir := filepath.Join(path, ".beads")
|
||||
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
|
||||
// Check if using JSONL-only mode
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
// Check if JSONL exists (--no-db mode)
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
if _, err := os.Stat(jsonlPath); err == nil {
|
||||
return doctorCheck{
|
||||
Name: "Issue IDs",
|
||||
Status: statusOK,
|
||||
Message: "N/A (JSONL-only mode)",
|
||||
}
|
||||
}
|
||||
// No database and no JSONL
|
||||
return doctorCheck{
|
||||
Name: "Issue IDs",
|
||||
Status: statusOK,
|
||||
Message: "No issues yet (will use hash-based IDs)",
|
||||
}
|
||||
}
|
||||
|
||||
// Open database
|
||||
db, err := sql.Open("sqlite", dbPath+"?mode=ro")
|
||||
if err != nil {
|
||||
return doctorCheck{
|
||||
Name: "Issue IDs",
|
||||
Status: statusError,
|
||||
Message: "Unable to open database",
|
||||
}
|
||||
}
|
||||
defer func() { _ = db.Close() }() // Intentionally ignore close error
|
||||
|
||||
// Get first issue to check ID format
|
||||
var issueID string
|
||||
err = db.QueryRow("SELECT id FROM issues ORDER BY created_at LIMIT 1").Scan(&issueID)
|
||||
if err == sql.ErrNoRows {
|
||||
return doctorCheck{
|
||||
Name: "Issue IDs",
|
||||
Status: statusOK,
|
||||
Message: "No issues yet (will use hash-based IDs)",
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return doctorCheck{
|
||||
Name: "Issue IDs",
|
||||
Status: statusError,
|
||||
Message: "Unable to query issues",
|
||||
}
|
||||
}
|
||||
|
||||
// Detect ID format
|
||||
if isHashID(issueID) {
|
||||
return doctorCheck{
|
||||
Name: "Issue IDs",
|
||||
Status: statusOK,
|
||||
Message: "hash-based ✓",
|
||||
}
|
||||
}
|
||||
|
||||
// Sequential IDs - recommend migration
|
||||
return doctorCheck{
|
||||
Name: "Issue IDs",
|
||||
Status: statusWarning,
|
||||
Message: "sequential (e.g., bd-1, bd-2, ...)",
|
||||
Fix: "Run 'bd migrate --to-hash-ids' to upgrade (prevents ID collisions in multi-worker scenarios)",
|
||||
}
|
||||
}
|
||||
|
||||
func checkCLIVersion() doctorCheck {
|
||||
latestVersion, err := fetchLatestGitHubRelease()
|
||||
if err != nil {
|
||||
// Network error or API issue - don't fail, just warn
|
||||
return doctorCheck{
|
||||
Name: "CLI Version",
|
||||
Status: statusOK,
|
||||
Message: fmt.Sprintf("%s (unable to check for updates)", Version),
|
||||
}
|
||||
}
|
||||
|
||||
if latestVersion == "" || latestVersion == Version {
|
||||
return doctorCheck{
|
||||
Name: "CLI Version",
|
||||
Status: statusOK,
|
||||
Message: fmt.Sprintf("%s (latest)", Version),
|
||||
}
|
||||
}
|
||||
|
||||
// Compare versions using simple semver-aware comparison
|
||||
if compareVersions(latestVersion, Version) > 0 {
|
||||
upgradeCmds := ` • Homebrew: brew upgrade bd
|
||||
• Script: curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash`
|
||||
|
||||
return doctorCheck{
|
||||
Name: "CLI Version",
|
||||
Status: statusWarning,
|
||||
Message: fmt.Sprintf("%s (latest: %s)", Version, latestVersion),
|
||||
Fix: fmt.Sprintf("Upgrade to latest version:\n%s", upgradeCmds),
|
||||
}
|
||||
}
|
||||
|
||||
return doctorCheck{
|
||||
Name: "CLI Version",
|
||||
Status: statusOK,
|
||||
Message: fmt.Sprintf("%s (latest)", Version),
|
||||
}
|
||||
}
|
||||
|
||||
func getDatabaseVersionFromPath(dbPath string) string {
|
||||
db, err := sql.Open("sqlite", dbPath+"?mode=ro")
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Try to read version from metadata table
|
||||
var version string
|
||||
err = db.QueryRow("SELECT value FROM metadata WHERE key = 'bd_version'").Scan(&version)
|
||||
if err == nil {
|
||||
return version
|
||||
}
|
||||
|
||||
// Check if metadata table exists
|
||||
var tableName string
|
||||
err = db.QueryRow(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='metadata'
|
||||
`).Scan(&tableName)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return "pre-0.17.5"
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// Note: isHashID is defined in migrate_hash_ids.go to avoid duplication
|
||||
|
||||
// compareVersions compares two semantic version strings.
|
||||
// Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
|
||||
// Handles versions like "0.20.1", "1.2.3", etc.
|
||||
func compareVersions(v1, v2 string) int {
|
||||
// Split versions into parts
|
||||
parts1 := strings.Split(v1, ".")
|
||||
parts2 := strings.Split(v2, ".")
|
||||
|
||||
// Compare each part
|
||||
maxLen := len(parts1)
|
||||
if len(parts2) > maxLen {
|
||||
maxLen = len(parts2)
|
||||
}
|
||||
|
||||
for i := 0; i < maxLen; i++ {
|
||||
var p1, p2 int
|
||||
|
||||
// Get part value or default to 0 if part doesn't exist
|
||||
if i < len(parts1) {
|
||||
fmt.Sscanf(parts1[i], "%d", &p1)
|
||||
}
|
||||
if i < len(parts2) {
|
||||
fmt.Sscanf(parts2[i], "%d", &p2)
|
||||
}
|
||||
|
||||
if p1 < p2 {
|
||||
return -1
|
||||
}
|
||||
if p1 > p2 {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func fetchLatestGitHubRelease() (string, error) {
|
||||
url := "https://api.github.com/repos/steveyegge/beads/releases/latest"
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Set User-Agent as required by GitHub API
|
||||
req.Header.Set("User-Agent", "beads-cli-doctor")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("github api returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &release); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Strip 'v' prefix if present
|
||||
version := strings.TrimPrefix(release.TagName, "v")
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func printDiagnostics(result doctorResult) {
|
||||
// Print header
|
||||
fmt.Println("\nDiagnostics")
|
||||
|
||||
// Print each check with tree formatting
|
||||
for i, check := range result.Checks {
|
||||
// Determine prefix
|
||||
prefix := "├"
|
||||
if i == len(result.Checks)-1 {
|
||||
prefix = "└"
|
||||
}
|
||||
|
||||
// Format status indicator
|
||||
var statusIcon string
|
||||
switch check.Status {
|
||||
case statusOK:
|
||||
statusIcon = ""
|
||||
case statusWarning:
|
||||
statusIcon = color.YellowString(" ⚠")
|
||||
case statusError:
|
||||
statusIcon = color.RedString(" ✗")
|
||||
}
|
||||
|
||||
// Print main check line
|
||||
fmt.Printf(" %s %s: %s%s\n", prefix, check.Name, check.Message, statusIcon)
|
||||
|
||||
// Print detail if present (indented under the check)
|
||||
if check.Detail != "" {
|
||||
detailPrefix := "│"
|
||||
if i == len(result.Checks)-1 {
|
||||
detailPrefix = " "
|
||||
}
|
||||
fmt.Printf(" %s %s\n", detailPrefix, color.New(color.Faint).Sprint(check.Detail))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
// Print warnings/errors with fixes
|
||||
hasIssues := false
|
||||
for _, check := range result.Checks {
|
||||
if check.Status != statusOK && check.Fix != "" {
|
||||
if !hasIssues {
|
||||
hasIssues = true
|
||||
}
|
||||
|
||||
switch check.Status {
|
||||
case statusWarning:
|
||||
color.Yellow("⚠ Warning: %s\n", check.Message)
|
||||
case statusError:
|
||||
color.Red("✗ Error: %s\n", check.Message)
|
||||
}
|
||||
|
||||
fmt.Printf(" Fix: %s\n\n", check.Fix)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasIssues {
|
||||
color.Green("✓ All checks passed\n")
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
doctorCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
rootCmd.AddCommand(doctorCmd)
|
||||
}
|
||||
173
cmd/bd/doctor_test.go
Normal file
173
cmd/bd/doctor_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -111,7 +112,8 @@ var rootCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
// Skip database initialization for commands that don't need a database
|
||||
if cmd.Name() == "init" || cmd.Name() == cmdDaemon || cmd.Name() == "help" || cmd.Name() == "version" || cmd.Name() == "quickstart" {
|
||||
noDbCommands := []string{"init", cmdDaemon, "help", "version", "quickstart", "doctor"}
|
||||
if slices.Contains(noDbCommands, cmd.Name()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user