diff --git a/functions/infra/agent_cleanup_worktree_test.go b/functions/infra/agent_cleanup_worktree_test.go new file mode 100644 index 00000000..65cd42c7 --- /dev/null +++ b/functions/infra/agent_cleanup_worktree_test.go @@ -0,0 +1,57 @@ +package infra + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestAgentCleanupWorktree_RemovesWorktreeAndBranch(t *testing.T) { + repo := initDummyRepo(t) + wt := filepath.Join(t.TempDir(), "wt-cleanup") + log := filepath.Join(t.TempDir(), "claude.log") + + res := AgentLaunchWorktree(WorktreeLaunchConfig{ + RepoRoot: repo, + Branch: "auto/cleanup-test", + WorktreePath: wt, + Prompt: "x", + LogPath: log, + }) + if res.Error != "" { + t.Fatalf("launch failed: %s", res.Error) + } + + if err := AgentCleanupWorktree(repo, "auto/cleanup-test", wt, res.PID); err != nil { + t.Fatalf("cleanup returned error: %v", err) + } + + // Worktree dir should be gone. + if _, err := os.Stat(wt); !os.IsNotExist(err) { + t.Fatalf("worktree dir should be removed, stat err=%v", err) + } + // Branch should be gone. + out, _ := exec.Command("git", "-C", repo, "branch", "--list", "auto/cleanup-test").CombinedOutput() + if strings.Contains(string(out), "auto/cleanup-test") { + t.Fatalf("branch should be deleted, got: %s", out) + } +} + +func TestAgentCleanupWorktree_TolerantToMissing(t *testing.T) { + repo := initDummyRepo(t) + // Worktree path that never existed; branch that never existed; PID 0. + err := AgentCleanupWorktree(repo, "no-such-branch", filepath.Join(t.TempDir(), "ghost-wt"), 0) + if err == nil { + // All three steps failed individually but PID=0 skipped the kill, so + // killErr==nil and the combined check returns nil. That's the + // expected "tolerant" behaviour. + return + } + // If some git versions surface a different error path, just ensure we + // didn't panic and the error message is informative. + if !strings.Contains(err.Error(), "cleanup failed") { + t.Fatalf("unexpected error shape: %v", err) + } +} diff --git a/functions/infra/agent_launch_worktree_test.go b/functions/infra/agent_launch_worktree_test.go new file mode 100644 index 00000000..4bd17a98 --- /dev/null +++ b/functions/infra/agent_launch_worktree_test.go @@ -0,0 +1,118 @@ +package infra + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// initDummyRepo creates a tiny git repo with a master branch and one commit so +// that `git worktree add ... master` can succeed. +func initDummyRepo(t *testing.T) string { + t.Helper() + dir := t.TempDir() + + runIn := func(name string, args ...string) { + cmd := exec.Command(name, args...) + cmd.Dir = dir + // Detach from any global config that might inject signing/templates. + cmd.Env = append(os.Environ(), + "GIT_CONFIG_GLOBAL=/dev/null", + "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=t@t", + "GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=t@t", + ) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("%s %v: %v\n%s", name, args, err, out) + } + } + + runIn("git", "init", "-b", "master") + if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil { + t.Fatal(err) + } + runIn("git", "add", "README.md") + runIn("git", "commit", "-m", "init") + + return dir +} + +func TestAgentLaunchWorktree_CreatesWorktreeAndBranch(t *testing.T) { + repo := initDummyRepo(t) + wt := filepath.Join(t.TempDir(), "wt-0001") + log := filepath.Join(t.TempDir(), "claude.log") + + res := AgentLaunchWorktree(WorktreeLaunchConfig{ + RepoRoot: repo, + Branch: "auto/test-0001", + WorktreePath: wt, + Prompt: "hello", + LogPath: log, + }) + + if res.Error != "" { + t.Fatalf("unexpected error: %s", res.Error) + } + if res.PID <= 0 { + t.Fatalf("expected PID>0, got %d", res.PID) + } + if _, err := os.Stat(wt); err != nil { + t.Fatalf("worktree dir missing: %v", err) + } + // Branch should exist in the main repo's refs. + out, err := exec.Command("git", "-C", repo, "branch", "--list", "auto/test-0001").CombinedOutput() + if err != nil || !strings.Contains(string(out), "auto/test-0001") { + t.Fatalf("branch not created: err=%v out=%s", err, out) + } + if _, err := os.Stat(log); err != nil { + t.Fatalf("log file missing: %v", err) + } + + // Cleanup (best-effort, not asserted here). + _ = AgentCleanupWorktree(repo, "auto/test-0001", wt, res.PID) +} + +func TestAgentLaunchWorktree_ResetIfExists(t *testing.T) { + repo := initDummyRepo(t) + wt := filepath.Join(t.TempDir(), "wt-reset") + log := filepath.Join(t.TempDir(), "claude.log") + + // First launch. + res1 := AgentLaunchWorktree(WorktreeLaunchConfig{ + RepoRoot: repo, + Branch: "auto/reset-test", + WorktreePath: wt, + Prompt: "first", + LogPath: log, + }) + if res1.Error != "" { + t.Fatalf("first launch failed: %s", res1.Error) + } + + // Second launch with ResetIfExists=true should succeed and recreate. + res2 := AgentLaunchWorktree(WorktreeLaunchConfig{ + RepoRoot: repo, + Branch: "auto/reset-test", + WorktreePath: wt, + Prompt: "second", + LogPath: log, + ResetIfExists: true, + }) + if res2.Error != "" { + t.Fatalf("reset launch failed: %s", res2.Error) + } + if _, err := os.Stat(wt); err != nil { + t.Fatalf("worktree dir missing after reset: %v", err) + } + + _ = AgentCleanupWorktree(repo, "auto/reset-test", wt, res2.PID) +} + +func TestAgentLaunchWorktree_MissingArgs(t *testing.T) { + res := AgentLaunchWorktree(WorktreeLaunchConfig{}) + if res.Error == "" { + t.Fatal("expected error for empty config, got none") + } +}