Files
gastown/internal/cmd/sling_test.go
Joshua Vial a9ed342be6 fix: ignore hidden directories when enumerating polecats (#258)
* fix(sling): route bd mol commands to target rig directory

* Fix daemon polecat enumeration to ignore hidden dirs

* Ignore hidden dirs when discovering rig polecats

* Fix CI: enable beads custom types during install

---------

Co-authored-by: joshuavial <git@codewithjv.com>
2026-01-07 20:48:09 -08:00

346 lines
8.2 KiB
Go

package cmd
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseWispIDFromJSON(t *testing.T) {
tests := []struct {
name string
json string
wantID string
wantErr bool
}{
{
name: "new_epic_id",
json: `{"new_epic_id":"gt-wisp-abc","created":7,"phase":"vapor"}`,
wantID: "gt-wisp-abc",
},
{
name: "root_id legacy",
json: `{"root_id":"gt-wisp-legacy"}`,
wantID: "gt-wisp-legacy",
},
{
name: "result_id forward compat",
json: `{"result_id":"gt-wisp-result"}`,
wantID: "gt-wisp-result",
},
{
name: "precedence prefers new_epic_id",
json: `{"root_id":"gt-wisp-legacy","new_epic_id":"gt-wisp-new"}`,
wantID: "gt-wisp-new",
},
{
name: "missing id keys",
json: `{"created":7,"phase":"vapor"}`,
wantErr: true,
},
{
name: "invalid JSON",
json: `{"new_epic_id":`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotID, err := parseWispIDFromJSON([]byte(tt.json))
if (err != nil) != tt.wantErr {
t.Fatalf("parseWispIDFromJSON() error = %v, wantErr %v", err, tt.wantErr)
}
if gotID != tt.wantID {
t.Fatalf("parseWispIDFromJSON() id = %q, want %q", gotID, tt.wantID)
}
})
}
}
func TestFormatTrackBeadID(t *testing.T) {
tests := []struct {
name string
beadID string
expected string
}{
// HQ beads should remain unchanged
{
name: "hq bead unchanged",
beadID: "hq-abc123",
expected: "hq-abc123",
},
{
name: "hq convoy unchanged",
beadID: "hq-cv-xyz789",
expected: "hq-cv-xyz789",
},
// Cross-rig beads get external: prefix
{
name: "gastown rig bead",
beadID: "gt-mol-abc123",
expected: "external:gt-mol:gt-mol-abc123",
},
{
name: "beads rig task",
beadID: "beads-task-xyz",
expected: "external:beads-task:beads-task-xyz",
},
{
name: "two segment ID",
beadID: "foo-bar",
expected: "external:foo-bar:foo-bar",
},
// Edge cases
{
name: "single segment fallback",
beadID: "orphan",
expected: "orphan",
},
{
name: "empty string fallback",
beadID: "",
expected: "",
},
{
name: "many segments",
beadID: "a-b-c-d-e-f",
expected: "external:a-b:a-b-c-d-e-f",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatTrackBeadID(tt.beadID)
if result != tt.expected {
t.Errorf("formatTrackBeadID(%q) = %q, want %q", tt.beadID, result, tt.expected)
}
})
}
}
// TestFormatTrackBeadIDConsumerCompatibility verifies that the external ref format
// produced by formatTrackBeadID can be correctly parsed by the consumer pattern
// used in convoy.go, model.go, feed/convoy.go, and web/fetcher.go.
func TestFormatTrackBeadIDConsumerCompatibility(t *testing.T) {
// Consumer pattern from convoy.go:1062-1068:
// if strings.HasPrefix(issueID, "external:") {
// parts := strings.SplitN(issueID, ":", 3)
// if len(parts) == 3 {
// issueID = parts[2] // Extract the actual issue ID
// }
// }
tests := []struct {
name string
beadID string
wantOriginalID string
}{
{
name: "cross-rig bead round-trips",
beadID: "gt-mol-abc123",
wantOriginalID: "gt-mol-abc123",
},
{
name: "beads rig bead round-trips",
beadID: "beads-task-xyz",
wantOriginalID: "beads-task-xyz",
},
{
name: "hq bead unchanged",
beadID: "hq-abc123",
wantOriginalID: "hq-abc123",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
formatted := formatTrackBeadID(tt.beadID)
// Simulate consumer parsing logic
parsed := formatted
if len(formatted) > 9 && formatted[:9] == "external:" {
parts := make([]string, 0, 3)
start := 0
count := 0
for i := 0; i < len(formatted) && count < 2; i++ {
if formatted[i] == ':' {
parts = append(parts, formatted[start:i])
start = i + 1
count++
}
}
if count == 2 {
parts = append(parts, formatted[start:])
}
if len(parts) == 3 {
parsed = parts[2]
}
}
if parsed != tt.wantOriginalID {
t.Errorf("round-trip failed: formatTrackBeadID(%q) = %q, parsed back to %q, want %q",
tt.beadID, formatted, parsed, tt.wantOriginalID)
}
})
}
}
func TestSlingFormulaOnBeadRoutesBDCommandsToTargetRig(t *testing.T) {
townRoot := t.TempDir()
// Minimal workspace marker so workspace.FindFromCwd() succeeds.
if err := os.MkdirAll(filepath.Join(townRoot, "mayor", "rig"), 0755); err != nil {
t.Fatalf("mkdir mayor/rig: %v", err)
}
// Create a rig path that owns gt-* beads, and a routes.jsonl pointing to it.
rigDir := filepath.Join(townRoot, "gastown", "mayor", "rig")
if err := os.MkdirAll(filepath.Join(townRoot, ".beads"), 0755); err != nil {
t.Fatalf("mkdir .beads: %v", err)
}
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)
}
// Stub bd so we can observe the working directory for cook/wisp/bond.
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")
bdPath := filepath.Join(binDir, "bd")
bdScript := `#!/bin/sh
set -e
echo "$(pwd)|$*" >> "${BD_LOG}"
if [ "$1" = "--no-daemon" ]; then
shift
fi
cmd="$1"
shift || true
case "$cmd" in
show)
echo '[{"title":"Test issue","status":"open","assignee":"","description":""}]'
;;
formula)
# formula show <name>
exit 0
;;
cook)
exit 0
;;
mol)
sub="$1"
shift || true
case "$sub" in
wisp)
echo '{"new_epic_id":"gt-wisp-xyz"}'
;;
bond)
echo '{"root_id":"gt-wisp-xyz"}'
;;
esac
;;
esac
exit 0
`
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"))
t.Setenv(EnvGTRole, "mayor")
t.Setenv("GT_POLECAT", "")
t.Setenv("GT_CREW", "")
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)
}
// Ensure we don't leak global flag state across tests.
prevOn := slingOnTarget
prevVars := slingVars
prevDryRun := slingDryRun
prevNoConvoy := slingNoConvoy
t.Cleanup(func() {
slingOnTarget = prevOn
slingVars = prevVars
slingDryRun = prevDryRun
slingNoConvoy = prevNoConvoy
})
slingDryRun = false
slingNoConvoy = true
slingVars = nil
slingOnTarget = "gt-abc123"
if err := runSling(nil, []string{"mol-review"}); err != nil {
t.Fatalf("runSling: %v", err)
}
logBytes, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("read bd log: %v", err)
}
logLines := strings.Split(strings.TrimSpace(string(logBytes)), "\n")
wantDir := rigDir
if resolved, err := filepath.EvalSymlinks(wantDir); err == nil {
wantDir = resolved
}
gotCook := false
gotWisp := false
gotBond := false
for _, line := range logLines {
parts := strings.SplitN(line, "|", 2)
if len(parts) != 2 {
continue
}
dir := parts[0]
if resolved, err := filepath.EvalSymlinks(dir); err == nil {
dir = resolved
}
args := parts[1]
switch {
case strings.Contains(args, " cook "):
gotCook = true
if dir != wantDir {
t.Fatalf("bd cook ran in %q, want %q (args: %q)", dir, wantDir, args)
}
case strings.Contains(args, " mol wisp "):
gotWisp = true
if dir != wantDir {
t.Fatalf("bd mol wisp ran in %q, want %q (args: %q)", dir, wantDir, args)
}
case strings.Contains(args, " mol bond "):
gotBond = true
if dir != wantDir {
t.Fatalf("bd mol bond ran in %q, want %q (args: %q)", dir, wantDir, args)
}
}
}
if !gotCook || !gotWisp || !gotBond {
t.Fatalf("missing expected bd commands: cook=%v wisp=%v bond=%v (log: %q)", gotCook, gotWisp, gotBond, string(logBytes))
}
}