package agent import ( "os" "path/filepath" "testing" ) func TestSanitizeBranch(t *testing.T) { tests := []struct { in, want string }{ {"benvin/agentws", "benvin-agentws"}, {"main", "main"}, {" feature/x ", "feature-x"}, {"a/b/c", "a-b-c"}, {"ns:thing", "ns-thing"}, {"with space", "with-space"}, } for _, tt := range tests { if got := SanitizeBranch(tt.in); got != tt.want { t.Errorf("SanitizeBranch(%q) = %q, want %q", tt.in, got, tt.want) } } } func TestWorktreeDirName(t *testing.T) { if got := WorktreeDirName("argocd-apps", "benvin/foo"); got != "argocd-apps__benvin-foo" { t.Errorf("WorktreeDirName = %q", got) } } func TestParseWorktreeList(t *testing.T) { out := `worktree /home/ben/src/prodenv/repo HEAD 1111111111111111111111111111111111111111 branch refs/heads/main worktree /home/ben/.cache/agentws/repo__benvin-foo HEAD 2222222222222222222222222222222222222222 branch refs/heads/benvin/foo worktree /home/ben/.cache/agentws/repo__detached HEAD 3333333333333333333333333333333333333333 detached ` wts := ParseWorktreeList(out) if len(wts) != 3 { t.Fatalf("got %d worktrees, want 3: %+v", len(wts), wts) } if wts[0].Branch != "main" || wts[0].Path != "/home/ben/src/prodenv/repo" { t.Errorf("wt[0] = %+v", wts[0]) } if wts[1].Branch != "benvin/foo" { t.Errorf("wt[1].Branch = %q, want benvin/foo", wts[1].Branch) } if !wts[2].Detached || wts[2].Branch != "" { t.Errorf("wt[2] = %+v, want detached with empty branch", wts[2]) } } // gitSeed sets a repo-local identity so commits work without global config. func gitIdentity(t *testing.T, dir string) { t.Helper() if err := GitConfigSet(dir, false, "user.email", "test@example.com"); err != nil { t.Fatalf("set user.email: %v", err) } if err := GitConfigSet(dir, false, "user.name", "Test"); err != nil { t.Fatalf("set user.name: %v", err) } } // newTempRepos builds a bare "origin" with one commit on main and clones it into // srcDir (so refs/remotes/origin/HEAD is set), returning the source checkout. func newTempRepos(t *testing.T) string { t.Helper() root := t.TempDir() bare := filepath.Join(root, "origin.git") if _, err := runGit(root, "init", "--bare", "-b", "main", bare); err != nil { t.Fatalf("init bare: %v", err) } seed := filepath.Join(root, "seed") if _, err := runGit(root, "init", "-b", "main", seed); err != nil { t.Fatalf("init seed: %v", err) } gitIdentity(t, seed) if err := os.WriteFile(filepath.Join(seed, "README.md"), []byte("hi\n"), 0o644); err != nil { t.Fatal(err) } if _, err := runGit(seed, "add", "."); err != nil { t.Fatalf("add: %v", err) } if _, err := runGit(seed, "commit", "-m", "init"); err != nil { t.Fatalf("commit: %v", err) } if _, err := runGit(seed, "remote", "add", "origin", bare); err != nil { t.Fatalf("remote add: %v", err) } if _, err := runGit(seed, "push", "-u", "origin", "main"); err != nil { t.Fatalf("push: %v", err) } srcDir := filepath.Join(root, "src") if err := GitClone(bare, srcDir); err != nil { t.Fatalf("clone: %v", err) } gitIdentity(t, srcDir) return srcDir } func TestGitWorktreeLifecycle(t *testing.T) { srcDir := newTempRepos(t) def, err := GitRemoteDefaultBranch(srcDir, "origin") if err != nil { t.Fatalf("GitRemoteDefaultBranch: %v", err) } if def != "main" { t.Errorf("default branch = %q, want main", def) } if err := GitFetch(srcDir, "origin"); err != nil { t.Fatalf("GitFetch: %v", err) } wtPath := filepath.Join(t.TempDir(), "repo__benvin-x") if GitBranchExists(srcDir, "benvin/x") { t.Fatal("branch benvin/x should not exist yet") } if err := GitWorktreeAdd(srcDir, wtPath, "benvin/x", "origin/main"); err != nil { t.Fatalf("GitWorktreeAdd: %v", err) } if !GitBranchExists(srcDir, "benvin/x") { t.Error("branch benvin/x should exist after worktree add") } if br, err := GitCurrentBranch(wtPath); err != nil || br != "benvin/x" { t.Errorf("GitCurrentBranch = %q, %v; want benvin/x", br, err) } src2, err := SourceRepoDir(wtPath) if err != nil { t.Fatalf("SourceRepoDir: %v", err) } if resolve(t, src2) != resolve(t, srcDir) { t.Errorf("SourceRepoDir = %q, want %q", src2, srcDir) } wts, err := GitWorktreeList(srcDir) if err != nil { t.Fatalf("GitWorktreeList: %v", err) } found := false for _, w := range wts { if resolve(t, w.Path) == resolve(t, wtPath) && w.Branch == "benvin/x" { found = true } } if !found { t.Errorf("worktree %s not found in list: %+v", wtPath, wts) } // Per-worktree config must not leak into the shared checkout. if err := GitConfigSet(srcDir, false, "extensions.worktreeConfig", "true"); err != nil { t.Fatalf("enable worktreeConfig: %v", err) } if err := GitConfigSet(wtPath, true, "user.name", "unkin-agent"); err != nil { t.Fatalf("set worktree user.name: %v", err) } if name, _ := runGit(srcDir, "config", "user.name"); name == "unkin-agent" { t.Error("shared checkout user.name was polluted by worktree config") } if err := GitWorktreeRemove(srcDir, wtPath, true); err != nil { t.Fatalf("GitWorktreeRemove: %v", err) } if err := GitDeleteBranch(srcDir, "benvin/x", true); err != nil { t.Fatalf("GitDeleteBranch: %v", err) } if GitBranchExists(srcDir, "benvin/x") { t.Error("branch benvin/x should be gone after delete") } if err := GitWorktreePrune(srcDir); err != nil { t.Fatalf("GitWorktreePrune: %v", err) } } // resolve canonicalizes a path (temp dirs may live behind symlinks like /var). func resolve(t *testing.T, p string) string { t.Helper() r, err := filepath.EvalSymlinks(p) if err != nil { return p } return r }