DOCTOR IMPROVEMENTS: visual improvements/grouping + add comprehensive tests + fix gosec warnings (#656)
* test(doctor): add comprehensive tests for fix and check functions Add edge case tests, e2e tests, and improve test coverage for: - database_test.go: database integrity and sync checks - git_test.go: git hooks, merge driver, sync branch tests - gitignore_test.go: gitignore validation - prefix_test.go: ID prefix handling - fix/fix_test.go: fix operations - fix/e2e_test.go: end-to-end fix scenarios - fix/fix_edge_cases_test.go: edge case handling * docs: add testing philosophy and anti-patterns guide - Create TESTING_PHILOSOPHY.md covering test pyramid, priority matrix, what NOT to test, and 5 anti-patterns with code examples - Add cross-reference from README_TESTING.md - Document beads-specific guidance (well-covered areas vs gaps) - Include target metrics (test-to-code ratio, execution time targets) * chore: revert .beads/ to upstream/main state * refactor(doctor): add category grouping and Ayu theme colors - Add Category field to DoctorCheck for organizing checks by type - Define category constants: Core, Git, Runtime, Data, Integration, Metadata - Update thanks command to use shared Ayu color palette from internal/ui - Simplify test fixtures by removing redundant test cases * fix(doctor): prevent test fork bomb and fix test failures - Add ErrTestBinary guard in getBdBinary() to prevent tests from recursively executing the test binary when calling bd subcommands - Update claude_test.go to use new check names (CLI Availability, Prime Documentation) - Fix syncbranch test path comparison by resolving symlinks (/var vs /private/var on macOS) - Fix permissions check to use exact comparison instead of bitmask - Fix UntrackedJSONL to use git commit --only to preserve staged changes - Fix MergeDriver edge case test by making both .git dir and config read-only - Add skipIfTestBinary helper for E2E tests that need real bd binary * test(doctor): skip read-only config test in CI environments GitHub Actions containers may have CAP_DAC_OVERRIDE or similar capabilities that allow writing to read-only files, causing the test to fail. Skip the test when CI=true or GITHUB_ACTIONS=true.
This commit is contained in:
@@ -6,129 +6,81 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckIDFormat(t *testing.T) {
|
||||
t.Run("no beads directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
check := CheckIDFormat(tmpDir)
|
||||
// TestIntegrityChecks_NoBeadsDir verifies all integrity check functions handle
|
||||
// missing .beads directories gracefully. This replaces 4 individual subtests.
|
||||
func TestIntegrityChecks_NoBeadsDir(t *testing.T) {
|
||||
checks := []struct {
|
||||
name string
|
||||
fn func(string) DoctorCheck
|
||||
wantName string
|
||||
}{
|
||||
{"IDFormat", CheckIDFormat, "Issue IDs"},
|
||||
{"DependencyCycles", CheckDependencyCycles, "Dependency Cycles"},
|
||||
{"Tombstones", CheckTombstones, "Tombstones"},
|
||||
{"DeletionsManifest", CheckDeletionsManifest, "Deletions Manifest"},
|
||||
}
|
||||
|
||||
// Should handle missing .beads gracefully
|
||||
if check.Name != "Issue IDs" {
|
||||
t.Errorf("Name = %q, want %q", check.Name, "Issue IDs")
|
||||
}
|
||||
})
|
||||
for _, tc := range checks {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
result := tc.fn(tmpDir)
|
||||
|
||||
t.Run("no database file", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
check := CheckIDFormat(tmpDir)
|
||||
|
||||
// Should report "will use hash-based IDs" for new install
|
||||
if check.Status != StatusOK {
|
||||
t.Errorf("Status = %q, want %q", check.Status, StatusOK)
|
||||
}
|
||||
})
|
||||
if result.Name != tc.wantName {
|
||||
t.Errorf("Name = %q, want %q", result.Name, tc.wantName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDependencyCycles(t *testing.T) {
|
||||
t.Run("no beads directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
check := CheckDependencyCycles(tmpDir)
|
||||
// TestIntegrityChecks_EmptyBeadsDir verifies all integrity check functions return OK
|
||||
// when .beads directory exists but is empty (no database/files to check).
|
||||
func TestIntegrityChecks_EmptyBeadsDir(t *testing.T) {
|
||||
checks := []struct {
|
||||
name string
|
||||
fn func(string) DoctorCheck
|
||||
}{
|
||||
{"IDFormat", CheckIDFormat},
|
||||
{"DependencyCycles", CheckDependencyCycles},
|
||||
{"Tombstones", CheckTombstones},
|
||||
{"DeletionsManifest", CheckDeletionsManifest},
|
||||
}
|
||||
|
||||
// Should handle missing directory gracefully
|
||||
if check.Name != "Dependency Cycles" {
|
||||
t.Errorf("Name = %q, want %q", check.Name, "Dependency Cycles")
|
||||
}
|
||||
})
|
||||
for _, tc := range checks {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("no database", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := tc.fn(tmpDir)
|
||||
|
||||
check := CheckDependencyCycles(tmpDir)
|
||||
|
||||
// Should return OK when no database (nothing to check)
|
||||
if check.Status != StatusOK {
|
||||
t.Errorf("Status = %q, want %q", check.Status, StatusOK)
|
||||
}
|
||||
})
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %q, want %q", result.Status, StatusOK)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckTombstones(t *testing.T) {
|
||||
t.Run("no beads directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
check := CheckTombstones(tmpDir)
|
||||
// TestCheckDeletionsManifest_LegacyFile tests the specific case where a legacy
|
||||
// deletions.jsonl file exists and should trigger a warning.
|
||||
func TestCheckDeletionsManifest_LegacyFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should handle missing directory
|
||||
if check.Name != "Tombstones" {
|
||||
t.Errorf("Name = %q, want %q", check.Name, "Tombstones")
|
||||
}
|
||||
})
|
||||
// Create a deletions.jsonl file
|
||||
deletionsPath := filepath.Join(beadsDir, "deletions.jsonl")
|
||||
if err := os.WriteFile(deletionsPath, []byte(`{"id":"test-1"}`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("empty beads directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check := CheckDeletionsManifest(tmpDir)
|
||||
|
||||
check := CheckTombstones(tmpDir)
|
||||
|
||||
// Should return OK when no tombstones file
|
||||
if check.Status != StatusOK {
|
||||
t.Errorf("Status = %q, want %q", check.Status, StatusOK)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckDeletionsManifest(t *testing.T) {
|
||||
t.Run("no beads directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
check := CheckDeletionsManifest(tmpDir)
|
||||
|
||||
if check.Name != "Deletions Manifest" {
|
||||
t.Errorf("Name = %q, want %q", check.Name, "Deletions Manifest")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no deletions file", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
check := CheckDeletionsManifest(tmpDir)
|
||||
|
||||
// Should return OK when no deletions.jsonl (nothing to migrate)
|
||||
if check.Status != StatusOK {
|
||||
t.Errorf("Status = %q, want %q", check.Status, StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("has deletions file", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.Mkdir(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Create a deletions.jsonl file
|
||||
deletionsPath := filepath.Join(beadsDir, "deletions.jsonl")
|
||||
if err := os.WriteFile(deletionsPath, []byte(`{"id":"test-1"}`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
check := CheckDeletionsManifest(tmpDir)
|
||||
|
||||
// Should warn about legacy deletions file
|
||||
if check.Status != StatusWarning {
|
||||
t.Errorf("Status = %q, want %q", check.Status, StatusWarning)
|
||||
}
|
||||
})
|
||||
// Should warn about legacy deletions file
|
||||
if check.Status != StatusWarning {
|
||||
t.Errorf("Status = %q, want %q", check.Status, StatusWarning)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user