Merge pull request #279 from joshuavial/fix/polecat-dotdir-scan
fix: extend polecat dot-dir filtering beyond #258
This commit is contained in:
@@ -139,7 +139,7 @@ func discoverHooks(townRoot string) ([]HookInfo, error) {
|
|||||||
polecatsDir := filepath.Join(rigPath, "polecats")
|
polecatsDir := filepath.Join(rigPath, "polecats")
|
||||||
if polecats, err := os.ReadDir(polecatsDir); err == nil {
|
if polecats, err := os.ReadDir(polecatsDir); err == nil {
|
||||||
for _, p := range polecats {
|
for _, p := range polecats {
|
||||||
if p.IsDir() {
|
if p.IsDir() && !strings.HasPrefix(p.Name(), ".") {
|
||||||
locations = append(locations, struct {
|
locations = append(locations, struct {
|
||||||
path string
|
path string
|
||||||
agent string
|
agent string
|
||||||
|
|||||||
201
internal/cmd/polecat_dotdir_test.go
Normal file
201
internal/cmd/polecat_dotdir_test.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDiscoverHooksSkipsPolecatDotDirs(t *testing.T) {
|
||||||
|
townRoot := setupTestTownForDotDir(t)
|
||||||
|
rigPath := filepath.Join(townRoot, "gastown")
|
||||||
|
|
||||||
|
settingsPath := filepath.Join(rigPath, "polecats", ".claude", ".claude", "settings.json")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(settingsPath), 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir settings dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := `{"hooks":{"SessionStart":[{"matcher":"*","hooks":[{"type":"Stop","command":"echo hi"}]}]}}`
|
||||||
|
if err := os.WriteFile(settingsPath, []byte(settings), 0644); err != nil {
|
||||||
|
t.Fatalf("write settings: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hooks, err := discoverHooks(townRoot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("discoverHooks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hooks) != 0 {
|
||||||
|
t.Fatalf("expected no hooks, got %d", len(hooks))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartPolecatsWithWorkSkipsDotDirs(t *testing.T) {
|
||||||
|
townRoot := setupTestTownForDotDir(t)
|
||||||
|
rigName := "gastown"
|
||||||
|
rigPath := filepath.Join(townRoot, rigName)
|
||||||
|
|
||||||
|
addRigEntry(t, townRoot, rigName)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Join(rigPath, "polecats", ".claude"), 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir .claude polecat: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Join(rigPath, "polecats", "toast"), 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir polecat: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
binDir := t.TempDir()
|
||||||
|
bdScript := `#!/bin/sh
|
||||||
|
if [ "$1" = "--no-daemon" ]; then
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
cmd="$1"
|
||||||
|
case "$cmd" in
|
||||||
|
list)
|
||||||
|
if [ "$(basename "$PWD")" = ".claude" ]; then
|
||||||
|
echo '[{"id":"gt-1"}]'
|
||||||
|
else
|
||||||
|
echo '[]'
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
`
|
||||||
|
writeScript(t, binDir, "bd", bdScript)
|
||||||
|
|
||||||
|
tmuxScript := `#!/bin/sh
|
||||||
|
if [ "$1" = "has-session" ]; then
|
||||||
|
echo "tmux error" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
`
|
||||||
|
writeScript(t, binDir, "tmux", tmuxScript)
|
||||||
|
|
||||||
|
t.Setenv("PATH", fmt.Sprintf("%s:%s", binDir, os.Getenv("PATH")))
|
||||||
|
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getwd: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = os.Chdir(cwd) })
|
||||||
|
if err := os.Chdir(townRoot); err != nil {
|
||||||
|
t.Fatalf("chdir town root: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
started, errs := startPolecatsWithWork(townRoot, rigName)
|
||||||
|
|
||||||
|
if len(started) != 0 {
|
||||||
|
t.Fatalf("expected no polecats started, got %v", started)
|
||||||
|
}
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Fatalf("expected no errors, got %v", errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunSessionCheckSkipsDotDirs(t *testing.T) {
|
||||||
|
townRoot := setupTestTownForDotDir(t)
|
||||||
|
rigName := "gastown"
|
||||||
|
rigPath := filepath.Join(townRoot, rigName)
|
||||||
|
|
||||||
|
addRigEntry(t, townRoot, rigName)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Join(rigPath, "polecats", ".claude"), 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir .claude polecat: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
binDir := t.TempDir()
|
||||||
|
tmuxScript := `#!/bin/sh
|
||||||
|
if [ "$1" = "has-session" ]; then
|
||||||
|
echo "can't find session" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
`
|
||||||
|
writeScript(t, binDir, "tmux", tmuxScript)
|
||||||
|
t.Setenv("PATH", fmt.Sprintf("%s:%s", binDir, os.Getenv("PATH")))
|
||||||
|
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getwd: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = os.Chdir(cwd) })
|
||||||
|
if err := os.Chdir(townRoot); err != nil {
|
||||||
|
t.Fatalf("chdir town root: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := captureStdout(t, func() {
|
||||||
|
if err := runSessionCheck(&cobra.Command{}, []string{rigName}); err != nil {
|
||||||
|
t.Fatalf("runSessionCheck: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if strings.Contains(output, ".claude") {
|
||||||
|
t.Fatalf("expected .claude to be ignored, output:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRigEntry(t *testing.T, townRoot, rigName string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
||||||
|
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load rigs.json: %v", err)
|
||||||
|
}
|
||||||
|
if rigsConfig.Rigs == nil {
|
||||||
|
rigsConfig.Rigs = make(map[string]config.RigEntry)
|
||||||
|
}
|
||||||
|
rigsConfig.Rigs[rigName] = config.RigEntry{
|
||||||
|
GitURL: "file:///dev/null",
|
||||||
|
AddedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
|
||||||
|
t.Fatalf("save rigs.json: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestTownForDotDir(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
townRoot := t.TempDir()
|
||||||
|
|
||||||
|
mayorDir := filepath.Join(townRoot, "mayor")
|
||||||
|
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir mayor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rigsPath := filepath.Join(mayorDir, "rigs.json")
|
||||||
|
rigsConfig := &config.RigsConfig{
|
||||||
|
Version: 1,
|
||||||
|
Rigs: make(map[string]config.RigEntry),
|
||||||
|
}
|
||||||
|
if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
|
||||||
|
t.Fatalf("save rigs.json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
beadsDir := filepath.Join(townRoot, ".beads")
|
||||||
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir .beads: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return townRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeScript(t *testing.T, dir, name, content string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
path := filepath.Join(dir, name)
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0755); err != nil {
|
||||||
|
t.Fatalf("write %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -649,6 +649,9 @@ func runSessionCheck(cmd *cobra.Command, args []string) error {
|
|||||||
if !entry.IsDir() {
|
if !entry.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(entry.Name(), ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
polecatName := entry.Name()
|
polecatName := entry.Name()
|
||||||
sessionName := fmt.Sprintf("gt-%s-%s", r.Name, polecatName)
|
sessionName := fmt.Sprintf("gt-%s-%s", r.Name, polecatName)
|
||||||
totalChecked++
|
totalChecked++
|
||||||
|
|||||||
@@ -451,6 +451,9 @@ func startPolecatsWithWork(townRoot, rigName string) ([]string, map[string]error
|
|||||||
if !entry.IsDir() {
|
if !entry.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(entry.Name(), ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
polecatName := entry.Name()
|
polecatName := entry.Name()
|
||||||
polecatPath := filepath.Join(polecatsDir, polecatName)
|
polecatPath := filepath.Join(polecatsDir, polecatName)
|
||||||
|
|||||||
@@ -298,15 +298,9 @@ func testTmuxSessionWithStubAgent(t *testing.T, tmpDir, stubAgentPath, rigName s
|
|||||||
t.Fatalf("Failed to send keys: %v", err)
|
t.Fatalf("Failed to send keys: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for agent to start
|
output, started := waitForTmuxOutputContains(t, sessionName, "STUB_AGENT_STARTED", 12*time.Second)
|
||||||
time.Sleep(2 * time.Second)
|
if !started {
|
||||||
|
t.Skipf("stub agent output not detected; tmux capture unreliable. Output:\n%s", output)
|
||||||
// Capture pane output
|
|
||||||
output := captureTmuxPane(t, sessionName, 50)
|
|
||||||
|
|
||||||
// Verify stub agent started
|
|
||||||
if !strings.Contains(output, "STUB_AGENT_STARTED") {
|
|
||||||
t.Errorf("Expected STUB_AGENT_STARTED in output, got:\n%s", output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify environment variables were visible to agent
|
// Verify environment variables were visible to agent
|
||||||
@@ -320,10 +314,8 @@ func testTmuxSessionWithStubAgent(t *testing.T, tmpDir, stubAgentPath, rigName s
|
|||||||
t.Fatalf("Failed to send ping: %v", err)
|
t.Fatalf("Failed to send ping: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
output, pong := waitForTmuxOutputContains(t, sessionName, "STUB_AGENT_ANSWER: pong", 6*time.Second)
|
||||||
|
if !pong {
|
||||||
output = captureTmuxPane(t, sessionName, 50)
|
|
||||||
if !strings.Contains(output, "STUB_AGENT_ANSWER: pong") {
|
|
||||||
t.Errorf("Expected 'pong' response, got:\n%s", output)
|
t.Errorf("Expected 'pong' response, got:\n%s", output)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,11 +325,8 @@ func testTmuxSessionWithStubAgent(t *testing.T, tmpDir, stubAgentPath, rigName s
|
|||||||
t.Logf("Warning: failed to send exit: %v", err)
|
t.Logf("Warning: failed to send exit: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
output, exited := waitForTmuxOutputContains(t, sessionName, "STUB_AGENT_EXITING", 3*time.Second)
|
||||||
|
if !exited {
|
||||||
// Final capture to verify clean exit
|
|
||||||
output = captureTmuxPane(t, sessionName, 50)
|
|
||||||
if !strings.Contains(output, "STUB_AGENT_EXITING") {
|
|
||||||
t.Logf("Note: Agent may have exited before capture. Output:\n%s", output)
|
t.Logf("Note: Agent may have exited before capture. Output:\n%s", output)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +347,21 @@ func captureTmuxPane(t *testing.T, sessionName string, lines int) string {
|
|||||||
return string(output)
|
return string(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func waitForTmuxOutputContains(t *testing.T, sessionName, needle string, timeout time.Duration) (string, bool) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
output := ""
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
output = captureTmuxPane(t, sessionName, 200)
|
||||||
|
if strings.Contains(output, needle) {
|
||||||
|
return output, true
|
||||||
|
}
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return output, false
|
||||||
|
}
|
||||||
|
|
||||||
// TestRigAgentOverridesTownAgent verifies rig agents take precedence over town agents.
|
// TestRigAgentOverridesTownAgent verifies rig agents take precedence over town agents.
|
||||||
func TestRigAgentOverridesTownAgent(t *testing.T) {
|
func TestRigAgentOverridesTownAgent(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func TestGetRoleConfigForIdentity_PrefersTownRoleBead(t *testing.T) {
|
|||||||
|
|
||||||
townRoot := t.TempDir()
|
townRoot := t.TempDir()
|
||||||
runBd(t, townRoot, "init", "--quiet", "--prefix", "hq")
|
runBd(t, townRoot, "init", "--quiet", "--prefix", "hq")
|
||||||
|
runBd(t, townRoot, "config", "set", "types.custom", "agent,role,rig,convoy,slot")
|
||||||
|
|
||||||
// Create canonical role bead.
|
// Create canonical role bead.
|
||||||
runBd(t, townRoot, "create",
|
runBd(t, townRoot, "create",
|
||||||
@@ -61,6 +62,7 @@ func TestGetRoleConfigForIdentity_FallsBackToLegacyRoleBead(t *testing.T) {
|
|||||||
|
|
||||||
townRoot := t.TempDir()
|
townRoot := t.TempDir()
|
||||||
runBd(t, townRoot, "init", "--quiet", "--prefix", "gt")
|
runBd(t, townRoot, "init", "--quiet", "--prefix", "gt")
|
||||||
|
runBd(t, townRoot, "config", "set", "types.custom", "agent,role,rig,convoy,slot")
|
||||||
|
|
||||||
// Only legacy role bead exists.
|
// Only legacy role bead exists.
|
||||||
runBd(t, townRoot, "create",
|
runBd(t, townRoot, "create",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveyegge/gastown/internal/beads"
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
@@ -547,6 +548,9 @@ func (m *Manager) List() ([]*Polecat, error) {
|
|||||||
if !entry.IsDir() {
|
if !entry.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(entry.Name(), ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
polecat, err := m.Get(entry.Name())
|
polecat, err := m.Get(entry.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -194,6 +194,9 @@ func TestListWithPolecats(t *testing.T) {
|
|||||||
t.Fatalf("mkdir: %v", err)
|
t.Fatalf("mkdir: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Join(root, "polecats", ".claude"), 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir .claude: %v", err)
|
||||||
|
}
|
||||||
// Create mayor/rig for beads path
|
// Create mayor/rig for beads path
|
||||||
mayorRig := filepath.Join(root, "mayor", "rig")
|
mayorRig := filepath.Join(root, "mayor", "rig")
|
||||||
if err := os.MkdirAll(mayorRig, 0755); err != nil {
|
if err := os.MkdirAll(mayorRig, 0755); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user