bd doctor: add Claude integration verification checks
Add two new checks to verify Claude Code integration: - CheckBdInPath: verifies 'bd' is in PATH (needed for hooks) - CheckDocumentationBdPrimeReference: checks if docs reference 'bd prime' and verifies the command exists (detects version mismatches) Closes bd-o78 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -327,6 +327,16 @@ func runDiagnostics(path string) doctorResult {
|
||||
result.Checks = append(result.Checks, claudeCheck)
|
||||
// Don't fail overall check for missing Claude integration, just warn
|
||||
|
||||
// Check 11a: bd in PATH (needed for Claude hooks to work)
|
||||
bdPathCheck := convertDoctorCheck(doctor.CheckBdInPath())
|
||||
result.Checks = append(result.Checks, bdPathCheck)
|
||||
// Don't fail overall check for missing bd in PATH, just warn
|
||||
|
||||
// Check 11b: Documentation bd prime references match installed version
|
||||
bdPrimeDocsCheck := convertDoctorCheck(doctor.CheckDocumentationBdPrimeReference(path))
|
||||
result.Checks = append(result.Checks, bdPrimeDocsCheck)
|
||||
// Don't fail overall check for doc mismatch, just warn
|
||||
|
||||
// Check 12: Agent documentation presence
|
||||
agentDocsCheck := convertDoctorCheck(doctor.CheckAgentDocumentation(path))
|
||||
result.Checks = append(result.Checks, agentDocsCheck)
|
||||
|
||||
@@ -272,3 +272,81 @@ func VerifyPrimeOutput() DoctorCheck {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckBdInPath verifies that 'bd' command is available in PATH.
|
||||
// This is important because Claude hooks rely on executing 'bd prime'.
|
||||
func CheckBdInPath() DoctorCheck {
|
||||
_, err := exec.LookPath("bd")
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "bd in PATH",
|
||||
Status: "warning",
|
||||
Message: "'bd' command not found in PATH",
|
||||
Detail: "Claude hooks execute 'bd prime' and won't work without bd in PATH",
|
||||
Fix: "Install bd globally:\n" +
|
||||
" • Homebrew: brew install steveyegge/tap/bd\n" +
|
||||
" • Script: curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash\n" +
|
||||
" • Or add bd to your PATH",
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "bd in PATH",
|
||||
Status: "ok",
|
||||
Message: "'bd' command available",
|
||||
}
|
||||
}
|
||||
|
||||
// CheckDocumentationBdPrimeReference checks if AGENTS.md or CLAUDE.md reference 'bd prime'
|
||||
// and verifies the command exists. This helps catch version mismatches where docs
|
||||
// reference features not available in the installed version.
|
||||
func CheckDocumentationBdPrimeReference(repoPath string) DoctorCheck {
|
||||
docFiles := []string{
|
||||
filepath.Join(repoPath, "AGENTS.md"),
|
||||
filepath.Join(repoPath, "CLAUDE.md"),
|
||||
filepath.Join(repoPath, ".claude", "CLAUDE.md"),
|
||||
}
|
||||
|
||||
var filesWithBdPrime []string
|
||||
for _, docFile := range docFiles {
|
||||
content, err := os.ReadFile(docFile) // #nosec G304 - controlled paths from repoPath
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(string(content), "bd prime") {
|
||||
filesWithBdPrime = append(filesWithBdPrime, filepath.Base(docFile))
|
||||
}
|
||||
}
|
||||
|
||||
// If no docs reference bd prime, that's fine - not everyone uses it
|
||||
if len(filesWithBdPrime) == 0 {
|
||||
return DoctorCheck{
|
||||
Name: "Documentation bd prime",
|
||||
Status: "ok",
|
||||
Message: "No bd prime references in documentation",
|
||||
}
|
||||
}
|
||||
|
||||
// Docs reference bd prime - verify the command works
|
||||
cmd := exec.Command("bd", "prime", "--help")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Documentation bd prime",
|
||||
Status: "warning",
|
||||
Message: "Documentation references 'bd prime' but command not found",
|
||||
Detail: "Files: " + strings.Join(filesWithBdPrime, ", "),
|
||||
Fix: "Upgrade bd to get the 'bd prime' command:\n" +
|
||||
" • Homebrew: brew upgrade bd\n" +
|
||||
" • Script: curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash\n" +
|
||||
" Or remove 'bd prime' references from documentation if using older version",
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "Documentation bd prime",
|
||||
Status: "ok",
|
||||
Message: "Documentation references match installed features",
|
||||
Detail: "Files: " + strings.Join(filesWithBdPrime, ", "),
|
||||
}
|
||||
}
|
||||
|
||||
296
cmd/bd/doctor/claude_test.go
Normal file
296
cmd/bd/doctor/claude_test.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckBdInPath(t *testing.T) {
|
||||
// This test verifies CheckBdInPath works correctly
|
||||
// Note: This test will pass if bd is in PATH (which it likely is during development)
|
||||
// In CI environments, the test may show "warning" if bd isn't installed
|
||||
check := CheckBdInPath()
|
||||
|
||||
// Just verify the check returns a valid result
|
||||
if check.Name != "bd in PATH" {
|
||||
t.Errorf("Expected check name 'bd in PATH', got %s", check.Name)
|
||||
}
|
||||
|
||||
if check.Status != "ok" && check.Status != "warning" {
|
||||
t.Errorf("Expected status 'ok' or 'warning', got %s", check.Status)
|
||||
}
|
||||
|
||||
// If warning, should have a fix message
|
||||
if check.Status == "warning" && check.Fix == "" {
|
||||
t.Error("Expected fix message for warning status, got empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDocumentationBdPrimeReference(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fileContent map[string]string // filename -> content
|
||||
expectedStatus string
|
||||
expectDetail bool
|
||||
}{
|
||||
{
|
||||
name: "no documentation files",
|
||||
fileContent: map[string]string{},
|
||||
expectedStatus: "ok",
|
||||
expectDetail: false,
|
||||
},
|
||||
{
|
||||
name: "documentation without bd prime",
|
||||
fileContent: map[string]string{
|
||||
"AGENTS.md": "# Agents\n\nUse bd ready to see ready issues.",
|
||||
},
|
||||
expectedStatus: "ok",
|
||||
expectDetail: false,
|
||||
},
|
||||
{
|
||||
name: "AGENTS.md references bd prime",
|
||||
fileContent: map[string]string{
|
||||
"AGENTS.md": "# Agents\n\nRun `bd prime` to get context.",
|
||||
},
|
||||
expectedStatus: "ok", // Will be ok if bd is installed, warning otherwise
|
||||
expectDetail: true,
|
||||
},
|
||||
{
|
||||
name: "CLAUDE.md references bd prime",
|
||||
fileContent: map[string]string{
|
||||
"CLAUDE.md": "# Claude\n\nUse bd prime for workflow context.",
|
||||
},
|
||||
expectedStatus: "ok",
|
||||
expectDetail: true,
|
||||
},
|
||||
{
|
||||
name: ".claude/CLAUDE.md references bd prime",
|
||||
fileContent: map[string]string{
|
||||
".claude/CLAUDE.md": "Run bd prime to see workflow.",
|
||||
},
|
||||
expectedStatus: "ok",
|
||||
expectDetail: true,
|
||||
},
|
||||
{
|
||||
name: "multiple files reference bd prime",
|
||||
fileContent: map[string]string{
|
||||
"AGENTS.md": "Use bd prime",
|
||||
"CLAUDE.md": "Run bd prime",
|
||||
},
|
||||
expectedStatus: "ok",
|
||||
expectDetail: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test files
|
||||
for filename, content := range tt.fileContent {
|
||||
filePath := filepath.Join(tmpDir, filename)
|
||||
dir := filepath.Dir(filePath)
|
||||
if dir != tmpDir {
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
check := CheckDocumentationBdPrimeReference(tmpDir)
|
||||
|
||||
if check.Name != "Documentation bd prime" {
|
||||
t.Errorf("Expected check name 'Documentation bd prime', got %s", check.Name)
|
||||
}
|
||||
|
||||
// The status depends on whether bd is installed, so we accept both ok and warning
|
||||
if check.Status != "ok" && check.Status != "warning" {
|
||||
t.Errorf("Expected status 'ok' or 'warning', got %s", check.Status)
|
||||
}
|
||||
|
||||
// If we expect detail (files were found), verify it's present
|
||||
if tt.expectDetail && check.Status == "ok" && check.Detail == "" {
|
||||
t.Error("Expected Detail field to be set when files reference bd prime")
|
||||
}
|
||||
|
||||
// If warning, should have a fix message
|
||||
if check.Status == "warning" && check.Fix == "" {
|
||||
t.Error("Expected fix message for warning status, got empty string")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDocumentationBdPrimeReferenceNoFiles(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
check := CheckDocumentationBdPrimeReference(tmpDir)
|
||||
|
||||
if check.Status != "ok" {
|
||||
t.Errorf("Expected status 'ok' for no documentation files, got %s", check.Status)
|
||||
}
|
||||
|
||||
if check.Message != "No bd prime references in documentation" {
|
||||
t.Errorf("Expected message about no references, got: %s", check.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsMCPServerInstalled(t *testing.T) {
|
||||
// This test verifies the function doesn't crash with missing/invalid settings
|
||||
// We can't easily test the positive case without modifying the user's actual settings
|
||||
|
||||
// The function should return false if settings don't exist or are invalid
|
||||
// This is a basic sanity check
|
||||
result := isMCPServerInstalled()
|
||||
|
||||
// Just verify it returns a boolean without panicking
|
||||
if result != true && result != false {
|
||||
t.Error("Expected boolean result from isMCPServerInstalled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsBeadsPluginInstalled(t *testing.T) {
|
||||
// Similar sanity check for plugin detection
|
||||
result := isBeadsPluginInstalled()
|
||||
|
||||
// Just verify it returns a boolean without panicking
|
||||
if result != true && result != false {
|
||||
t.Error("Expected boolean result from isBeadsPluginInstalled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasClaudeHooks(t *testing.T) {
|
||||
// Sanity check for hooks detection
|
||||
result := hasClaudeHooks()
|
||||
|
||||
// Just verify it returns a boolean without panicking
|
||||
if result != true && result != false {
|
||||
t.Error("Expected boolean result from hasClaudeHooks")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckClaude(t *testing.T) {
|
||||
// Verify CheckClaude returns a valid DoctorCheck
|
||||
check := CheckClaude()
|
||||
|
||||
if check.Name != "Claude Integration" {
|
||||
t.Errorf("Expected check name 'Claude Integration', got %s", check.Name)
|
||||
}
|
||||
|
||||
validStatuses := map[string]bool{"ok": true, "warning": true, "error": true}
|
||||
if !validStatuses[check.Status] {
|
||||
t.Errorf("Invalid status: %s", check.Status)
|
||||
}
|
||||
|
||||
// If warning, should have fix message
|
||||
if check.Status == "warning" && check.Fix == "" {
|
||||
t.Error("Expected fix message for warning status")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasBeadsHooksWithInvalidPath(t *testing.T) {
|
||||
// Test that hasBeadsHooks handles invalid/missing paths gracefully
|
||||
result := hasBeadsHooks("/nonexistent/path/to/settings.json")
|
||||
|
||||
if result != false {
|
||||
t.Error("Expected false for non-existent settings file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasBeadsHooksWithInvalidJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
settingsPath := filepath.Join(tmpDir, "settings.json")
|
||||
|
||||
// Write invalid JSON
|
||||
if err := os.WriteFile(settingsPath, []byte("not valid json"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result := hasBeadsHooks(settingsPath)
|
||||
|
||||
if result != false {
|
||||
t.Error("Expected false for invalid JSON settings file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasBeadsHooksWithNoHooksSection(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
settingsPath := filepath.Join(tmpDir, "settings.json")
|
||||
|
||||
// Write valid JSON without hooks section
|
||||
if err := os.WriteFile(settingsPath, []byte(`{"enabledPlugins": {}}`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result := hasBeadsHooks(settingsPath)
|
||||
|
||||
if result != false {
|
||||
t.Error("Expected false for settings file without hooks section")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasBeadsHooksWithBdPrime(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
settingsPath := filepath.Join(tmpDir, "settings.json")
|
||||
|
||||
// Write settings with bd prime hook
|
||||
settingsContent := `{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "beads",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bd prime"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(settingsPath, []byte(settingsContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result := hasBeadsHooks(settingsPath)
|
||||
|
||||
if result != true {
|
||||
t.Error("Expected true for settings file with bd prime hook")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasBeadsHooksWithoutBdPrime(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
settingsPath := filepath.Join(tmpDir, "settings.json")
|
||||
|
||||
// Write settings with hooks but not bd prime
|
||||
settingsContent := `{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "something",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo hello"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(settingsPath, []byte(settingsContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result := hasBeadsHooks(settingsPath)
|
||||
|
||||
if result != false {
|
||||
t.Error("Expected false for settings file without bd prime hook")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user