The test helper createTestGitRepo was using plain `git init` which creates a branch based on the system's init.defaultBranch config. When AddRig tries to detect and checkout the default branch, it falls back to "main" if detection fails, causing "pathspec 'main' did not match" errors in CI where the system default is "master". 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
603 lines
16 KiB
Go
603 lines
16 KiB
Go
//go:build integration
|
|
|
|
// Package cmd contains integration tests for the rig command.
|
|
//
|
|
// Run with: go test -tags=integration ./internal/cmd -run TestRigAdd -v
|
|
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/steveyegge/gastown/internal/beads"
|
|
"github.com/steveyegge/gastown/internal/config"
|
|
"github.com/steveyegge/gastown/internal/git"
|
|
"github.com/steveyegge/gastown/internal/rig"
|
|
)
|
|
|
|
// createTestGitRepo creates a minimal git repository for testing.
|
|
// Returns the path to the bare repo URL (suitable for cloning).
|
|
func createTestGitRepo(t *testing.T, name string) string {
|
|
t.Helper()
|
|
|
|
// Create a regular repo with initial commit
|
|
repoDir := filepath.Join(t.TempDir(), name)
|
|
if err := os.MkdirAll(repoDir, 0755); err != nil {
|
|
t.Fatalf("mkdir repo: %v", err)
|
|
}
|
|
|
|
// Initialize git repo with explicit main branch
|
|
// (system default may vary, causing checkout failures)
|
|
cmds := [][]string{
|
|
{"git", "init", "--initial-branch=main"},
|
|
{"git", "config", "user.email", "test@test.com"},
|
|
{"git", "config", "user.name", "Test User"},
|
|
}
|
|
for _, args := range cmds {
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
cmd.Dir = repoDir
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v: %v\n%s", args, err, out)
|
|
}
|
|
}
|
|
|
|
// Create initial file and commit
|
|
readmePath := filepath.Join(repoDir, "README.md")
|
|
if err := os.WriteFile(readmePath, []byte("# Test Repo\n"), 0644); err != nil {
|
|
t.Fatalf("write README: %v", err)
|
|
}
|
|
|
|
commitCmds := [][]string{
|
|
{"git", "add", "."},
|
|
{"git", "commit", "-m", "Initial commit"},
|
|
}
|
|
for _, args := range commitCmds {
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
cmd.Dir = repoDir
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v: %v\n%s", args, err, out)
|
|
}
|
|
}
|
|
|
|
// Return the path as a file:// URL
|
|
return repoDir
|
|
}
|
|
|
|
// setupTestTown creates a minimal Gas Town workspace for testing.
|
|
// Returns townRoot and a cleanup function.
|
|
func setupTestTown(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
townRoot := t.TempDir()
|
|
|
|
// Create mayor directory (required for rigs.json)
|
|
mayorDir := filepath.Join(townRoot, "mayor")
|
|
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
|
t.Fatalf("mkdir mayor: %v", err)
|
|
}
|
|
|
|
// Create empty rigs.json
|
|
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)
|
|
}
|
|
|
|
// Create .beads directory for routes
|
|
beadsDir := filepath.Join(townRoot, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir .beads: %v", err)
|
|
}
|
|
|
|
return townRoot
|
|
}
|
|
|
|
// mockBdCommand creates a fake bd binary that simulates bd behavior.
|
|
// This avoids needing bd installed for tests.
|
|
func mockBdCommand(t *testing.T) {
|
|
t.Helper()
|
|
|
|
binDir := t.TempDir()
|
|
bdPath := filepath.Join(binDir, "bd")
|
|
|
|
// Create a script that simulates bd init and other commands
|
|
script := `#!/bin/sh
|
|
# Mock bd for testing
|
|
|
|
case "$1" in
|
|
init)
|
|
# Create .beads directory and config.yaml
|
|
mkdir -p .beads
|
|
prefix="gt"
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--prefix=*) prefix="${arg#--prefix=}" ;;
|
|
--prefix)
|
|
# Next arg is the prefix
|
|
shift
|
|
if [ -n "$1" ] && [ "$1" != "--"* ]; then
|
|
prefix="$1"
|
|
fi
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
# Handle positional --prefix VALUE
|
|
shift # skip 'init'
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--prefix)
|
|
shift
|
|
prefix="$1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
echo "prefix: $prefix" > .beads/config.yaml
|
|
exit 0
|
|
;;
|
|
migrate)
|
|
exit 0
|
|
;;
|
|
show)
|
|
echo '{"error":"not found"}' >&2
|
|
exit 1
|
|
;;
|
|
create)
|
|
# Return minimal JSON for agent bead creation
|
|
echo '{}'
|
|
exit 0
|
|
;;
|
|
mol|list)
|
|
exit 0
|
|
;;
|
|
*)
|
|
exit 0
|
|
;;
|
|
esac
|
|
`
|
|
if err := os.WriteFile(bdPath, []byte(script), 0755); err != nil {
|
|
t.Fatalf("write mock bd: %v", err)
|
|
}
|
|
|
|
// Prepend to PATH
|
|
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
}
|
|
|
|
// TestRigAddCreatesCorrectStructure verifies that gt rig add creates
|
|
// the expected directory structure.
|
|
func TestRigAddCreatesCorrectStructure(t *testing.T) {
|
|
mockBdCommand(t)
|
|
townRoot := setupTestTown(t)
|
|
gitURL := createTestGitRepo(t, "testproject")
|
|
|
|
// Load rigs config
|
|
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
|
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
|
if err != nil {
|
|
t.Fatalf("load rigs.json: %v", err)
|
|
}
|
|
|
|
// Create rig manager and add rig
|
|
g := git.NewGit(townRoot)
|
|
mgr := rig.NewManager(townRoot, rigsConfig, g)
|
|
|
|
_, err = mgr.AddRig(rig.AddRigOptions{
|
|
Name: "testrig",
|
|
GitURL: gitURL,
|
|
BeadsPrefix: "tr",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AddRig: %v", err)
|
|
}
|
|
|
|
rigPath := filepath.Join(townRoot, "testrig")
|
|
|
|
// Verify directory structure
|
|
expectedDirs := []string{
|
|
"", // rig root
|
|
"mayor", // mayor container
|
|
"mayor/rig", // mayor clone
|
|
"refinery", // refinery container
|
|
"refinery/rig", // refinery worktree
|
|
"witness", // witness dir
|
|
"polecats", // polecats dir
|
|
"crew", // crew dir
|
|
".beads", // beads dir
|
|
"plugins", // plugins dir
|
|
}
|
|
|
|
for _, dir := range expectedDirs {
|
|
path := filepath.Join(rigPath, dir)
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Errorf("expected directory %s to exist: %v", dir, err)
|
|
continue
|
|
}
|
|
if !info.IsDir() {
|
|
t.Errorf("expected %s to be a directory", dir)
|
|
}
|
|
}
|
|
|
|
// Verify config.json exists
|
|
configPath := filepath.Join(rigPath, "config.json")
|
|
if _, err := os.Stat(configPath); err != nil {
|
|
t.Errorf("config.json not found: %v", err)
|
|
}
|
|
|
|
// Verify .repo.git (bare repo) exists
|
|
bareRepoPath := filepath.Join(rigPath, ".repo.git")
|
|
if _, err := os.Stat(bareRepoPath); err != nil {
|
|
t.Errorf(".repo.git not found: %v", err)
|
|
}
|
|
|
|
// Verify mayor/rig is a git repo
|
|
mayorRigPath := filepath.Join(rigPath, "mayor", "rig")
|
|
gitDirPath := filepath.Join(mayorRigPath, ".git")
|
|
if _, err := os.Stat(gitDirPath); err != nil {
|
|
t.Errorf("mayor/rig/.git not found: %v", err)
|
|
}
|
|
|
|
// Verify refinery/rig is a git worktree (has .git file pointing to bare repo)
|
|
refineryRigPath := filepath.Join(rigPath, "refinery", "rig")
|
|
refineryGitPath := filepath.Join(refineryRigPath, ".git")
|
|
info, err := os.Stat(refineryGitPath)
|
|
if err != nil {
|
|
t.Errorf("refinery/rig/.git not found: %v", err)
|
|
} else if info.IsDir() {
|
|
t.Errorf("refinery/rig/.git should be a file (worktree), not a directory")
|
|
}
|
|
}
|
|
|
|
// TestRigAddInitializesBeads verifies that beads is initialized with
|
|
// the correct prefix.
|
|
func TestRigAddInitializesBeads(t *testing.T) {
|
|
mockBdCommand(t)
|
|
townRoot := setupTestTown(t)
|
|
gitURL := createTestGitRepo(t, "beadstest")
|
|
|
|
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
|
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
|
if err != nil {
|
|
t.Fatalf("load rigs.json: %v", err)
|
|
}
|
|
|
|
g := git.NewGit(townRoot)
|
|
mgr := rig.NewManager(townRoot, rigsConfig, g)
|
|
|
|
newRig, err := mgr.AddRig(rig.AddRigOptions{
|
|
Name: "beadstest",
|
|
GitURL: gitURL,
|
|
BeadsPrefix: "bt",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AddRig: %v", err)
|
|
}
|
|
|
|
// Verify rig config has correct prefix
|
|
if newRig.Config == nil {
|
|
t.Fatal("rig.Config is nil")
|
|
}
|
|
if newRig.Config.Prefix != "bt" {
|
|
t.Errorf("rig.Config.Prefix = %q, want %q", newRig.Config.Prefix, "bt")
|
|
}
|
|
|
|
// Verify .beads directory was created
|
|
beadsDir := filepath.Join(townRoot, "beadstest", ".beads")
|
|
if _, err := os.Stat(beadsDir); err != nil {
|
|
t.Errorf(".beads directory not found: %v", err)
|
|
}
|
|
|
|
// Verify config.yaml exists with correct prefix
|
|
configPath := filepath.Join(beadsDir, "config.yaml")
|
|
if _, err := os.Stat(configPath); err != nil {
|
|
t.Errorf(".beads/config.yaml not found: %v", err)
|
|
} else {
|
|
content, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Errorf("reading config.yaml: %v", err)
|
|
} else if !strings.Contains(string(content), "prefix: bt") && !strings.Contains(string(content), "prefix:bt") {
|
|
t.Errorf("config.yaml doesn't contain expected prefix, got: %s", string(content))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRigAddUpdatesRoutes verifies that routes.jsonl is updated
|
|
// with the new rig's route.
|
|
func TestRigAddUpdatesRoutes(t *testing.T) {
|
|
mockBdCommand(t)
|
|
townRoot := setupTestTown(t)
|
|
gitURL := createTestGitRepo(t, "routetest")
|
|
|
|
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
|
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
|
if err != nil {
|
|
t.Fatalf("load rigs.json: %v", err)
|
|
}
|
|
|
|
g := git.NewGit(townRoot)
|
|
mgr := rig.NewManager(townRoot, rigsConfig, g)
|
|
|
|
newRig, err := mgr.AddRig(rig.AddRigOptions{
|
|
Name: "routetest",
|
|
GitURL: gitURL,
|
|
BeadsPrefix: "rt",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AddRig: %v", err)
|
|
}
|
|
|
|
// Append route to routes.jsonl (this is done by the CLI command, not AddRig)
|
|
// The CLI command in runRigAdd calls beads.AppendRoute after AddRig succeeds
|
|
if newRig.Config != nil && newRig.Config.Prefix != "" {
|
|
route := beads.Route{
|
|
Prefix: newRig.Config.Prefix + "-",
|
|
Path: "routetest",
|
|
}
|
|
if err := beads.AppendRoute(townRoot, route); err != nil {
|
|
t.Fatalf("AppendRoute: %v", err)
|
|
}
|
|
}
|
|
|
|
// Save rigs config (normally done by the command)
|
|
if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
|
|
t.Fatalf("save rigs.json: %v", err)
|
|
}
|
|
|
|
// Load routes and verify the new route exists
|
|
townBeadsDir := filepath.Join(townRoot, ".beads")
|
|
routes, err := beads.LoadRoutes(townBeadsDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadRoutes: %v", err)
|
|
}
|
|
|
|
// Find route for our rig
|
|
var foundRoute *beads.Route
|
|
for _, r := range routes {
|
|
if r.Prefix == "rt-" {
|
|
foundRoute = &r
|
|
break
|
|
}
|
|
}
|
|
|
|
if foundRoute == nil {
|
|
t.Error("route with prefix 'rt-' not found in routes.jsonl")
|
|
t.Logf("routes: %+v", routes)
|
|
} else {
|
|
// The path should point to the rig (or mayor/rig if .beads is tracked in source)
|
|
if !strings.HasPrefix(foundRoute.Path, "routetest") {
|
|
t.Errorf("route path = %q, want prefix 'routetest'", foundRoute.Path)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRigAddUpdatesRigsJson verifies that rigs.json is updated
|
|
// with the new rig entry.
|
|
func TestRigAddUpdatesRigsJson(t *testing.T) {
|
|
mockBdCommand(t)
|
|
townRoot := setupTestTown(t)
|
|
gitURL := createTestGitRepo(t, "jsontest")
|
|
|
|
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
|
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
|
if err != nil {
|
|
t.Fatalf("load rigs.json: %v", err)
|
|
}
|
|
|
|
g := git.NewGit(townRoot)
|
|
mgr := rig.NewManager(townRoot, rigsConfig, g)
|
|
|
|
_, err = mgr.AddRig(rig.AddRigOptions{
|
|
Name: "jsontest",
|
|
GitURL: gitURL,
|
|
BeadsPrefix: "jt",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AddRig: %v", err)
|
|
}
|
|
|
|
// Save rigs config (normally done by the command)
|
|
if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
|
|
t.Fatalf("save rigs.json: %v", err)
|
|
}
|
|
|
|
// Reload and verify
|
|
rigsConfig2, err := config.LoadRigsConfig(rigsPath)
|
|
if err != nil {
|
|
t.Fatalf("reload rigs.json: %v", err)
|
|
}
|
|
|
|
entry, ok := rigsConfig2.Rigs["jsontest"]
|
|
if !ok {
|
|
t.Error("rig 'jsontest' not found in rigs.json")
|
|
t.Logf("rigs: %+v", rigsConfig2.Rigs)
|
|
} else {
|
|
if entry.GitURL != gitURL {
|
|
t.Errorf("GitURL = %q, want %q", entry.GitURL, gitURL)
|
|
}
|
|
if entry.BeadsConfig == nil {
|
|
t.Error("BeadsConfig is nil")
|
|
} else if entry.BeadsConfig.Prefix != "jt" {
|
|
t.Errorf("BeadsConfig.Prefix = %q, want %q", entry.BeadsConfig.Prefix, "jt")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRigAddDerivesPrefix verifies that when no prefix is specified,
|
|
// one is derived from the rig name.
|
|
func TestRigAddDerivesPrefix(t *testing.T) {
|
|
mockBdCommand(t)
|
|
townRoot := setupTestTown(t)
|
|
gitURL := createTestGitRepo(t, "myproject")
|
|
|
|
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
|
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
|
if err != nil {
|
|
t.Fatalf("load rigs.json: %v", err)
|
|
}
|
|
|
|
g := git.NewGit(townRoot)
|
|
mgr := rig.NewManager(townRoot, rigsConfig, g)
|
|
|
|
newRig, err := mgr.AddRig(rig.AddRigOptions{
|
|
Name: "myproject",
|
|
GitURL: gitURL,
|
|
// No BeadsPrefix - should be derived
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AddRig: %v", err)
|
|
}
|
|
|
|
// For a single-word name like "myproject", the prefix should be first 2 chars
|
|
if newRig.Config.Prefix != "my" {
|
|
t.Errorf("derived prefix = %q, want %q", newRig.Config.Prefix, "my")
|
|
}
|
|
}
|
|
|
|
// TestRigAddCreatesRigConfig verifies that config.json contains
|
|
// the correct rig configuration.
|
|
func TestRigAddCreatesRigConfig(t *testing.T) {
|
|
mockBdCommand(t)
|
|
townRoot := setupTestTown(t)
|
|
gitURL := createTestGitRepo(t, "configtest")
|
|
|
|
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
|
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
|
if err != nil {
|
|
t.Fatalf("load rigs.json: %v", err)
|
|
}
|
|
|
|
g := git.NewGit(townRoot)
|
|
mgr := rig.NewManager(townRoot, rigsConfig, g)
|
|
|
|
_, err = mgr.AddRig(rig.AddRigOptions{
|
|
Name: "configtest",
|
|
GitURL: gitURL,
|
|
BeadsPrefix: "ct",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AddRig: %v", err)
|
|
}
|
|
|
|
// Read and verify config.json
|
|
configPath := filepath.Join(townRoot, "configtest", "config.json")
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatalf("reading config.json: %v", err)
|
|
}
|
|
|
|
var rigCfg rig.RigConfig
|
|
if err := json.Unmarshal(data, &rigCfg); err != nil {
|
|
t.Fatalf("parsing config.json: %v", err)
|
|
}
|
|
|
|
if rigCfg.Type != "rig" {
|
|
t.Errorf("Type = %q, want 'rig'", rigCfg.Type)
|
|
}
|
|
if rigCfg.Name != "configtest" {
|
|
t.Errorf("Name = %q, want 'configtest'", rigCfg.Name)
|
|
}
|
|
if rigCfg.GitURL != gitURL {
|
|
t.Errorf("GitURL = %q, want %q", rigCfg.GitURL, gitURL)
|
|
}
|
|
if rigCfg.Beads == nil {
|
|
t.Error("Beads config is nil")
|
|
} else if rigCfg.Beads.Prefix != "ct" {
|
|
t.Errorf("Beads.Prefix = %q, want 'ct'", rigCfg.Beads.Prefix)
|
|
}
|
|
if rigCfg.DefaultBranch == "" {
|
|
t.Error("DefaultBranch is empty")
|
|
}
|
|
}
|
|
|
|
// TestRigAddCreatesAgentDirs verifies that agent state files are created.
|
|
func TestRigAddCreatesAgentDirs(t *testing.T) {
|
|
mockBdCommand(t)
|
|
townRoot := setupTestTown(t)
|
|
gitURL := createTestGitRepo(t, "agenttest")
|
|
|
|
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
|
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
|
if err != nil {
|
|
t.Fatalf("load rigs.json: %v", err)
|
|
}
|
|
|
|
g := git.NewGit(townRoot)
|
|
mgr := rig.NewManager(townRoot, rigsConfig, g)
|
|
|
|
_, err = mgr.AddRig(rig.AddRigOptions{
|
|
Name: "agenttest",
|
|
GitURL: gitURL,
|
|
BeadsPrefix: "at",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AddRig: %v", err)
|
|
}
|
|
|
|
rigPath := filepath.Join(townRoot, "agenttest")
|
|
|
|
// Verify agent directories exist (state.json files are no longer created)
|
|
expectedDirs := []string{
|
|
"witness",
|
|
"refinery",
|
|
"mayor",
|
|
}
|
|
|
|
for _, dir := range expectedDirs {
|
|
path := filepath.Join(rigPath, dir)
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Errorf("expected directory %s to exist: %v", dir, err)
|
|
} else if !info.IsDir() {
|
|
t.Errorf("expected %s to be a directory", dir)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRigAddRejectsInvalidNames verifies that rig names with invalid
|
|
// characters are rejected.
|
|
func TestRigAddRejectsInvalidNames(t *testing.T) {
|
|
mockBdCommand(t)
|
|
townRoot := setupTestTown(t)
|
|
gitURL := createTestGitRepo(t, "validname")
|
|
|
|
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
|
|
rigsConfig, err := config.LoadRigsConfig(rigsPath)
|
|
if err != nil {
|
|
t.Fatalf("load rigs.json: %v", err)
|
|
}
|
|
|
|
g := git.NewGit(townRoot)
|
|
mgr := rig.NewManager(townRoot, rigsConfig, g)
|
|
|
|
// Characters that break agent ID parsing (hyphens, dots, spaces)
|
|
// Note: underscores are allowed
|
|
invalidNames := []string{
|
|
"my-rig", // hyphens break agent ID parsing
|
|
"my.rig", // dots break parsing
|
|
"my rig", // spaces are invalid
|
|
"my-multi-rig", // multiple hyphens
|
|
}
|
|
|
|
for _, name := range invalidNames {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := mgr.AddRig(rig.AddRigOptions{
|
|
Name: name,
|
|
GitURL: gitURL,
|
|
})
|
|
if err == nil {
|
|
t.Errorf("AddRig(%q) should have failed", name)
|
|
} else if !strings.Contains(err.Error(), "invalid characters") {
|
|
t.Errorf("AddRig(%q) error = %v, want 'invalid characters'", name, err)
|
|
}
|
|
})
|
|
}
|
|
}
|