Files
gastown/internal/crew/manager_test.go
joshuavial 183a0d7d8d fix(crew): use directory name as source of truth in loadState (#785)
Fixes gt crew list showing wrong names when state.json contains stale data.
Always use directory name as source of truth in loadState() instead of
trusting potentially stale state.json.

Co-authored-by: joshuavial <git@codewithjv.com>
2026-01-20 14:16:24 -08:00

400 lines
11 KiB
Go

package crew
import (
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/rig"
)
func TestManagerAddAndGet(t *testing.T) {
// Create temp directory for test
tmpDir, err := os.MkdirTemp("", "crew-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tmpDir) }()
// Create a mock rig
rigPath := filepath.Join(tmpDir, "test-rig")
if err := os.MkdirAll(rigPath, 0755); err != nil {
t.Fatalf("failed to create rig dir: %v", err)
}
// Initialize git repo for the rig
g := git.NewGit(rigPath)
// For testing, we need a git URL - use a local bare repo
bareRepoPath := filepath.Join(tmpDir, "bare-repo.git")
cmd := []string{"git", "init", "--bare", bareRepoPath}
if err := runCmd(cmd[0], cmd[1:]...); err != nil {
t.Fatalf("failed to create bare repo: %v", err)
}
r := &rig.Rig{
Name: "test-rig",
Path: rigPath,
GitURL: bareRepoPath,
}
mgr := NewManager(r, g)
// Test Add
worker, err := mgr.Add("dave", false)
if err != nil {
t.Fatalf("Add failed: %v", err)
}
if worker.Name != "dave" {
t.Errorf("expected name 'dave', got '%s'", worker.Name)
}
if worker.Rig != "test-rig" {
t.Errorf("expected rig 'test-rig', got '%s'", worker.Rig)
}
if worker.Branch != "main" {
t.Errorf("expected branch 'main', got '%s'", worker.Branch)
}
// Verify directory structure
crewDir := filepath.Join(rigPath, "crew", "dave")
if _, err := os.Stat(crewDir); os.IsNotExist(err) {
t.Error("crew directory was not created")
}
mailDir := filepath.Join(crewDir, "mail")
if _, err := os.Stat(mailDir); os.IsNotExist(err) {
t.Error("mail directory was not created")
}
// NOTE: CLAUDE.md is NOT created by Add() - it's injected via SessionStart hook
// See manager.go line 107-110 for why we skip CLAUDE.md creation
stateFile := filepath.Join(crewDir, "state.json")
if _, err := os.Stat(stateFile); os.IsNotExist(err) {
t.Error("state.json was not created")
}
// Test Get
retrieved, err := mgr.Get("dave")
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if retrieved.Name != "dave" {
t.Errorf("expected name 'dave', got '%s'", retrieved.Name)
}
// Test duplicate Add
_, err = mgr.Add("dave", false)
if err != ErrCrewExists {
t.Errorf("expected ErrCrewExists, got %v", err)
}
// Test Get non-existent
_, err = mgr.Get("nonexistent")
if err != ErrCrewNotFound {
t.Errorf("expected ErrCrewNotFound, got %v", err)
}
}
func TestManagerAddUsesLocalRepoReference(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "crew-test-local-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tmpDir) }()
rigPath := filepath.Join(tmpDir, "test-rig")
if err := os.MkdirAll(rigPath, 0755); err != nil {
t.Fatalf("failed to create rig dir: %v", err)
}
remoteRepoPath := filepath.Join(tmpDir, "remote.git")
if err := runCmd("git", "init", "--bare", remoteRepoPath); err != nil {
t.Fatalf("failed to create bare repo: %v", err)
}
localRepoPath := filepath.Join(tmpDir, "local-repo")
if err := runCmd("git", "init", localRepoPath); err != nil {
t.Fatalf("failed to init local repo: %v", err)
}
if err := runCmd("git", "-C", localRepoPath, "config", "user.email", "test@test.com"); err != nil {
t.Fatalf("failed to configure email: %v", err)
}
if err := runCmd("git", "-C", localRepoPath, "config", "user.name", "Test"); err != nil {
t.Fatalf("failed to configure name: %v", err)
}
if err := runCmd("git", "-C", localRepoPath, "remote", "add", "origin", remoteRepoPath); err != nil {
t.Fatalf("failed to add origin: %v", err)
}
if err := os.WriteFile(filepath.Join(localRepoPath, "README.md"), []byte("# Test\n"), 0644); err != nil {
t.Fatalf("failed to write file: %v", err)
}
if err := runCmd("git", "-C", localRepoPath, "add", "."); err != nil {
t.Fatalf("failed to add file: %v", err)
}
if err := runCmd("git", "-C", localRepoPath, "commit", "-m", "initial"); err != nil {
t.Fatalf("failed to commit: %v", err)
}
r := &rig.Rig{
Name: "test-rig",
Path: rigPath,
GitURL: remoteRepoPath,
LocalRepo: localRepoPath,
}
mgr := NewManager(r, git.NewGit(rigPath))
worker, err := mgr.Add("dave", false)
if err != nil {
t.Fatalf("Add failed: %v", err)
}
alternates := filepath.Join(worker.ClonePath, ".git", "objects", "info", "alternates")
if _, err := os.Stat(alternates); err != nil {
t.Fatalf("expected alternates file: %v", err)
}
}
func TestManagerAddWithBranch(t *testing.T) {
// Create temp directory for test
tmpDir, err := os.MkdirTemp("", "crew-test-branch-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tmpDir) }()
// Create a mock rig
rigPath := filepath.Join(tmpDir, "test-rig")
if err := os.MkdirAll(rigPath, 0755); err != nil {
t.Fatalf("failed to create rig dir: %v", err)
}
g := git.NewGit(rigPath)
// Create a local repo with initial commit for branch testing
sourceRepoPath := filepath.Join(tmpDir, "source-repo")
if err := os.MkdirAll(sourceRepoPath, 0755); err != nil {
t.Fatalf("failed to create source repo dir: %v", err)
}
// Initialize source repo with a commit
cmds := [][]string{
{"git", "-C", sourceRepoPath, "init"},
{"git", "-C", sourceRepoPath, "config", "user.email", "test@test.com"},
{"git", "-C", sourceRepoPath, "config", "user.name", "Test"},
}
for _, cmd := range cmds {
if err := runCmd(cmd[0], cmd[1:]...); err != nil {
t.Fatalf("failed to run %v: %v", cmd, err)
}
}
// Create initial file and commit
testFile := filepath.Join(sourceRepoPath, "README.md")
if err := os.WriteFile(testFile, []byte("# Test"), 0644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
cmds = [][]string{
{"git", "-C", sourceRepoPath, "add", "."},
{"git", "-C", sourceRepoPath, "commit", "-m", "Initial commit"},
}
for _, cmd := range cmds {
if err := runCmd(cmd[0], cmd[1:]...); err != nil {
t.Fatalf("failed to run %v: %v", cmd, err)
}
}
r := &rig.Rig{
Name: "test-rig",
Path: rigPath,
GitURL: sourceRepoPath,
}
mgr := NewManager(r, g)
// Test Add with branch
worker, err := mgr.Add("emma", true)
if err != nil {
t.Fatalf("Add with branch failed: %v", err)
}
if worker.Branch != "crew/emma" {
t.Errorf("expected branch 'crew/emma', got '%s'", worker.Branch)
}
}
func TestManagerList(t *testing.T) {
// Create temp directory for test
tmpDir, err := os.MkdirTemp("", "crew-test-list-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tmpDir) }()
// Create a mock rig
rigPath := filepath.Join(tmpDir, "test-rig")
if err := os.MkdirAll(rigPath, 0755); err != nil {
t.Fatalf("failed to create rig dir: %v", err)
}
g := git.NewGit(rigPath)
// Create a bare repo for cloning
bareRepoPath := filepath.Join(tmpDir, "bare-repo.git")
if err := runCmd("git", "init", "--bare", bareRepoPath); err != nil {
t.Fatalf("failed to create bare repo: %v", err)
}
r := &rig.Rig{
Name: "test-rig",
Path: rigPath,
GitURL: bareRepoPath,
}
mgr := NewManager(r, g)
// Initially empty
workers, err := mgr.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(workers) != 0 {
t.Errorf("expected 0 workers, got %d", len(workers))
}
// Add some workers
_, err = mgr.Add("alice", false)
if err != nil {
t.Fatalf("Add alice failed: %v", err)
}
_, err = mgr.Add("bob", false)
if err != nil {
t.Fatalf("Add bob failed: %v", err)
}
workers, err = mgr.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(workers) != 2 {
t.Errorf("expected 2 workers, got %d", len(workers))
}
}
func TestManagerRemove(t *testing.T) {
// Create temp directory for test
tmpDir, err := os.MkdirTemp("", "crew-test-remove-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tmpDir) }()
// Create a mock rig
rigPath := filepath.Join(tmpDir, "test-rig")
if err := os.MkdirAll(rigPath, 0755); err != nil {
t.Fatalf("failed to create rig dir: %v", err)
}
g := git.NewGit(rigPath)
// Create a bare repo for cloning
bareRepoPath := filepath.Join(tmpDir, "bare-repo.git")
if err := runCmd("git", "init", "--bare", bareRepoPath); err != nil {
t.Fatalf("failed to create bare repo: %v", err)
}
r := &rig.Rig{
Name: "test-rig",
Path: rigPath,
GitURL: bareRepoPath,
}
mgr := NewManager(r, g)
// Add a worker
_, err = mgr.Add("charlie", false)
if err != nil {
t.Fatalf("Add failed: %v", err)
}
// Remove it (with force since CLAUDE.md is uncommitted)
err = mgr.Remove("charlie", true)
if err != nil {
t.Fatalf("Remove failed: %v", err)
}
// Verify it's gone
_, err = mgr.Get("charlie")
if err != ErrCrewNotFound {
t.Errorf("expected ErrCrewNotFound, got %v", err)
}
// Remove non-existent
err = mgr.Remove("nonexistent", false)
if err != ErrCrewNotFound {
t.Errorf("expected ErrCrewNotFound, got %v", err)
}
}
func TestManagerGetWithStaleStateName(t *testing.T) {
// Regression test: state.json with wrong name should not affect Get() result
// See: gt-h1w - gt crew list shows wrong names
tmpDir, err := os.MkdirTemp("", "crew-test-stale-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tmpDir) }()
rigPath := filepath.Join(tmpDir, "test-rig")
if err := os.MkdirAll(rigPath, 0755); err != nil {
t.Fatalf("failed to create rig dir: %v", err)
}
r := &rig.Rig{
Name: "test-rig",
Path: rigPath,
}
mgr := NewManager(r, git.NewGit(rigPath))
// Manually create a crew directory with wrong name in state.json
crewDir := filepath.Join(rigPath, "crew", "alice")
if err := os.MkdirAll(crewDir, 0755); err != nil {
t.Fatalf("failed to create crew dir: %v", err)
}
// Write state.json with wrong name (simulates stale/copied state)
stateFile := filepath.Join(crewDir, "state.json")
staleState := `{"name": "bob", "rig": "test-rig", "clone_path": "/wrong/path"}`
if err := os.WriteFile(stateFile, []byte(staleState), 0644); err != nil {
t.Fatalf("failed to write state file: %v", err)
}
// Get should return correct name (alice) not stale name (bob)
worker, err := mgr.Get("alice")
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if worker.Name != "alice" {
t.Errorf("expected name 'alice', got '%s' (stale state.json not overridden)", worker.Name)
}
expectedPath := filepath.Join(rigPath, "crew", "alice")
if worker.ClonePath != expectedPath {
t.Errorf("expected clone_path '%s', got '%s'", expectedPath, worker.ClonePath)
}
}
// Helper to run commands
func runCmd(name string, args ...string) error {
cmd := exec.Command(name, args...)
return cmd.Run()
}