Merge branch 'main' of github.com:steveyegge/beads
This commit is contained in:
@@ -273,6 +273,10 @@ bd info
|
||||
|
||||
## Usage
|
||||
|
||||
### Health Check
|
||||
|
||||
Check installation health: `bd doctor` validates your `.beads/` setup, database version, ID format, and CLI version. Provides actionable fixes for any issues found.
|
||||
|
||||
### Creating Issues
|
||||
|
||||
```bash
|
||||
|
||||
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