fix(doctor): detect plugin and hooks in project-level settings (#1091)
* fix(doctor): detect beads plugin in project-level settings isBeadsPluginInstalled() now checks project-level settings files (.claude/settings.json and .claude/settings.local.json) in addition to user-level settings (~/.claude/settings.json). This fixes the contradictory bd doctor output where the plugin check passes but the integration check warns "Not configured" when the plugin is enabled at project scope. Fixes #1090 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(doctor): detect Claude hooks in project-level settings.json hasClaudeHooks() was missing .claude/settings.json - it only checked .claude/settings.local.json for project-level hooks. Now checks all three locations: - ~/.claude/settings.json (user-level) - .claude/settings.json (project-level) - .claude/settings.local.json (project-level, gitignored) Also uses filepath.Join consistently for cross-platform compatibility. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -179,6 +179,63 @@ func TestIsBeadsPluginInstalled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsBeadsPluginInstalledProjectLevel(t *testing.T) {
|
||||
// Test that plugin is detected in each project-level settings file
|
||||
for _, filename := range []string{"settings.json", "settings.local.json"} {
|
||||
t.Run(filename, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
if err := os.MkdirAll(".claude", 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := `{"enabledPlugins":{"beads@beads-marketplace":true}}`
|
||||
if err := os.WriteFile(filepath.Join(".claude", filename), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !isBeadsPluginInstalled() {
|
||||
t.Errorf("expected to detect plugin in .claude/%s", filename)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test negative cases - plugin should NOT be detected
|
||||
t.Run("plugin disabled", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
if err := os.MkdirAll(".claude", 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := `{"enabledPlugins":{"beads@beads-marketplace":false}}`
|
||||
if err := os.WriteFile(filepath.Join(".claude", "settings.json"), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if isBeadsPluginInstalled() {
|
||||
t.Error("expected NOT to detect plugin when explicitly disabled")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no plugin section", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
if err := os.MkdirAll(".claude", 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := `{"hooks":{}}`
|
||||
if err := os.WriteFile(filepath.Join(".claude", "settings.json"), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if isBeadsPluginInstalled() {
|
||||
t.Error("expected NOT to detect plugin when enabledPlugins section missing")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHasClaudeHooks(t *testing.T) {
|
||||
// Sanity check for hooks detection
|
||||
result := hasClaudeHooks()
|
||||
@@ -189,6 +246,76 @@ func TestHasClaudeHooks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasClaudeHooksProjectLevel(t *testing.T) {
|
||||
hooksContent := `{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{"matcher": "", "hooks": [{"type": "command", "command": "bd prime"}]}
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
// Test that hooks are detected in each project-level settings file
|
||||
for _, filename := range []string{"settings.json", "settings.local.json"} {
|
||||
t.Run(filename, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
if err := os.MkdirAll(".claude", 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(".claude", filename), []byte(hooksContent), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !hasClaudeHooks() {
|
||||
t.Errorf("expected to detect hooks in .claude/%s", filename)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test negative cases
|
||||
t.Run("no hooks section", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
if err := os.MkdirAll(".claude", 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := `{"enabledPlugins":{}}`
|
||||
if err := os.WriteFile(filepath.Join(".claude", "settings.json"), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if hasClaudeHooks() {
|
||||
t.Error("expected NOT to detect hooks when hooks section missing")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hooks but not bd prime", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
if err := os.MkdirAll(".claude", 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := `{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{"matcher": "", "hooks": [{"type": "command", "command": "echo hello"}]}
|
||||
]
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(filepath.Join(".claude", "settings.json"), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if hasClaudeHooks() {
|
||||
t.Error("expected NOT to detect hooks when bd prime not present")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckClaude(t *testing.T) {
|
||||
// Verify CheckClaude returns a valid DoctorCheck
|
||||
check := CheckClaude()
|
||||
|
||||
Reference in New Issue
Block a user