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
379 lines
10 KiB
Go
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)
|
|
}
|
|
}
|