1package subagents
2
3import (
4 "context"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "strings"
9 "testing"
10)
11
12func TestGitWorktreeWorkspaceIsolatesAndCapturesPatch(t *testing.T) {
13 if _, err := exec.LookPath("git"); err != nil {
14 t.Skip("git is required")
15 }
16 repo := initTestRepo(t)
17
18 state := t.TempDir()
19 handle, err := PrepareWorkspace(context.Background(), WorkspaceRequest{
20 Mode: WorkspaceWorktree, RepositoryRoot: repo, StateDir: state, AgentID: "worker-1", Base: "HEAD", Capture: CapturePatch,
21 })
22 if err != nil {
23 t.Fatal(err)
24 }
25 if handle.Mode() != WorkspaceWorktree || handle.Dir() == repo {
26 t.Fatalf("handle = mode %s dir %q repo %q", handle.Mode(), handle.Dir(), repo)
27 }
28 if err := os.WriteFile(filepath.Join(handle.Dir(), "README.md"), []byte("after\n"), 0o600); err != nil {
29 t.Fatal(err)
30 }
31 before, err := os.ReadFile(filepath.Join(repo, "README.md"))
32 if err != nil {
33 t.Fatal(err)
34 }
35 if string(before) != "before\n" {
36 t.Fatalf("host checkout changed: %q", before)
37 }
38 capture, err := handle.Capture(context.Background())
39 if err != nil {
40 t.Fatal(err)
41 }
42 if len(capture.Patch) == 0 || !strings.Contains(string(capture.Patch), "after") {
43 t.Fatalf("patch = %q", capture.Patch)
44 }
45 if len(capture.ChangedFiles) != 1 || capture.ChangedFiles[0] != "README.md" {
46 t.Fatalf("changed files = %#v", capture.ChangedFiles)
47 }
48 if err := handle.Cleanup(context.Background()); err != nil {
49 t.Fatal(err)
50 }
51 if _, err := os.Stat(handle.Dir()); !os.IsNotExist(err) {
52 t.Fatalf("worktree still exists: %v", err)
53 }
54}
55
56func TestGitWorktreeWorkspaceReusesDetachedCheckout(t *testing.T) {
57 if _, err := exec.LookPath("git"); err != nil {
58 t.Skip("git is required")
59 }
60 repo := initTestRepo(t)
61
62 state := t.TempDir()
63 first, err := PrepareWorkspace(context.Background(), WorkspaceRequest{
64 Mode: WorkspaceWorktree, RepositoryRoot: repo, StateDir: state, AgentID: "worker-1", Base: "HEAD", Capture: CapturePatch,
65 })
66 if err != nil {
67 t.Fatal(err)
68 }
69 if err := os.WriteFile(filepath.Join(first.Dir(), "draft.txt"), []byte("keep me\n"), 0o600); err != nil {
70 t.Fatal(err)
71 }
72 resumed, err := PrepareWorkspace(context.Background(), WorkspaceRequest{
73 Mode: WorkspaceWorktree, RepositoryRoot: repo, StateDir: state, AgentID: "worker-1", Base: "HEAD", Capture: CapturePatch,
74 ExistingPath: first.Dir(),
75 })
76 if err != nil {
77 t.Fatal(err)
78 }
79 if resumed.Dir() != first.Dir() {
80 t.Fatalf("resume dir = %q, want %q", resumed.Dir(), first.Dir())
81 }
82 if _, err := os.Stat(filepath.Join(resumed.Dir(), "draft.txt")); err != nil {
83 t.Fatalf("resumed worktree lost uncommitted file: %v", err)
84 }
85 if err := resumed.Cleanup(context.Background()); err != nil {
86 t.Fatal(err)
87 }
88}
89
90func TestGitWorktreeWorkspaceCaptureIncludesUntrackedFile(t *testing.T) {
91 if _, err := exec.LookPath("git"); err != nil {
92 t.Skip("git is required")
93 }
94 repo := initTestRepo(t)
95 state := t.TempDir()
96 handle, err := PrepareWorkspace(context.Background(), WorkspaceRequest{
97 Mode: WorkspaceWorktree, RepositoryRoot: repo, StateDir: state, Base: "HEAD", Capture: CapturePatch,
98 })
99 if err != nil {
100 t.Fatal(err)
101 }
102 defer func() { _ = handle.Cleanup(context.Background()) }()
103
104 name := "new file – 日本語.txt"
105 if err := os.WriteFile(filepath.Join(handle.Dir(), name), []byte("created\n"), 0o600); err != nil {
106 t.Fatal(err)
107 }
108 capture, err := handle.Capture(context.Background())
109 if err != nil {
110 t.Fatal(err)
111 }
112 if len(capture.ChangedFiles) != 1 || capture.ChangedFiles[0] != name {
113 t.Fatalf("changed files = %#v, want [%q]", capture.ChangedFiles, name)
114 }
115 if !strings.Contains(string(capture.Patch), "created") {
116 t.Fatalf("patch = %q", capture.Patch)
117 }
118}
119
120func TestChangedFilesFromStatus(t *testing.T) {
121 status := []byte(" M path with spaces.txt\x00R renamed – 新.txt\x00original name – старый.txt\x00?? untracked – 文件.txt\x00")
122 got := changedFilesFromStatus(status)
123 want := []string{"path with spaces.txt", "renamed – 新.txt", "untracked – 文件.txt"}
124 if len(got) != len(want) {
125 t.Fatalf("changed files = %#v, want %#v", got, want)
126 }
127 for i := range want {
128 if got[i] != want[i] {
129 t.Errorf("changed files[%d] = %q, want %q", i, got[i], want[i])
130 }
131 }
132}
133
134func TestSharedWorkspaceUsesRepositoryRoot(t *testing.T) {
135 root := t.TempDir()
136 handle, err := PrepareWorkspace(context.Background(), WorkspaceRequest{Mode: WorkspaceShared, RepositoryRoot: root})
137 if err != nil {
138 t.Fatal(err)
139 }
140 if handle.Dir() != root || handle.RepositoryRoot() != root || handle.Mode() != WorkspaceShared {
141 t.Fatalf("shared handle = %#v", handle)
142 }
143 if err := handle.Cleanup(context.Background()); err != nil {
144 t.Fatal(err)
145 }
146}
147
148func initTestRepo(t *testing.T) string {
149 t.Helper()
150 home := t.TempDir()
151 t.Setenv("HOME", home)
152 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, "xdg-config"))
153 t.Setenv("GIT_CONFIG_GLOBAL", os.DevNull)
154 t.Setenv("GIT_CONFIG_SYSTEM", os.DevNull)
155 unsetenvForTest(t, "GIT_DIR")
156 unsetenvForTest(t, "GIT_WORK_TREE")
157
158 repo := t.TempDir()
159 runGit(t, repo, "init")
160 runGit(t, repo, "config", "user.email", "test@example.invalid")
161 runGit(t, repo, "config", "user.name", "Test User")
162 runGit(t, repo, "config", "commit.gpgsign", "false")
163 if err := os.WriteFile(filepath.Join(repo, "README.md"), []byte("before\n"), 0o600); err != nil {
164 t.Fatal(err)
165 }
166 runGit(t, repo, "add", "README.md")
167 runGit(t, repo, "commit", "-m", "initial")
168 return repo
169}
170
171func unsetenvForTest(t *testing.T, name string) {
172 t.Helper()
173 value, ok := os.LookupEnv(name)
174 if err := os.Unsetenv(name); err != nil {
175 t.Fatal(err)
176 }
177 t.Cleanup(func() {
178 if ok {
179 _ = os.Setenv(name, value)
180 } else {
181 _ = os.Unsetenv(name)
182 }
183 })
184}
185
186func runGit(t *testing.T, dir string, args ...string) {
187 t.Helper()
188 cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
189 if output, err := cmd.CombinedOutput(); err != nil {
190 t.Fatalf("git %v: %v\n%s", args, err, output)
191 }
192}