fix(doctor): filter bd "Note:" messages from custom types check (#381)

* fix(doctor): filter bd "Note:" messages from custom types check

bd outputs "Note: No git repository initialized..." to stdout when
running outside a git repo, which was contaminating the custom types
parsing and causing false warnings.

- Use Output() instead of CombinedOutput() to avoid stderr
- Filter out lines starting with "Note:" from stdout

Co-Authored-By: Claude <noreply@anthropic.com>

* test(doctor): add unit tests for custom types Note: filtering

Extract parseConfigOutput helper function and add tests verifying
that bd "Note:" informational messages are properly filtered from
config output. Tests fail without the fix and pass with it.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: julianknutsen <julianknutsen@users.noreply.github>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Julian Knutsen
2026-01-12 09:45:36 +00:00
committed by GitHub
parent 043a6abc59
commit a9080ed04f
2 changed files with 106 additions and 3 deletions

View File

@@ -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)

View File

@@ -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)
}
}