Files
gastown/internal/rig/manager_test.go
Erik LaBianca 14435cacad fix: update test assertions and set BEADS_DIR in EnsureCustomTypes (#853)
* fix: update test assertions and set BEADS_DIR in EnsureCustomTypes

- Update TestBuildAgentStartupCommand to check for 'exec env' instead
  of 'export' (matches current BuildStartupCommand implementation)
- Add 'config' command handling to fake bd script in manager_test.go
- Set BEADS_DIR env var when running bd config in EnsureCustomTypes
  to ensure bd operates on the correct database during agent bead creation
- Apply gofmt formatting

These fixes address pre-existing test failures on main.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: inject mock in TestRoleLabelCheck_NoBeadsDir for Windows CI

The test was failing on Windows CI because bd is not installed,
causing exec.LookPath("bd") to fail and return "beads not installed"
before checking for the .beads directory.

Inject an empty mock beadShower to skip the LookPath check, allowing
the test to properly verify the "No beads database" path.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: regenerate formulas and fix unused parameter lint error

- Regenerate mol-witness-patrol.formula.toml to sync with source
- Mark unused hookName parameter with _ in installHookTo

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(tests): make Windows CI tests pass

- Skip symlink tests on Windows (require elevated privileges)
- Fix GT_ROOT assertion to handle Windows path escaping
- Use platform-appropriate paths in TestNewManager_PathConstruction

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix tests for quoted env and OS paths

* fix(test): add Windows batch scripts to molecule lifecycle tests

The molecule_lifecycle_test.go tests were failing on Windows CI because
they used Unix shell scripts (#!/bin/sh) for mock bd commands, which
don't work on Windows.

This commit adds Windows batch file equivalents for all three tests:
- TestSlingFormulaOnBeadHooksBaseBead
- TestSlingFormulaOnBeadSetsAttachedMoleculeInBaseBead
- TestDoneClosesAttachedMolecule

Uses the same pattern as writeBDStub() from sling_test.go for
cross-platform test mocks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(test): add Windows batch scripts to more tests

Adds Windows batch script equivalents to tests that use mock bd commands:

molecule_lifecycle_test.go:
- TestSlingFormulaOnBeadHooksBaseBead
- TestSlingFormulaOnBeadSetsAttachedMoleculeInBaseBead
- TestDoneClosesAttachedMolecule

sling_288_test.go:
- TestInstantiateFormulaOnBead
- TestInstantiateFormulaOnBeadSkipCook
- TestCookFormula
- TestFormulaOnBeadPassesVariables

These tests were failing on Windows CI because they used Unix shell
scripts (#!/bin/sh) which don't work on Windows.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(test): skip TestSlingFormulaOnBeadSetsAttachedMoleculeInBaseBead on Windows

The test's Windows batch script JSON output causes
storeAttachedMoleculeInBead to fail silently when parsing the bd show
response. This is a pre-existing limitation - the test was failing on
Windows before the batch scripts were added (shell scripts don't work
on Windows at all).

Skip this test on Windows until the underlying JSON parsing issue is
resolved.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: re-trigger CI after GitHub Internal Server Error

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:43:21 -08:00

770 lines
22 KiB
Go

package rig
import (
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"testing"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/git"
)
func setupTestTown(t *testing.T) (string, *config.RigsConfig) {
t.Helper()
root := t.TempDir()
rigsConfig := &config.RigsConfig{
Version: 1,
Rigs: make(map[string]config.RigEntry),
}
return root, rigsConfig
}
func writeFakeBD(t *testing.T, script string, windowsScript string) string {
t.Helper()
binDir := t.TempDir()
if runtime.GOOS == "windows" {
if windowsScript == "" {
t.Fatal("windows script is required on Windows")
}
scriptPath := filepath.Join(binDir, "bd.cmd")
if err := os.WriteFile(scriptPath, []byte(windowsScript), 0644); err != nil {
t.Fatalf("write fake bd: %v", err)
}
return binDir
}
scriptPath := filepath.Join(binDir, "bd")
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
t.Fatalf("write fake bd: %v", err)
}
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 {
trimmed := strings.TrimSuffix(line, "\r")
if trimmed != want {
t.Fatalf("BEADS_DIR = %q, want %q", trimmed, want)
}
}
}
func createTestRig(t *testing.T, root, name string) {
t.Helper()
rigPath := filepath.Join(root, name)
if err := os.MkdirAll(rigPath, 0755); err != nil {
t.Fatalf("mkdir rig: %v", err)
}
// Create agent dirs (witness, refinery, mayor)
for _, dir := range AgentDirs {
dirPath := filepath.Join(rigPath, dir)
if err := os.MkdirAll(dirPath, 0755); err != nil {
t.Fatalf("mkdir %s: %v", dir, err)
}
}
// Create some polecats
polecatsDir := filepath.Join(rigPath, "polecats")
for _, polecat := range []string{"Toast", "Cheedo"} {
if err := os.MkdirAll(filepath.Join(polecatsDir, polecat), 0755); err != nil {
t.Fatalf("mkdir polecat: %v", err)
}
}
// Create a shared support dir that should not be treated as a polecat worktree.
if err := os.MkdirAll(filepath.Join(polecatsDir, ".claude"), 0755); err != nil {
t.Fatalf("mkdir polecats/.claude: %v", err)
}
}
func TestDiscoverRigs(t *testing.T) {
root, rigsConfig := setupTestTown(t)
// Create test rig
createTestRig(t, root, "gastown")
rigsConfig.Rigs["gastown"] = config.RigEntry{
GitURL: "git@github.com:test/gastown.git",
}
manager := NewManager(root, rigsConfig, git.NewGit(root))
rigs, err := manager.DiscoverRigs()
if err != nil {
t.Fatalf("DiscoverRigs: %v", err)
}
if len(rigs) != 1 {
t.Errorf("rigs count = %d, want 1", len(rigs))
}
rig := rigs[0]
if rig.Name != "gastown" {
t.Errorf("Name = %q, want gastown", rig.Name)
}
if len(rig.Polecats) != 2 {
t.Errorf("Polecats count = %d, want 2", len(rig.Polecats))
}
if slices.Contains(rig.Polecats, ".claude") {
t.Errorf("expected polecats/.claude to be ignored, got %v", rig.Polecats)
}
if !rig.HasWitness {
t.Error("expected HasWitness = true")
}
if !rig.HasRefinery {
t.Error("expected HasRefinery = true")
}
}
func TestGetRig(t *testing.T) {
root, rigsConfig := setupTestTown(t)
createTestRig(t, root, "test-rig")
rigsConfig.Rigs["test-rig"] = config.RigEntry{
GitURL: "git@github.com:test/test-rig.git",
}
manager := NewManager(root, rigsConfig, git.NewGit(root))
rig, err := manager.GetRig("test-rig")
if err != nil {
t.Fatalf("GetRig: %v", err)
}
if rig.Name != "test-rig" {
t.Errorf("Name = %q, want test-rig", rig.Name)
}
}
func TestGetRigNotFound(t *testing.T) {
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
_, err := manager.GetRig("nonexistent")
if err != ErrRigNotFound {
t.Errorf("GetRig = %v, want ErrRigNotFound", err)
}
}
func TestRigExists(t *testing.T) {
root, rigsConfig := setupTestTown(t)
rigsConfig.Rigs["exists"] = config.RigEntry{}
manager := NewManager(root, rigsConfig, git.NewGit(root))
if !manager.RigExists("exists") {
t.Error("expected RigExists = true for existing rig")
}
if manager.RigExists("nonexistent") {
t.Error("expected RigExists = false for nonexistent rig")
}
}
func TestRemoveRig(t *testing.T) {
root, rigsConfig := setupTestTown(t)
rigsConfig.Rigs["to-remove"] = config.RigEntry{}
manager := NewManager(root, rigsConfig, git.NewGit(root))
if err := manager.RemoveRig("to-remove"); err != nil {
t.Fatalf("RemoveRig: %v", err)
}
if manager.RigExists("to-remove") {
t.Error("rig should not exist after removal")
}
}
func TestRemoveRigNotFound(t *testing.T) {
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
err := manager.RemoveRig("nonexistent")
if err != ErrRigNotFound {
t.Errorf("RemoveRig = %v, want ErrRigNotFound", err)
}
}
func TestAddRig_RejectsInvalidNames(t *testing.T) {
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
tests := []struct {
name string
wantError string
}{
{"op-baby", `rig name "op-baby" contains invalid characters`},
{"my.rig", `rig name "my.rig" contains invalid characters`},
{"my rig", `rig name "my rig" contains invalid characters`},
{"op-baby-test", `rig name "op-baby-test" contains invalid characters`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := manager.AddRig(AddRigOptions{
Name: tt.name,
GitURL: "git@github.com:test/test.git",
})
if err == nil {
t.Errorf("AddRig(%q) succeeded, want error containing %q", tt.name, tt.wantError)
return
}
if !strings.Contains(err.Error(), tt.wantError) {
t.Errorf("AddRig(%q) error = %q, want error containing %q", tt.name, err.Error(), tt.wantError)
}
})
}
}
func TestListRigNames(t *testing.T) {
root, rigsConfig := setupTestTown(t)
rigsConfig.Rigs["rig1"] = config.RigEntry{}
rigsConfig.Rigs["rig2"] = config.RigEntry{}
manager := NewManager(root, rigsConfig, git.NewGit(root))
names := manager.ListRigNames()
if len(names) != 2 {
t.Errorf("names count = %d, want 2", len(names))
}
}
func TestRigSummary(t *testing.T) {
rig := &Rig{
Name: "test",
Polecats: []string{"a", "b", "c"},
HasWitness: true,
HasRefinery: false,
}
summary := rig.Summary()
if summary.Name != "test" {
t.Errorf("Name = %q, want test", summary.Name)
}
if summary.PolecatCount != 3 {
t.Errorf("PolecatCount = %d, want 3", summary.PolecatCount)
}
if !summary.HasWitness {
t.Error("expected HasWitness = true")
}
if summary.HasRefinery {
t.Error("expected HasRefinery = false")
}
}
func TestEnsureGitignoreEntry_AddsEntry(t *testing.T) {
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
gitignorePath := filepath.Join(root, ".gitignore")
if err := manager.ensureGitignoreEntry(gitignorePath, ".test-entry/"); err != nil {
t.Fatalf("ensureGitignoreEntry: %v", err)
}
content, _ := os.ReadFile(gitignorePath)
if string(content) != ".test-entry/\n" {
t.Errorf("content = %q, want .test-entry/", string(content))
}
}
func TestEnsureGitignoreEntry_DoesNotDuplicate(t *testing.T) {
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
gitignorePath := filepath.Join(root, ".gitignore")
// Pre-populate with the entry
if err := os.WriteFile(gitignorePath, []byte(".test-entry/\n"), 0644); err != nil {
t.Fatalf("writing .gitignore: %v", err)
}
if err := manager.ensureGitignoreEntry(gitignorePath, ".test-entry/"); err != nil {
t.Fatalf("ensureGitignoreEntry: %v", err)
}
content, _ := os.ReadFile(gitignorePath)
if string(content) != ".test-entry/\n" {
t.Errorf("content = %q, want single .test-entry/", string(content))
}
}
func TestEnsureGitignoreEntry_AppendsToExisting(t *testing.T) {
root, rigsConfig := setupTestTown(t)
manager := NewManager(root, rigsConfig, git.NewGit(root))
gitignorePath := filepath.Join(root, ".gitignore")
// Pre-populate with existing entries
if err := os.WriteFile(gitignorePath, []byte("node_modules/\n*.log\n"), 0644); err != nil {
t.Fatalf("writing .gitignore: %v", err)
}
if err := manager.ensureGitignoreEntry(gitignorePath, ".test-entry/"); err != nil {
t.Fatalf("ensureGitignoreEntry: %v", err)
}
content, _ := os.ReadFile(gitignorePath)
expected := "node_modules/\n*.log\n.test-entry/\n"
if string(content) != expected {
t.Errorf("content = %q, want %q", string(content), expected)
}
}
func TestInitBeads_TrackedBeads_CreatesRedirect(t *testing.T) {
t.Parallel()
// When the cloned repo has tracked beads (mayor/rig/.beads exists),
// initBeads should create a redirect file at <rig>/.beads/redirect
// pointing to mayor/rig/.beads instead of creating a local database.
rigPath := t.TempDir()
// Simulate tracked beads in the cloned repo
mayorBeadsDir := filepath.Join(rigPath, "mayor", "rig", ".beads")
if err := os.MkdirAll(mayorBeadsDir, 0755); err != nil {
t.Fatalf("mkdir mayor beads: %v", err)
}
// Create a config file to simulate a real beads directory
if err := os.WriteFile(filepath.Join(mayorBeadsDir, "config.yaml"), []byte("prefix: gt\n"), 0644); err != nil {
t.Fatalf("write mayor config: %v", err)
}
manager := &Manager{}
if err := manager.initBeads(rigPath, "gt"); err != nil {
t.Fatalf("initBeads: %v", err)
}
// Verify redirect file was created
redirectPath := filepath.Join(rigPath, ".beads", "redirect")
content, err := os.ReadFile(redirectPath)
if err != nil {
t.Fatalf("reading redirect file: %v", err)
}
expected := "mayor/rig/.beads\n"
if string(content) != expected {
t.Errorf("redirect content = %q, want %q", string(content), expected)
}
// Verify no local database was created (no config.yaml at rig level)
rigConfigPath := filepath.Join(rigPath, ".beads", "config.yaml")
if _, err := os.Stat(rigConfigPath); !os.IsNotExist(err) {
t.Errorf("expected no config.yaml at rig level when using redirect, but it exists")
}
}
func TestInitBeads_LocalBeads_CreatesDatabase(t *testing.T) {
// Cannot use t.Parallel() due to t.Setenv
// When the cloned repo does NOT have tracked beads (no mayor/rig/.beads),
// initBeads should create a local database at <rig>/.beads/
rigPath := t.TempDir()
// Create mayor/rig directory but WITHOUT .beads (no tracked beads)
mayorRigDir := filepath.Join(rigPath, "mayor", "rig")
if err := os.MkdirAll(mayorRigDir, 0755); err != nil {
t.Fatalf("mkdir mayor/rig: %v", err)
}
// Use fake bd that succeeds
script := `#!/usr/bin/env bash
set -e
if [[ "$1" == "init" ]]; then
# Simulate successful bd init
exit 0
fi
exit 0
`
windowsScript := "@echo off\r\nif \"%1\"==\"init\" exit /b 0\r\nexit /b 0\r\n"
binDir := writeFakeBD(t, script, windowsScript)
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
manager := &Manager{}
if err := manager.initBeads(rigPath, "gt"); err != nil {
t.Fatalf("initBeads: %v", err)
}
// Verify NO redirect file was created
redirectPath := filepath.Join(rigPath, ".beads", "redirect")
if _, err := os.Stat(redirectPath); !os.IsNotExist(err) {
t.Errorf("expected no redirect file for local beads, but it exists")
}
// Verify .beads directory was created
beadsDir := filepath.Join(rigPath, ".beads")
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
t.Errorf("expected .beads directory to be created")
}
}
func TestInitBeadsWritesConfigOnFailure(t *testing.T) {
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
echo "bd init failed" >&2
exit 1
fi
echo "unexpected command: $cmd" >&2
exit 1
`
windowsScript := "@echo off\r\nif defined BEADS_DIR_LOG (\r\n if defined BEADS_DIR (\r\n echo %BEADS_DIR%>>\"%BEADS_DIR_LOG%\"\r\n ) else (\r\n echo ^<unset^> >>\"%BEADS_DIR_LOG%\"\r\n )\r\n)\r\nif \"%1\"==\"init\" (\r\n exit /b 1\r\n)\r\nexit /b 1\r\n"
binDir := writeFakeBD(t, script, windowsScript)
beadsDirLog := filepath.Join(t.TempDir(), "beads-dir.log")
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("BEADS_DIR_LOG", beadsDirLog)
manager := &Manager{}
if err := manager.initBeads(rigPath, "gt"); err != nil {
t.Fatalf("initBeads: %v", err)
}
configPath := filepath.Join(beadsDir, "config.yaml")
config, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("reading config.yaml: %v", err)
}
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) {
if runtime.GOOS == "windows" {
t.Skip("fake bd stub is not compatible with multiline descriptions on Windows")
}
// 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,
// using the resolved rig beads directory for BEADS_DIR.
townRoot := t.TempDir()
rigPath := filepath.Join(townRoot, "testrip")
rigBeadsDir := filepath.Join(rigPath, ".beads")
if err := os.MkdirAll(rigBeadsDir, 0755); err != nil {
t.Fatalf("mkdir rig beads dir: %v", err)
}
// Track which agent IDs were created
var createdAgents []string
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
if [[ "$1" == "--allow-stale" ]]; then
shift
fi
cmd="$1"
shift
case "$cmd" in
show)
# Return empty to indicate agent doesn't exist yet
echo "[]"
;;
create)
id=""
title=""
for arg in "$@"; do
case "$arg" in
--id=*) id="${arg#--id=}" ;;
--title=*) title="${arg#--title=}" ;;
esac
done
# Log the created agent ID for verification
echo "$id" >> "$AGENT_LOG"
printf '{"id":"%s","title":"%s","description":"","issue_type":"agent"}' "$id" "$title"
;;
slot)
# Accept slot commands
;;
config)
# Accept config commands (e.g., "bd config set types.custom ...")
;;
*)
echo "unexpected command: $cmd" >&2
exit 1
;;
esac
`
windowsScript := "@echo off\r\nsetlocal enabledelayedexpansion\r\nif defined BEADS_DIR_LOG (\r\n if defined BEADS_DIR (\r\n echo %BEADS_DIR%>>\"%BEADS_DIR_LOG%\"\r\n ) else (\r\n echo ^<unset^> >>\"%BEADS_DIR_LOG%\"\r\n )\r\n)\r\nset \"cmd=%1\"\r\nset \"arg2=%2\"\r\nset \"arg3=%3\"\r\nif \"%cmd%\"==\"--no-daemon\" (\r\n set \"cmd=%2\"\r\n set \"arg2=%3\"\r\n set \"arg3=%4\"\r\n)\r\nif \"%cmd%\"==\"--allow-stale\" (\r\n set \"cmd=%2\"\r\n set \"arg2=%3\"\r\n set \"arg3=%4\"\r\n)\r\nif \"%cmd%\"==\"show\" (\r\n echo []\r\n exit /b 0\r\n)\r\nif \"%cmd%\"==\"create\" (\r\n set \"id=\"\r\n set \"title=\"\r\n for %%A in (%*) do (\r\n set \"arg=%%~A\"\r\n if /i \"!arg:~0,5!\"==\"--id=\" set \"id=!arg:~5!\"\r\n if /i \"!arg:~0,8!\"==\"--title=\" set \"title=!arg:~8!\"\r\n )\r\n if defined AGENT_LOG (\r\n echo !id!>>\"%AGENT_LOG%\"\r\n )\r\n echo {\"id\":\"!id!\",\"title\":\"!title!\",\"description\":\"\",\"issue_type\":\"agent\"}\r\n exit /b 0\r\n)\r\nif \"%cmd%\"==\"slot\" exit /b 0\r\nif \"%cmd%\"==\"config\" exit /b 0\r\nexit /b 1\r\n"
binDir := writeFakeBD(t, script, windowsScript)
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}
if err := manager.initAgentBeads(rigPath, "demo", "gt"); err != nil {
t.Fatalf("initAgentBeads: %v", err)
}
// Verify the expected rig-level agents were created
data, err := os.ReadFile(agentLog)
if err != nil {
t.Fatalf("reading agent log: %v", err)
}
createdAgents = strings.Split(strings.TrimSpace(string(data)), "\n")
// Should create witness and refinery for the rig
expectedAgents := map[string]bool{
"gt-demo-witness": false,
"gt-demo-refinery": false,
}
for _, id := range createdAgents {
if _, ok := expectedAgents[id]; ok {
expectedAgents[id] = true
}
}
for id, found := range expectedAgents {
if !found {
t.Errorf("expected agent %s was not created", id)
}
}
assertBeadsDirLog(t, beadsDirLog, rigBeadsDir)
}
func TestIsValidBeadsPrefix(t *testing.T) {
tests := []struct {
prefix string
want bool
}{
// Valid prefixes
{"gt", true},
{"bd", true},
{"hq", true},
{"gastown", true},
{"myProject", true},
{"my-project", true},
{"a", true},
{"A", true},
{"test123", true},
{"a1b2c3", true},
{"a-b-c", true},
// Invalid prefixes
{"", false}, // empty
{"1abc", false}, // starts with number
{"-abc", false}, // starts with hyphen
{"abc def", false}, // contains space
{"abc;ls", false}, // shell injection attempt
{"$(whoami)", false}, // command substitution
{"`id`", false}, // backtick command
{"abc|cat", false}, // pipe
{"../etc/passwd", false}, // path traversal
{"aaaaaaaaaaaaaaaaaaaaa", false}, // too long (21 chars, >20 limit)
{"valid-but-with-$var", false}, // variable reference
}
for _, tt := range tests {
t.Run(tt.prefix, func(t *testing.T) {
got := isValidBeadsPrefix(tt.prefix)
if got != tt.want {
t.Errorf("isValidBeadsPrefix(%q) = %v, want %v", tt.prefix, got, tt.want)
}
})
}
}
func TestInitBeadsRejectsInvalidPrefix(t *testing.T) {
rigPath := t.TempDir()
manager := &Manager{}
tests := []string{
"",
"$(whoami)",
"abc;rm -rf /",
"../etc",
"123",
}
for _, prefix := range tests {
t.Run(prefix, func(t *testing.T) {
err := manager.initBeads(rigPath, prefix)
if err == nil {
t.Errorf("initBeads(%q) should have failed", prefix)
}
if !strings.Contains(err.Error(), "invalid beads prefix") {
t.Errorf("initBeads(%q) error = %q, want error containing 'invalid beads prefix'", prefix, err.Error())
}
})
}
}
func TestDeriveBeadsPrefix(t *testing.T) {
tests := []struct {
name string
want string
}{
// Compound words with common suffixes should split
{"gastown", "gt"}, // gas + town
{"nashville", "nv"}, // nash + ville
{"bridgeport", "bp"}, // bridge + port
{"someplace", "sp"}, // some + place
{"greenland", "gl"}, // green + land
{"springfield", "sf"}, // spring + field
{"hollywood", "hw"}, // holly + wood
{"oxford", "of"}, // ox + ford
// Hyphenated names
{"my-project", "mp"},
{"gas-town", "gt"},
{"some-long-name", "sln"},
// Underscored names
{"my_project", "mp"},
// Short single words (use the whole name)
{"foo", "foo"},
{"bar", "bar"},
{"ab", "ab"},
// Longer single words without known suffixes (first 2 chars)
{"myrig", "my"},
{"awesome", "aw"},
{"coolrig", "co"},
// With language suffixes stripped
{"myproject-py", "my"},
{"myproject-go", "my"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := deriveBeadsPrefix(tt.name)
if got != tt.want {
t.Errorf("deriveBeadsPrefix(%q) = %q, want %q", tt.name, got, tt.want)
}
})
}
}
func TestSplitCompoundWord(t *testing.T) {
tests := []struct {
word string
want []string
}{
// Known suffixes
{"gastown", []string{"gas", "town"}},
{"nashville", []string{"nash", "ville"}},
{"bridgeport", []string{"bridge", "port"}},
{"someplace", []string{"some", "place"}},
{"greenland", []string{"green", "land"}},
{"springfield", []string{"spring", "field"}},
{"hollywood", []string{"holly", "wood"}},
{"oxford", []string{"ox", "ford"}},
// Just the suffix (should not split)
{"town", []string{"town"}},
{"ville", []string{"ville"}},
// No known suffix
{"myrig", []string{"myrig"}},
{"awesome", []string{"awesome"}},
// Empty prefix would result (should not split)
// Note: "town" itself shouldn't split to ["", "town"]
}
for _, tt := range tests {
t.Run(tt.word, func(t *testing.T) {
got := splitCompoundWord(tt.word)
if len(got) != len(tt.want) {
t.Errorf("splitCompoundWord(%q) = %v, want %v", tt.word, got, tt.want)
return
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("splitCompoundWord(%q)[%d] = %q, want %q", tt.word, i, got[i], tt.want[i])
}
}
})
}
}
func TestConvertToSSH(t *testing.T) {
tests := []struct {
name string
https string
wantSSH string
}{
{
name: "GitHub with .git suffix",
https: "https://github.com/owner/repo.git",
wantSSH: "git@github.com:owner/repo.git",
},
{
name: "GitHub without .git suffix",
https: "https://github.com/owner/repo",
wantSSH: "git@github.com:owner/repo.git",
},
{
name: "GitHub with org/subpath",
https: "https://github.com/myorg/myproject.git",
wantSSH: "git@github.com:myorg/myproject.git",
},
{
name: "GitLab with .git suffix",
https: "https://gitlab.com/owner/repo.git",
wantSSH: "git@gitlab.com:owner/repo.git",
},
{
name: "GitLab without .git suffix",
https: "https://gitlab.com/owner/repo",
wantSSH: "git@gitlab.com:owner/repo.git",
},
{
name: "Unknown host returns empty",
https: "https://bitbucket.org/owner/repo.git",
wantSSH: "",
},
{
name: "Non-HTTPS URL returns empty",
https: "git@github.com:owner/repo.git",
wantSSH: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := convertToSSH(tt.https)
if got != tt.wantSSH {
t.Errorf("convertToSSH(%q) = %q, want %q", tt.https, got, tt.wantSSH)
}
})
}
}