Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion internal/beads/beads_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,30 @@ func EnsureCustomTypes(beadsDir string) error {
}

// Configure custom types via bd CLI
bdEnv := append(stripEnvPrefixes(os.Environ(), "BEADS_DIR="), "BEADS_DIR="+beadsDir)
cmd := exec.Command("bd", "config", "set", "types.custom", typesList)
cmd.Dir = beadsDir
// Set BEADS_DIR explicitly to ensure bd operates on the correct database.
// Strip inherited BEADS_DIR first — getenv() returns the first match (gt-uygpe).
cmd.Env = append(stripEnvPrefixes(os.Environ(), "BEADS_DIR="), "BEADS_DIR="+beadsDir)
cmd.Env = bdEnv
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("configure custom types in %s: %s: %w",
beadsDir, strings.TrimSpace(string(output)), err)
}

// Verify the config was actually persisted in the database (GH#2637).
// bd config set can exit 0 but fail to write if it targets the wrong
// database (redirect mismatch, stale metadata, server not running).
// Without this check, the sentinel file below would cache a lie,
// causing all future EnsureCustomTypes calls to skip re-configuration.
verifyCmd := exec.Command("bd", "config", "get", "types.custom")
verifyCmd.Dir = beadsDir
verifyCmd.Env = bdEnv
if verifyOutput, err := verifyCmd.Output(); err != nil || !strings.Contains(string(verifyOutput), "agent") {
return fmt.Errorf("types.custom not persisted in %s after bd config set (verify returned %q): db may be misconfigured",
beadsDir, strings.TrimSpace(string(verifyOutput)))
}

// Write sentinel file with the types list for staleness detection.
// On next invocation, if types have changed, the sentinel won't match
// and we'll re-configure automatically.
Expand Down
72 changes: 71 additions & 1 deletion internal/beads/beads_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ switch ($cmd) {
if ($args.Length -ge 3 -and $args[1] -eq 'get' -and $args[2] -eq 'status.custom') {
Write-Output ''
}
if ($args.Length -ge 3 -and $args[1] -eq 'get' -and $args[2] -eq 'types.custom') {
Write-Output 'agent,role,rig,convoy,slot,queue,event,message,molecule,gate,merge-request'
}
exit 0
}
'migrate' { exit 0 }
Expand Down Expand Up @@ -92,7 +95,14 @@ case "$cmd" in
printf 'prefix: %s\nissue-prefix: %s-\n' "$prefix" "$prefix" > "$target/config.yaml"
exit 0
;;
config|migrate)
config)
# Return types list for "config get types.custom" verification
if echo "$*" | grep -q "get types.custom"; then
echo "agent,role,rig,convoy,slot,queue,event,message,molecule,gate,merge-request"
fi
exit 0
;;
migrate)
exit 0
;;
*)
Expand Down Expand Up @@ -347,6 +357,66 @@ func TestEnsureCustomTypes(t *testing.T) {
})
}

func TestEnsureCustomTypes_VerifyPersistence(t *testing.T) {
t.Run("sentinel not written when db verify fails", func(t *testing.T) {
// Install a mock bd that succeeds on "config set" but returns empty
// on "config get types.custom" — simulating a silent write failure.
binDir := t.TempDir()
logPath := filepath.Join(binDir, "bd.log")
script := `#!/bin/sh
LOG_FILE='` + logPath + `'
printf '%s\n' "$*" >> "$LOG_FILE"
cmd=""
for arg in "$@"; do
case "$arg" in --*) ;; *) cmd="$arg"; break ;; esac
done
case "$cmd" in
init)
target="${BEADS_DIR:-$(pwd)/.beads}"
mkdir -p "$target/dolt"
printf 'prefix: gt\nissue-prefix: gt-\n' > "$target/config.yaml"
exit 0
;;
config)
# "config set" succeeds but "config get types.custom" returns empty
if echo "$*" | grep -q "get types.custom"; then
echo ""
fi
exit 0
;;
migrate) exit 0 ;;
*) exit 0 ;;
esac
`
if err := os.WriteFile(filepath.Join(binDir, "bd"), []byte(script), 0755); err != nil {
t.Fatalf("write mock bd: %v", err)
}
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))

tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatal(err)
}

ResetEnsuredDirs()

err := EnsureCustomTypes(beadsDir)
if err == nil {
t.Fatal("expected error when types.custom verify fails, got nil")
}
if !strings.Contains(err.Error(), "not persisted") {
t.Fatalf("expected 'not persisted' error, got: %v", err)
}

// Sentinel file should NOT have been written
sentinelPath := filepath.Join(beadsDir, typesSentinel)
if _, err := os.Stat(sentinelPath); !os.IsNotExist(err) {
t.Error("sentinel file should not exist when verify fails")
}
})
}

func TestEnsureCustomStatuses(t *testing.T) {
ResetEnsuredDirs()

Expand Down
Loading