返回 DeepSeek-Reasonix
workspace_changes_test.go
根目录 / desktop / workspace_changes_test.go
1 package main
2
3 import (
4 "os"
5 "os/exec"
6 "path/filepath"
7 "runtime"
8 "slices"
9 "sync"
10 "testing"
11 "time"
12 )
13
14 // The probe must not spawn a background daemon that opens a console of its own
15 // (#3906). Asserted as properties rather than a byte-exact argument list: the
16 // full invocation baseline is owned and pinned by internal/gitcmd, and pinning
17 // it a second time here only guaranteed this test would break whenever the
18 // baseline gained an entry.
19 func TestWorkspaceGitDisablesDaemonSpawns(t *testing.T) {
20 cmd := workspaceGit("-C", "repo", "status", "--porcelain=v1")
21 for _, want := range []string{"core.fsmonitor=false", "maintenance.auto=false"} {
22 if !hasGitConfigArg(cmd.Args, want) {
23 t.Fatalf("args = %v, want -c %s", cmd.Args, want)
24 }
25 }
26 if tail := cmd.Args[len(cmd.Args)-4:]; !slices.Equal(tail, []string{"-C", "repo", "status", "--porcelain=v1"}) {
27 t.Fatalf("args = %v, want the caller's arguments last", cmd.Args)
28 }
29 if runtime.GOOS == "windows" && cmd.SysProcAttr == nil {
30 t.Fatal("workspaceGit must hide the console window on Windows")
31 }
32 }
33
34 func hasGitConfigArg(args []string, want string) bool {
35 for i := 0; i+1 < len(args); i++ {
36 if args[i] == "-c" && args[i+1] == want {
37 return true
38 }
39 }
40 return false
41 }
42
43 func TestWorkspaceGitBranch(t *testing.T) {
44 if _, err := exec.LookPath("git"); err != nil {
45 t.Skip("git not installed")
46 }
47 orig, err := os.Getwd()
48 if err != nil {
49 t.Fatal(err)
50 }
51 defer func() {
52 if err := os.Chdir(orig); err != nil {
53 t.Fatal(err)
54 }
55 }()
56
57 repo := t.TempDir()
58 if err := os.Chdir(repo); err != nil {
59 t.Fatal(err)
60 }
61 runGit(t, "init")
62 runGit(t, "checkout", "-b", "feature/status")
63
64 if got := workspaceGitBranch(repo); got != "feature/status" {
65 t.Fatalf("branch = %q, want feature/status", got)
66 }
67 }
68
69 func TestWorkspaceGitBranchReflectsImmediateCheckout(t *testing.T) {
70 if _, err := exec.LookPath("git"); err != nil {
71 t.Skip("git not installed")
72 }
73 orig, err := os.Getwd()
74 if err != nil {
75 t.Fatal(err)
76 }
77 defer func() {
78 if err := os.Chdir(orig); err != nil {
79 t.Fatal(err)
80 }
81 }()
82
83 repo := t.TempDir()
84 if err := os.Chdir(repo); err != nil {
85 t.Fatal(err)
86 }
87 runGit(t, "init")
88 runGit(t, "checkout", "-b", "feature/one")
89
90 if got := workspaceGitBranch(repo); got != "feature/one" {
91 t.Fatalf("branch before checkout = %q, want feature/one", got)
92 }
93 runGit(t, "checkout", "-b", "feature/two")
94 if got := workspaceGitBranch(repo); got != "feature/two" {
95 t.Fatalf("branch after checkout = %q, want feature/two", got)
96 }
97 }
98
99 func TestWorkspaceGitBranchDetachedHead(t *testing.T) {
100 if _, err := exec.LookPath("git"); err != nil {
101 t.Skip("git not installed")
102 }
103 orig, err := os.Getwd()
104 if err != nil {
105 t.Fatal(err)
106 }
107 defer func() {
108 if err := os.Chdir(orig); err != nil {
109 t.Fatal(err)
110 }
111 }()
112
113 repo := t.TempDir()
114 if err := os.Chdir(repo); err != nil {
115 t.Fatal(err)
116 }
117 runGit(t, "init")
118 runGit(t, "config", "user.email", "test@example.com")
119 runGit(t, "config", "user.name", "Test User")
120 if err := os.WriteFile("tracked.txt", []byte("v1\n"), 0o644); err != nil {
121 t.Fatal(err)
122 }
123 runGit(t, "add", "tracked.txt")
124 runGit(t, "commit", "-m", "init")
125 short := gitOutput(t, "rev-parse", "--short", "HEAD")
126 runGit(t, "checkout", "--detach", "HEAD")
127
128 if got := workspaceGitBranch(repo); got != "@"+short {
129 t.Fatalf("branch = %q, want @%s", got, short)
130 }
131 }
132
133 func TestWorkspaceGitBranchNonGitDirectory(t *testing.T) {
134 if got := workspaceGitBranch(t.TempDir()); got != "" {
135 t.Fatalf("branch = %q, want empty", got)
136 }
137 }
138
139 func TestWorkspaceGitBranchForMetaDoesNotBlockOnColdProbe(t *testing.T) {
140 resetWorkspaceGitBranchMetaCacheForTest(t)
141 origProbe := workspaceGitBranchForMetaProbe
142 started := make(chan struct{})
143 release := make(chan struct{})
144 var releaseOnce sync.Once
145 releaseProbe := func() { releaseOnce.Do(func() { close(release) }) }
146 workspaceGitBranchForMetaProbe = func(string) string {
147 close(started)
148 <-release
149 return "feature/async"
150 }
151 defer func() {
152 releaseProbe()
153 workspaceGitBranchForMetaProbe = origProbe
154 }()
155
156 if got := workspaceGitBranchForMeta("/tmp/reasonix-cold-probe"); got != "" {
157 t.Fatalf("cold branch = %q, want empty while async refresh runs", got)
158 }
159 // The probe is still blocked here, so returning is the deterministic proof
160 // that the metadata read did not wait for its refresh.
161 select {
162 case <-started:
163 case <-time.After(time.Second):
164 t.Fatal("background branch refresh did not start")
165 }
166
167 releaseProbe()
168 eventuallyBranchForMeta(t, "/tmp/reasonix-cold-probe", "feature/async")
169 }
170
171 func TestWorkspaceGitBranchForMetaReturnsStaleDuringRefresh(t *testing.T) {
172 resetWorkspaceGitBranchMetaCacheForTest(t)
173 workspaceGitBranchCache.Lock()
174 workspaceGitBranchCache.entries[filepath.Clean("/tmp/reasonix-stale-probe")] = workspaceGitBranchCacheEntry{
175 branch: "feature/stale",
176 expires: time.Now().Add(-time.Second),
177 }
178 workspaceGitBranchCache.Unlock()
179
180 origProbe := workspaceGitBranchForMetaProbe
181 started := make(chan struct{})
182 release := make(chan struct{})
183 var releaseOnce sync.Once
184 releaseProbe := func() { releaseOnce.Do(func() { close(release) }) }
185 workspaceGitBranchForMetaProbe = func(string) string {
186 close(started)
187 <-release
188 return "feature/fresh"
189 }
190 defer func() {
191 releaseProbe()
192 workspaceGitBranchForMetaProbe = origProbe
193 }()
194
195 if got := workspaceGitBranchForMeta("/tmp/reasonix-stale-probe"); got != "feature/stale" {
196 t.Fatalf("stale branch = %q, want feature/stale", got)
197 }
198 // The probe is still blocked here, so returning the stale value proves that
199 // the read did not wait for its refresh.
200 select {
201 case <-started:
202 case <-time.After(time.Second):
203 t.Fatal("background branch refresh did not start")
204 }
205
206 releaseProbe()
207 eventuallyBranchForMeta(t, "/tmp/reasonix-stale-probe", "feature/fresh")
208 }
209
210 func resetWorkspaceGitBranchMetaCacheForTest(t *testing.T) {
211 t.Helper()
212 workspaceGitBranchCache.Lock()
213 workspaceGitBranchCache.entries = map[string]workspaceGitBranchCacheEntry{}
214 workspaceGitBranchCache.Unlock()
215 }
216
217 func eventuallyBranchForMeta(t *testing.T, base, want string) {
218 t.Helper()
219 deadline := time.Now().Add(time.Second)
220 for time.Now().Before(deadline) {
221 if got := workspaceGitBranchForMeta(base); got == want {
222 return
223 }
224 time.Sleep(10 * time.Millisecond)
225 }
226 t.Fatalf("branch did not refresh to %q", want)
227 }
228
228 lines GO