fix: prevent inherited BEADS_DIR from causing prefix mismatch (#321)

- Fix beads.run() to always explicitly set BEADS_DIR based on the working
  directory or explicit override
- This prevents inherited environment variables (e.g., from mayor session
  with BEADS_DIR=/home/erik/gt/.beads) from causing prefix mismatch errors
  when creating agent beads for rigs
- Update polecat manager to use NewWithBeadsDir for explicitness
- Add comprehensive test coverage for BEADS_DIR routing and validation
- Add SessionLister interface for deterministic orphan session testing

Root cause: When BEADS_DIR was set in the parent environment, all bd
commands used the town database (hq- prefix) instead of the rig database
(gt- prefix), causing "prefix mismatch: database uses 'hq' but you
specified 'gt'" errors during polecat spawn.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/joe
2026-01-11 18:33:34 -08:00
committed by Steve Yegge
parent ea84079f8b
commit 598a39e708
7 changed files with 1298 additions and 83 deletions

View File

@@ -33,6 +33,23 @@ func writeFakeBD(t *testing.T, script string) string {
return binDir
}
func assertBeadsDirLog(t *testing.T, logPath, want string) {
t.Helper()
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("reading beads dir log: %v", err)
}
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
if len(lines) == 0 || (len(lines) == 1 && lines[0] == "") {
t.Fatalf("expected beads dir log entries, got none")
}
for _, line := range lines {
if line != want {
t.Fatalf("BEADS_DIR = %q, want %q", line, want)
}
}
}
func createTestRig(t *testing.T, root, name string) {
t.Helper()
@@ -63,7 +80,6 @@ func createTestRig(t *testing.T, root, name string) {
}
func TestDiscoverRigs(t *testing.T) {
t.Parallel()
root, rigsConfig := setupTestTown(t)
// Create test rig
@@ -102,7 +118,6 @@ func TestDiscoverRigs(t *testing.T) {
}
func TestGetRig(t *testing.T) {
t.Parallel()
root, rigsConfig := setupTestTown(t)
createTestRig(t, root, "test-rig")
@@ -123,7 +138,6 @@ func TestGetRig(t *testing.T) {
}
func TestGetRigNotFound(t *testing.T) {
t.Parallel()
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
@@ -134,7 +148,6 @@ func TestGetRigNotFound(t *testing.T) {
}
func TestRigExists(t *testing.T) {
t.Parallel()
root, rigsConfig := setupTestTown(t)
rigsConfig.Rigs["exists"] = config.RigEntry{}
@@ -149,7 +162,6 @@ func TestRigExists(t *testing.T) {
}
func TestRemoveRig(t *testing.T) {
t.Parallel()
root, rigsConfig := setupTestTown(t)
rigsConfig.Rigs["to-remove"] = config.RigEntry{}
@@ -165,7 +177,6 @@ func TestRemoveRig(t *testing.T) {
}
func TestRemoveRigNotFound(t *testing.T) {
t.Parallel()
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
@@ -176,7 +187,6 @@ func TestRemoveRigNotFound(t *testing.T) {
}
func TestAddRig_RejectsInvalidNames(t *testing.T) {
t.Parallel()
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
@@ -208,7 +218,6 @@ func TestAddRig_RejectsInvalidNames(t *testing.T) {
}
func TestListRigNames(t *testing.T) {
t.Parallel()
root, rigsConfig := setupTestTown(t)
rigsConfig.Rigs["rig1"] = config.RigEntry{}
rigsConfig.Rigs["rig2"] = config.RigEntry{}
@@ -222,7 +231,6 @@ func TestListRigNames(t *testing.T) {
}
func TestRigSummary(t *testing.T) {
t.Parallel()
rig := &Rig{
Name: "test",
Polecats: []string{"a", "b", "c"},
@@ -247,7 +255,6 @@ func TestRigSummary(t *testing.T) {
}
func TestEnsureGitignoreEntry_AddsEntry(t *testing.T) {
t.Parallel()
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
@@ -264,7 +271,6 @@ func TestEnsureGitignoreEntry_AddsEntry(t *testing.T) {
}
func TestEnsureGitignoreEntry_DoesNotDuplicate(t *testing.T) {
t.Parallel()
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
@@ -286,7 +292,6 @@ func TestEnsureGitignoreEntry_DoesNotDuplicate(t *testing.T) {
}
func TestEnsureGitignoreEntry_AppendsToExisting(t *testing.T) {
t.Parallel()
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
@@ -392,12 +397,14 @@ exit 0
}
func TestInitBeadsWritesConfigOnFailure(t *testing.T) {
// Cannot use t.Parallel() due to t.Setenv
rigPath := t.TempDir()
beadsDir := filepath.Join(rigPath, ".beads")
script := `#!/usr/bin/env bash
set -e
if [[ -n "$BEADS_DIR_LOG" ]]; then
echo "${BEADS_DIR:-<unset>}" >> "$BEADS_DIR_LOG"
fi
cmd="$1"
shift
if [[ "$cmd" == "init" ]]; then
@@ -409,8 +416,9 @@ exit 1
`
binDir := writeFakeBD(t, script)
beadsDirLog := filepath.Join(t.TempDir(), "beads-dir.log")
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("EXPECT_BEADS_DIR", beadsDir)
t.Setenv("BEADS_DIR_LOG", beadsDirLog)
manager := &Manager{}
if err := manager.initBeads(rigPath, "gt"); err != nil {
@@ -425,14 +433,14 @@ exit 1
if string(config) != "prefix: gt\n" {
t.Fatalf("config.yaml = %q, want %q", string(config), "prefix: gt\n")
}
assertBeadsDirLog(t, beadsDirLog, beadsDir)
}
func TestInitAgentBeadsUsesRigBeadsDir(t *testing.T) {
// Cannot use t.Parallel() due to t.Setenv
// Rig-level agent beads (witness, refinery) are stored in rig beads.
// Town-level agents (mayor, deacon) are created by gt install in town beads.
// This test verifies that rig agent beads are created in the rig directory,
// without an explicit BEADS_DIR override (uses cwd-based discovery).
// using the resolved rig beads directory for BEADS_DIR.
townRoot := t.TempDir()
rigPath := filepath.Join(townRoot, "testrip")
rigBeadsDir := filepath.Join(rigPath, ".beads")
@@ -446,6 +454,9 @@ func TestInitAgentBeadsUsesRigBeadsDir(t *testing.T) {
script := `#!/usr/bin/env bash
set -e
if [[ -n "$BEADS_DIR_LOG" ]]; then
echo "${BEADS_DIR:-<unset>}" >> "$BEADS_DIR_LOG"
fi
if [[ "$1" == "--no-daemon" ]]; then
shift
fi
@@ -481,8 +492,10 @@ esac
binDir := writeFakeBD(t, script)
agentLog := filepath.Join(t.TempDir(), "agents.log")
beadsDirLog := filepath.Join(t.TempDir(), "beads-dir.log")
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("AGENT_LOG", agentLog)
t.Setenv("BEADS_DIR_LOG", beadsDirLog)
t.Setenv("BEADS_DIR", "") // Clear any existing BEADS_DIR
manager := &Manager{townRoot: townRoot}
@@ -514,10 +527,10 @@ esac
t.Errorf("expected agent %s was not created", id)
}
}
assertBeadsDirLog(t, beadsDirLog, rigBeadsDir)
}
func TestIsValidBeadsPrefix(t *testing.T) {
t.Parallel()
tests := []struct {
prefix string
want bool
@@ -560,7 +573,6 @@ func TestIsValidBeadsPrefix(t *testing.T) {
}
func TestInitBeadsRejectsInvalidPrefix(t *testing.T) {
t.Parallel()
rigPath := t.TempDir()
manager := &Manager{}
@@ -586,7 +598,6 @@ func TestInitBeadsRejectsInvalidPrefix(t *testing.T) {
}
func TestDeriveBeadsPrefix(t *testing.T) {
t.Parallel()
tests := []struct {
name string
want string
@@ -635,7 +646,6 @@ func TestDeriveBeadsPrefix(t *testing.T) {
}
func TestSplitCompoundWord(t *testing.T) {
t.Parallel()
tests := []struct {
word string
want []string