Files
gastown/internal/cmd/sling_288_test.go
Julian Knutsen 0dfb0be368 fix(sling): auto-apply mol-polecat-work (#288) and fix wisp orphan lifecycle bug (#842) (#859)
fix(sling): auto-apply mol-polecat-work (#288) and fix wisp orphan lifecycle bug (#842)

Fixes the formula-on-bead pattern to hook the base bead instead of the wisp:
- Auto-apply mol-polecat-work when slinging bare beads to polecats
- Hook BASE bead with attached_molecule pointing to wisp  
- gt done now closes attached molecule before closing hooked bead
- Convoys complete properly when work finishes

Fixes #288, #842, #858
2026-01-21 20:52:26 -08:00

379 lines
10 KiB
Go

package cmd
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestInstantiateFormulaOnBead verifies the helper function works correctly.
// This tests the formula-on-bead pattern used by issue #288.
func TestInstantiateFormulaOnBead(t *testing.T) {
townRoot := t.TempDir()
// Minimal workspace marker
if err := os.MkdirAll(filepath.Join(townRoot, "mayor", "rig"), 0755); err != nil {
t.Fatalf("mkdir mayor/rig: %v", err)
}
// Create routes.jsonl
if err := os.MkdirAll(filepath.Join(townRoot, ".beads"), 0755); err != nil {
t.Fatalf("mkdir .beads: %v", err)
}
rigDir := filepath.Join(townRoot, "gastown", "mayor", "rig")
if err := os.MkdirAll(rigDir, 0755); err != nil {
t.Fatalf("mkdir rigDir: %v", err)
}
routes := strings.Join([]string{
`{"prefix":"gt-","path":"gastown/mayor/rig"}`,
`{"prefix":"hq-","path":"."}`,
"",
}, "\n")
if err := os.WriteFile(filepath.Join(townRoot, ".beads", "routes.jsonl"), []byte(routes), 0644); err != nil {
t.Fatalf("write routes.jsonl: %v", err)
}
// Create stub bd that logs all commands
binDir := filepath.Join(townRoot, "bin")
if err := os.MkdirAll(binDir, 0755); err != nil {
t.Fatalf("mkdir binDir: %v", err)
}
logPath := filepath.Join(townRoot, "bd.log")
bdScript := `#!/bin/sh
set -e
echo "CMD:$*" >> "${BD_LOG}"
if [ "$1" = "--no-daemon" ]; then
shift
fi
cmd="$1"
shift || true
case "$cmd" in
show)
echo '[{"title":"Fix bug ABC","status":"open","assignee":"","description":""}]'
;;
formula)
echo '{"name":"mol-polecat-work"}'
;;
cook)
;;
mol)
sub="$1"
shift || true
case "$sub" in
wisp)
echo '{"new_epic_id":"gt-wisp-288"}'
;;
bond)
echo '{"root_id":"gt-wisp-288"}'
;;
esac
;;
update)
;;
esac
exit 0
`
bdPath := filepath.Join(binDir, "bd")
if err := os.WriteFile(bdPath, []byte(bdScript), 0755); err != nil {
t.Fatalf("write bd stub: %v", err)
}
t.Setenv("BD_LOG", logPath)
t.Setenv("PATH", binDir+string(os.PathListSeparator)+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(filepath.Join(townRoot, "mayor", "rig")); err != nil {
t.Fatalf("chdir: %v", err)
}
// Test the helper function directly
result, err := InstantiateFormulaOnBead("mol-polecat-work", "gt-abc123", "Test Bug Fix", "", townRoot, false)
if err != nil {
t.Fatalf("InstantiateFormulaOnBead failed: %v", err)
}
if result.WispRootID == "" {
t.Error("WispRootID should not be empty")
}
if result.BeadToHook == "" {
t.Error("BeadToHook should not be empty")
}
// Verify commands were logged
logBytes, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("read log: %v", err)
}
logContent := string(logBytes)
if !strings.Contains(logContent, "cook mol-polecat-work") {
t.Errorf("cook command not found in log:\n%s", logContent)
}
if !strings.Contains(logContent, "mol wisp mol-polecat-work") {
t.Errorf("mol wisp command not found in log:\n%s", logContent)
}
if !strings.Contains(logContent, "mol bond") {
t.Errorf("mol bond command not found in log:\n%s", logContent)
}
}
// TestInstantiateFormulaOnBeadSkipCook verifies the skipCook optimization.
func TestInstantiateFormulaOnBeadSkipCook(t *testing.T) {
townRoot := t.TempDir()
// Minimal workspace marker
if err := os.MkdirAll(filepath.Join(townRoot, "mayor", "rig"), 0755); err != nil {
t.Fatalf("mkdir mayor/rig: %v", err)
}
// Create routes.jsonl
if err := os.MkdirAll(filepath.Join(townRoot, ".beads"), 0755); err != nil {
t.Fatalf("mkdir .beads: %v", err)
}
routes := `{"prefix":"gt-","path":"."}`
if err := os.WriteFile(filepath.Join(townRoot, ".beads", "routes.jsonl"), []byte(routes), 0644); err != nil {
t.Fatalf("write routes.jsonl: %v", err)
}
// Create stub bd
binDir := filepath.Join(townRoot, "bin")
if err := os.MkdirAll(binDir, 0755); err != nil {
t.Fatalf("mkdir binDir: %v", err)
}
logPath := filepath.Join(townRoot, "bd.log")
bdScript := `#!/bin/sh
echo "CMD:$*" >> "${BD_LOG}"
if [ "$1" = "--no-daemon" ]; then shift; fi
cmd="$1"; shift || true
case "$cmd" in
mol)
sub="$1"; shift || true
case "$sub" in
wisp) echo '{"new_epic_id":"gt-wisp-skip"}';;
bond) echo '{"root_id":"gt-wisp-skip"}';;
esac;;
esac
exit 0
`
if err := os.WriteFile(filepath.Join(binDir, "bd"), []byte(bdScript), 0755); err != nil {
t.Fatalf("write bd stub: %v", err)
}
t.Setenv("BD_LOG", logPath)
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
cwd, _ := os.Getwd()
t.Cleanup(func() { _ = os.Chdir(cwd) })
_ = os.Chdir(townRoot)
// Test with skipCook=true
_, err := InstantiateFormulaOnBead("mol-polecat-work", "gt-test", "Test", "", townRoot, true)
if err != nil {
t.Fatalf("InstantiateFormulaOnBead failed: %v", err)
}
logBytes, _ := os.ReadFile(logPath)
logContent := string(logBytes)
// Verify cook was NOT called when skipCook=true
if strings.Contains(logContent, "cook") {
t.Errorf("cook should be skipped when skipCook=true, but was called:\n%s", logContent)
}
// Verify wisp and bond were still called
if !strings.Contains(logContent, "mol wisp") {
t.Errorf("mol wisp should still be called")
}
if !strings.Contains(logContent, "mol bond") {
t.Errorf("mol bond should still be called")
}
}
// TestCookFormula verifies the CookFormula helper.
func TestCookFormula(t *testing.T) {
townRoot := t.TempDir()
binDir := filepath.Join(townRoot, "bin")
if err := os.MkdirAll(binDir, 0755); err != nil {
t.Fatalf("mkdir binDir: %v", err)
}
logPath := filepath.Join(townRoot, "bd.log")
bdScript := `#!/bin/sh
echo "CMD:$*" >> "${BD_LOG}"
exit 0
`
if err := os.WriteFile(filepath.Join(binDir, "bd"), []byte(bdScript), 0755); err != nil {
t.Fatalf("write bd stub: %v", err)
}
t.Setenv("BD_LOG", logPath)
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
err := CookFormula("mol-polecat-work", townRoot)
if err != nil {
t.Fatalf("CookFormula failed: %v", err)
}
logBytes, _ := os.ReadFile(logPath)
if !strings.Contains(string(logBytes), "cook mol-polecat-work") {
t.Errorf("cook command not found in log")
}
}
// TestSlingHookRawBeadFlag verifies --hook-raw-bead flag exists.
func TestSlingHookRawBeadFlag(t *testing.T) {
// Verify the flag variable exists and works
prevValue := slingHookRawBead
t.Cleanup(func() { slingHookRawBead = prevValue })
slingHookRawBead = true
if !slingHookRawBead {
t.Error("slingHookRawBead flag should be true")
}
slingHookRawBead = false
if slingHookRawBead {
t.Error("slingHookRawBead flag should be false")
}
}
// TestAutoApplyLogic verifies the auto-apply detection logic.
// When formulaName is empty and target contains "/polecats/", mol-polecat-work should be applied.
func TestAutoApplyLogic(t *testing.T) {
tests := []struct {
name string
formulaName string
hookRawBead bool
targetAgent string
wantAutoApply bool
}{
{
name: "bare bead to polecat - should auto-apply",
formulaName: "",
hookRawBead: false,
targetAgent: "gastown/polecats/Toast",
wantAutoApply: true,
},
{
name: "bare bead with --hook-raw-bead - should not auto-apply",
formulaName: "",
hookRawBead: true,
targetAgent: "gastown/polecats/Toast",
wantAutoApply: false,
},
{
name: "formula already specified - should not auto-apply",
formulaName: "mol-review",
hookRawBead: false,
targetAgent: "gastown/polecats/Toast",
wantAutoApply: false,
},
{
name: "non-polecat target - should not auto-apply",
formulaName: "",
hookRawBead: false,
targetAgent: "gastown/witness",
wantAutoApply: false,
},
{
name: "mayor target - should not auto-apply",
formulaName: "",
hookRawBead: false,
targetAgent: "mayor",
wantAutoApply: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// This mirrors the logic in sling.go
shouldAutoApply := tt.formulaName == "" && !tt.hookRawBead && strings.Contains(tt.targetAgent, "/polecats/")
if shouldAutoApply != tt.wantAutoApply {
t.Errorf("auto-apply logic: got %v, want %v", shouldAutoApply, tt.wantAutoApply)
}
})
}
}
// TestFormulaOnBeadPassesVariables verifies that feature and issue variables are passed.
func TestFormulaOnBeadPassesVariables(t *testing.T) {
townRoot := t.TempDir()
// Minimal workspace
if err := os.MkdirAll(filepath.Join(townRoot, "mayor", "rig"), 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.MkdirAll(filepath.Join(townRoot, ".beads"), 0755); err != nil {
t.Fatalf("mkdir .beads: %v", err)
}
if err := os.WriteFile(filepath.Join(townRoot, ".beads", "routes.jsonl"), []byte(`{"prefix":"gt-","path":"."}`), 0644); err != nil {
t.Fatalf("write routes: %v", err)
}
binDir := filepath.Join(townRoot, "bin")
if err := os.MkdirAll(binDir, 0755); err != nil {
t.Fatalf("mkdir binDir: %v", err)
}
logPath := filepath.Join(townRoot, "bd.log")
bdScript := `#!/bin/sh
echo "CMD:$*" >> "${BD_LOG}"
if [ "$1" = "--no-daemon" ]; then shift; fi
cmd="$1"; shift || true
case "$cmd" in
cook) exit 0;;
mol)
sub="$1"; shift || true
case "$sub" in
wisp) echo '{"new_epic_id":"gt-wisp-var"}';;
bond) echo '{"root_id":"gt-wisp-var"}';;
esac;;
esac
exit 0
`
if err := os.WriteFile(filepath.Join(binDir, "bd"), []byte(bdScript), 0755); err != nil {
t.Fatalf("write bd: %v", err)
}
t.Setenv("BD_LOG", logPath)
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
cwd, _ := os.Getwd()
t.Cleanup(func() { _ = os.Chdir(cwd) })
_ = os.Chdir(townRoot)
_, err := InstantiateFormulaOnBead("mol-polecat-work", "gt-abc123", "My Cool Feature", "", townRoot, false)
if err != nil {
t.Fatalf("InstantiateFormulaOnBead: %v", err)
}
logBytes, _ := os.ReadFile(logPath)
logContent := string(logBytes)
// Find mol wisp line
var wispLine string
for _, line := range strings.Split(logContent, "\n") {
if strings.Contains(line, "mol wisp") {
wispLine = line
break
}
}
if wispLine == "" {
t.Fatalf("mol wisp command not found:\n%s", logContent)
}
if !strings.Contains(wispLine, "feature=My Cool Feature") {
t.Errorf("mol wisp missing feature variable:\n%s", wispLine)
}
if !strings.Contains(wispLine, "issue=gt-abc123") {
t.Errorf("mol wisp missing issue variable:\n%s", wispLine)
}
}