diff --git a/internal/doctor/config_check.go b/internal/doctor/config_check.go index 4914d628..77af74d8 100644 --- a/internal/doctor/config_check.go +++ b/internal/doctor/config_check.go @@ -581,9 +581,10 @@ func (c *CustomTypesCheck) Run(ctx *CheckContext) *CheckResult { } // Get current custom types configuration + // Use Output() not CombinedOutput() to avoid capturing bd's stderr messages cmd := exec.Command("bd", "config", "get", "types.custom") cmd.Dir = ctx.TownRoot - output, err := cmd.CombinedOutput() + output, err := cmd.Output() if err != nil { // If config key doesn't exist, types are not configured c.townRoot = ctx.TownRoot @@ -600,8 +601,8 @@ func (c *CustomTypesCheck) Run(ctx *CheckContext) *CheckResult { } } - // Parse configured types - configuredTypes := strings.TrimSpace(string(output)) + // Parse configured types, filtering out bd "Note:" messages that may appear in stdout + configuredTypes := parseConfigOutput(output) configuredSet := make(map[string]bool) for _, t := range strings.Split(configuredTypes, ",") { configuredSet[strings.TrimSpace(t)] = true @@ -640,6 +641,18 @@ func (c *CustomTypesCheck) Run(ctx *CheckContext) *CheckResult { } } +// parseConfigOutput extracts the config value from bd output, filtering out +// informational messages like "Note: ..." that bd may emit to stdout. +func parseConfigOutput(output []byte) string { + for _, line := range strings.Split(string(output), "\n") { + line = strings.TrimSpace(line) + if line != "" && !strings.HasPrefix(line, "Note:") { + return line + } + } + return "" +} + // Fix registers the missing custom types. func (c *CustomTypesCheck) Fix(ctx *CheckContext) error { cmd := exec.Command("bd", "config", "set", "types.custom", constants.BeadsCustomTypes) diff --git a/internal/doctor/config_check_test.go b/internal/doctor/config_check_test.go index 8a5230d6..136ef012 100644 --- a/internal/doctor/config_check_test.go +++ b/internal/doctor/config_check_test.go @@ -3,7 +3,10 @@ package doctor import ( "os" "path/filepath" + "strings" "testing" + + "github.com/steveyegge/gastown/internal/constants" ) func TestSessionHookCheck_UsesSessionStartScript(t *testing.T) { @@ -224,3 +227,90 @@ func TestSessionHookCheck_Run(t *testing.T) { } }) } + +func TestParseConfigOutput(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "simple value", + input: "agent,role,rig,convoy,slot\n", + want: "agent,role,rig,convoy,slot", + }, + { + name: "value with trailing newlines", + input: "agent,role,rig,convoy,slot\n\n", + want: "agent,role,rig,convoy,slot", + }, + { + name: "Note prefix filtered", + input: "Note: No git repository initialized - running without background sync\nagent,role,rig,convoy,slot\n", + want: "agent,role,rig,convoy,slot", + }, + { + name: "multiple Note prefixes filtered", + input: "Note: First note\nNote: Second note\nagent,role,rig,convoy,slot\n", + want: "agent,role,rig,convoy,slot", + }, + { + name: "empty output", + input: "", + want: "", + }, + { + name: "only whitespace", + input: " \n \n", + want: "", + }, + { + name: "Note with different casing is not filtered", + input: "note: lowercase should not match\n", + want: "note: lowercase should not match", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseConfigOutput([]byte(tt.input)) + if got != tt.want { + t.Errorf("parseConfigOutput() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestCustomTypesCheck_ParsesOutputWithNotePrefix(t *testing.T) { + // This test verifies that CustomTypesCheck correctly parses bd output + // that contains "Note:" informational messages before the actual config value. + // Without proper filtering, the check would see "Note: ..." as the config value + // and incorrectly report all custom types as missing. + + // Test the parsing logic directly - this simulates bd outputting: + // "Note: No git repository initialized - running without background sync" + // followed by the actual config value + output := "Note: No git repository initialized - running without background sync\n" + constants.BeadsCustomTypes + "\n" + parsed := parseConfigOutput([]byte(output)) + + if parsed != constants.BeadsCustomTypes { + t.Errorf("parseConfigOutput failed to filter Note: prefix\ngot: %q\nwant: %q", parsed, constants.BeadsCustomTypes) + } + + // Verify that all required types are found in the parsed output + configuredSet := make(map[string]bool) + for _, typ := range strings.Split(parsed, ",") { + configuredSet[strings.TrimSpace(typ)] = true + } + + var missing []string + for _, required := range constants.BeadsCustomTypesList() { + if !configuredSet[required] { + missing = append(missing, required) + } + } + + if len(missing) > 0 { + t.Errorf("After parsing, missing types: %v", missing) + } +}