返回 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 start := time.Now()
157 if got := workspaceGitBranchForMeta("/tmp/reasonix-cold-probe"); got != "" {
158 t.Fatalf("cold branch = %q, want empty while async refresh runs", got)
159 }
160 if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
161 t.Fatalf("cold metadata branch probe blocked for %s", elapsed)
162 }
163 select {
164 case <-started:
165 case <-time.After(time.Second):
166 t.Fatal("background branch refresh did not start")
167 }
168
169 releaseProbe()
170 eventuallyBranchForMeta(t, "/tmp/reasonix-cold-probe", "feature/async")
171 }
172
173 func TestWorkspaceGitBranchForMetaReturnsStaleDuringRefresh(t *testing.T) {
174 resetWorkspaceGitBranchMetaCacheForTest(t)
175 workspaceGitBranchCache.Lock()
176 workspaceGitBranchCache.entries[filepath.Clean("/tmp/reasonix-stale-probe")] = workspaceGitBranchCacheEntry{
177 branch: "feature/stale",
178 expires: time.Now().Add(-time.Second),
179 }
180 workspaceGitBranchCache.Unlock()
181
182 origProbe := workspaceGitBranchForMetaProbe
183 started := make(chan struct{})
184 release := make(chan struct{})
185 var releaseOnce sync.Once
186 releaseProbe := func() { releaseOnce.Do(func() { close(release) }) }
187 workspaceGitBranchForMetaProbe = func(string) string {
188 close(started)
189 <-release
190 return "feature/fresh"
191 }
192 defer func() {
193 releaseProbe()
194 workspaceGitBranchForMetaProbe = origProbe
195 }()
196
197 start := time.Now()
198 if got := workspaceGitBranchForMeta("/tmp/reasonix-stale-probe"); got != "feature/stale" {
199 t.Fatalf("stale branch = %q, want feature/stale", got)
200 }
201 if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
202 t.Fatalf("stale metadata branch probe blocked for %s", elapsed)
203 }
204 select {
205 case <-started:
206 case <-time.After(time.Second):
207 t.Fatal("background branch refresh did not start")
208 }
209
210 releaseProbe()
211 eventuallyBranchForMeta(t, "/tmp/reasonix-stale-probe", "feature/fresh")
212 }
213
214 func resetWorkspaceGitBranchMetaCacheForTest(t *testing.T) {
215 t.Helper()
216 workspaceGitBranchCache.Lock()
217 workspaceGitBranchCache.entries = map[string]workspaceGitBranchCacheEntry{}
218 workspaceGitBranchCache.Unlock()
219 }
220
221 func eventuallyBranchForMeta(t *testing.T, base, want string) {
222 t.Helper()
223 deadline := time.Now().Add(time.Second)
224 for time.Now().Before(deadline) {
225 if got := workspaceGitBranchForMeta(base); got == want {
226 return
227 }
228 time.Sleep(10 * time.Millisecond)
229 }
230 t.Fatalf("branch did not refresh to %q", want)
231 }
232
232 lines GO