返回 DeepSeek-Reasonix
workspace_watch_windows_test.go
根目录 / desktop / workspace_watch_windows_test.go
1 //go:build windows
2
3 package main
4
5 import (
6 "context"
7 "fmt"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "testing"
12 "time"
13
14 "reasonix/internal/gitcmd"
15 )
16
17 const workspaceGitCreateNoWindow = 0x08000000
18
19 func TestWorkspaceGitProbesHideConsoleWindowOnWindows(t *testing.T) {
20 // Both startup probes use the same gitcmd.Command construction path.
21 // Assert HideWindow + CREATE_NO_WINDOW for each rev-parse flag.
22 for _, flag := range []string{"--git-dir", "--git-common-dir"} {
23 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
24 cmd := gitcmd.Command(ctx, t.TempDir(), "rev-parse", flag)
25 cancel()
26 if cmd.SysProcAttr == nil {
27 t.Fatalf("%s: SysProcAttr is nil", flag)
28 }
29 if !cmd.SysProcAttr.HideWindow {
30 t.Fatalf("%s: HideWindow is false", flag)
31 }
32 if cmd.SysProcAttr.CreationFlags&workspaceGitCreateNoWindow == 0 {
33 t.Fatalf("%s: CREATE_NO_WINDOW not set; CreationFlags=%#x", flag, cmd.SysProcAttr.CreationFlags)
34 }
35 }
36 }
37
38 func TestWorkspaceWatcherDoesNotLockNestedParentOnWindows(t *testing.T) {
39 root := t.TempDir()
40 from := filepath.Join(root, "parent")
41 if err := os.MkdirAll(filepath.Join(from, "child"), 0o700); err != nil {
42 t.Fatal(err)
43 }
44 app := &App{tabs: map[string]*WorkspaceTab{"a": {ID: "a", WorkspaceRoot: root}}}
45 app.workspaceHub = newWorkspaceChangeHub(app)
46 t.Cleanup(func() { app.workspaceHub.close() })
47 if state := app.WorkspaceRevisionForTab("a").WatchState; state == "unavailable" {
48 t.Fatalf("watcher unavailable: %s", state)
49 }
50 before := app.WorkspaceRevisionForTab("a").Revisions.Content
51
52 to := filepath.Join(root, "renamed")
53 if err := os.Rename(from, to); err != nil {
54 t.Fatalf("rename parent while workspace watcher is active: %v", err)
55 }
56 waitForWorkspaceContentRevision(t, app, before)
57 before = waitForWorkspaceRevisionToSettle(t, app)
58 if err := os.WriteFile(filepath.Join(to, "child", "after.txt"), []byte("after"), 0o600); err != nil {
59 t.Fatal(err)
60 }
61 waitForWorkspaceContentRevision(t, app, before)
62 before = waitForWorkspaceRevisionToSettle(t, app)
63 if err := os.RemoveAll(to); err != nil {
64 t.Fatalf("delete renamed parent while workspace watcher is active: %v", err)
65 }
66 waitForWorkspaceContentRevision(t, app, before)
67 }
68
69 func TestWorkspaceWatcherUsesOneRecursiveWorkspaceHandleOnWindows(t *testing.T) {
70 root := t.TempDir()
71 if err := os.MkdirAll(filepath.Join(root, "one", "two", "three"), 0o700); err != nil {
72 t.Fatal(err)
73 }
74 app := &App{tabs: map[string]*WorkspaceTab{"a": {ID: "a", WorkspaceRoot: root}}}
75 app.workspaceHub = newWorkspaceChangeHub(app)
76 t.Cleanup(func() { app.workspaceHub.close() })
77 app.WorkspaceRevisionForTab("a")
78
79 key := canonicalWorkspaceRoot(root)
80 app.workspaceHub.mu.Lock()
81 r := app.workspaceHub.roots[key]
82 dirs := 0
83 if r != nil {
84 dirs = r.dirs
85 }
86 app.workspaceHub.mu.Unlock()
87 if dirs != 1 {
88 t.Fatalf("workspace handles = %d, want 1 recursive root handle", dirs)
89 }
90 }
91
92 func TestWindowsWorkspaceWatcherPreservesUnicodeEventPath(t *testing.T) {
93 root := t.TempDir()
94 deep := filepath.Join(root, "one", "two")
95 if err := os.MkdirAll(deep, 0o700); err != nil {
96 t.Fatal(err)
97 }
98 w, err := newWorkspaceWatcher()
99 if err != nil {
100 t.Fatal(err)
101 }
102 t.Cleanup(func() { _ = w.Close() })
103 if err := w.Add(root, true); err != nil {
104 t.Fatal(err)
105 }
106 target := filepath.Join(deep, "unicode-\u6587\u4ef6-\U0001F600.txt")
107 if err := os.WriteFile(target, []byte("deep"), 0o600); err != nil {
108 t.Fatal(err)
109 }
110 timer := time.NewTimer(3 * time.Second)
111 defer timer.Stop()
112 for {
113 select {
114 case ev := <-w.Events():
115 if filepath.Clean(ev.Name) == target {
116 return
117 }
118 case err := <-w.Errors():
119 t.Fatalf("watcher error: %v", err)
120 case <-timer.C:
121 t.Fatalf("no event for exact Unicode path %q", target)
122 }
123 }
124 }
125
126 func TestWorkspaceWatcherReportsPreexistingDeepFileChangesOnWindows(t *testing.T) {
127 root := t.TempDir()
128 deep := filepath.Join(root, "one", "two", "three")
129 if err := os.MkdirAll(deep, 0o700); err != nil {
130 t.Fatal(err)
131 }
132 app := &App{tabs: map[string]*WorkspaceTab{"a": {ID: "a", WorkspaceRoot: root}}}
133 app.workspaceHub = newWorkspaceChangeHub(app)
134 t.Cleanup(func() { app.workspaceHub.close() })
135 before := app.WorkspaceRevisionForTab("a").Revisions.Content
136 if err := os.WriteFile(filepath.Join(deep, "file.txt"), []byte("deep"), 0o600); err != nil {
137 t.Fatal(err)
138 }
139 waitForWorkspaceContentRevision(t, app, before)
140 }
141
142 func TestWorkspaceWatcherReportsFilesInNewDeepDirectoriesOnWindows(t *testing.T) {
143 root := t.TempDir()
144 app := &App{tabs: map[string]*WorkspaceTab{"a": {ID: "a", WorkspaceRoot: root}}}
145 app.workspaceHub = newWorkspaceChangeHub(app)
146 t.Cleanup(func() { app.workspaceHub.close() })
147 before := app.WorkspaceRevisionForTab("a").Revisions.Content
148 deep := filepath.Join(root, "new", "nested")
149 if err := os.MkdirAll(deep, 0o700); err != nil {
150 t.Fatal(err)
151 }
152 waitForWorkspaceContentRevision(t, app, before)
153 before = waitForWorkspaceRevisionToSettle(t, app)
154 if err := os.WriteFile(filepath.Join(deep, "later.txt"), []byte("later"), 0o600); err != nil {
155 t.Fatal(err)
156 }
157 waitForWorkspaceContentRevision(t, app, before)
158 }
159
160 func TestWorkspaceWatcherIgnoresGeneratedSubtreeChurnOnWindows(t *testing.T) {
161 root := t.TempDir()
162 generated := filepath.Join(root, "node_modules", "pkg")
163 if err := os.MkdirAll(generated, 0o700); err != nil {
164 t.Fatal(err)
165 }
166 app := &App{tabs: map[string]*WorkspaceTab{"a": {ID: "a", WorkspaceRoot: root}}}
167 app.workspaceHub = newWorkspaceChangeHub(app)
168 t.Cleanup(func() { app.workspaceHub.close() })
169 before := app.WorkspaceRevisionForTab("a").Revisions.Content
170 for i := range 256 {
171 name := filepath.Join(generated, fmt.Sprintf("generated-%03d.js", i))
172 if err := os.WriteFile(name, []byte("generated"), 0o600); err != nil {
173 t.Fatal(err)
174 }
175 }
176 time.Sleep(300 * time.Millisecond)
177 view := app.WorkspaceRevisionForTab("a")
178 after := view.Revisions.Content
179 if after != before {
180 t.Fatalf("generated subtree advanced content revision: before=%d after=%d", before, after)
181 }
182 if view.WatchState != "active" {
183 t.Fatalf("generated subtree degraded recursive watcher: %s", view.WatchState)
184 }
185 }
186
187 func TestWorkspaceWatcherCloseReleasesNestedDirectoriesOnWindows(t *testing.T) {
188 root := t.TempDir()
189 from := filepath.Join(root, "parent")
190 if err := os.MkdirAll(filepath.Join(from, "child"), 0o700); err != nil {
191 t.Fatal(err)
192 }
193 app := &App{tabs: map[string]*WorkspaceTab{"a": {ID: "a", WorkspaceRoot: root}}}
194 app.workspaceHub = newWorkspaceChangeHub(app)
195 app.WorkspaceRevisionForTab("a")
196 app.workspaceHub.close()
197 if err := os.Rename(from, filepath.Join(root, "after-close")); err != nil {
198 t.Fatalf("rename after workspace watcher close: %v", err)
199 }
200 }
201
202 func TestWorkspaceWatcherSwitchReleasesOldWorkspaceOnWindows(t *testing.T) {
203 rootA, rootB := t.TempDir(), t.TempDir()
204 from := filepath.Join(rootA, "parent")
205 if err := os.MkdirAll(filepath.Join(from, "child"), 0o700); err != nil {
206 t.Fatal(err)
207 }
208 app := &App{tabs: map[string]*WorkspaceTab{"a": {ID: "a", WorkspaceRoot: rootA}}}
209 app.workspaceHub = newWorkspaceChangeHub(app)
210 t.Cleanup(func() { app.workspaceHub.close() })
211 app.WorkspaceRevisionForTab("a")
212 app.mu.Lock()
213 app.tabs["a"].WorkspaceRoot = rootB
214 app.mu.Unlock()
215 app.WorkspaceRevisionForTab("a")
216 app.workspaceHub.reconcileRoots()
217
218 if err := os.Rename(from, filepath.Join(rootA, "after-switch")); err != nil {
219 t.Fatalf("rename in released workspace: %v", err)
220 }
221 before := app.WorkspaceRevisionForTab("a").Revisions.Content
222 if err := os.WriteFile(filepath.Join(rootB, "active.txt"), []byte("active"), 0o600); err != nil {
223 t.Fatal(err)
224 }
225 waitForWorkspaceContentRevision(t, app, before)
226 }
227
228 func TestWorkspaceWatcherKeepsGitMetadataScopeOnWindows(t *testing.T) {
229 root := t.TempDir()
230 if out, err := exec.Command("git", "-C", root, "init").CombinedOutput(); err != nil {
231 t.Fatalf("git init: %v: %s", err, out)
232 }
233 app := &App{tabs: map[string]*WorkspaceTab{"a": {ID: "a", WorkspaceRoot: root}}}
234 app.workspaceHub = newWorkspaceChangeHub(app)
235 t.Cleanup(func() { app.workspaceHub.close() })
236 before := app.WorkspaceRevisionForTab("a").Revisions.GitMeta
237 ref := filepath.Join(root, ".git", "refs", "heads", "watch-test")
238 if err := os.WriteFile(ref, []byte("0000000000000000000000000000000000000000\n"), 0o600); err != nil {
239 t.Fatal(err)
240 }
241 waitForWorkspaceGitRevision(t, app, before)
242 before = waitForWorkspaceGitRevisionToSettle(t, app)
243 objectDir := filepath.Join(root, ".git", "objects", "ff")
244 if err := os.MkdirAll(objectDir, 0o700); err != nil {
245 t.Fatal(err)
246 }
247 if err := os.WriteFile(filepath.Join(objectDir, "ignored"), []byte("object"), 0o600); err != nil {
248 t.Fatal(err)
249 }
250 time.Sleep(200 * time.Millisecond)
251 after := app.WorkspaceRevisionForTab("a").Revisions.GitMeta
252 if after != before {
253 t.Fatalf("git object churn advanced metadata revision: before=%d after=%d", before, after)
254 }
255 }
256
257 func TestWorkspaceWatcherTracksExternalGitMetadataOnWindows(t *testing.T) {
258 base := t.TempDir()
259 root := filepath.Join(base, "workspace")
260 gitDir := filepath.Join(base, "git-dir")
261 if out, err := exec.Command("git", "init", "--separate-git-dir", gitDir, root).CombinedOutput(); err != nil {
262 t.Fatalf("git init --separate-git-dir: %v: %s", err, out)
263 }
264 if err := os.MkdirAll(filepath.Join(root, "parent", "child"), 0o700); err != nil {
265 t.Fatal(err)
266 }
267 app := &App{tabs: map[string]*WorkspaceTab{"a": {ID: "a", WorkspaceRoot: root}}}
268 app.workspaceHub = newWorkspaceChangeHub(app)
269 t.Cleanup(func() { app.workspaceHub.close() })
270 before := app.WorkspaceRevisionForTab("a").Revisions.GitMeta
271 ref := filepath.Join(gitDir, "refs", "heads", "watch-test")
272 if err := os.WriteFile(ref, []byte("0000000000000000000000000000000000000000\n"), 0o600); err != nil {
273 t.Fatal(err)
274 }
275 waitForWorkspaceGitRevision(t, app, before)
276 if err := os.Rename(filepath.Join(root, "parent"), filepath.Join(root, "renamed")); err != nil {
277 t.Fatalf("external Git watches locked a workspace child: %v", err)
278 }
279 }
280
281 func waitForWorkspaceContentRevision(t *testing.T, app *App, before uint64) uint64 {
282 t.Helper()
283 deadline := time.Now().Add(3 * time.Second)
284 for time.Now().Before(deadline) {
285 got := app.WorkspaceRevisionForTab("a").Revisions.Content
286 if got > before {
287 return got
288 }
289 time.Sleep(10 * time.Millisecond)
290 }
291 t.Fatalf("content revision did not advance beyond %d", before)
292 return before
293 }
294
295 func waitForWorkspaceRevisionToSettle(t *testing.T, app *App) uint64 {
296 t.Helper()
297 last := app.WorkspaceRevisionForTab("a").Revisions.Content
298 stableSince := time.Now()
299 deadline := time.Now().Add(3 * time.Second)
300 for time.Now().Before(deadline) {
301 time.Sleep(10 * time.Millisecond)
302 got := app.WorkspaceRevisionForTab("a").Revisions.Content
303 if got != last {
304 last = got
305 stableSince = time.Now()
306 continue
307 }
308 if time.Since(stableSince) >= 100*time.Millisecond {
309 return got
310 }
311 }
312 t.Fatalf("content revision did not settle")
313 return last
314 }
315
316 func waitForWorkspaceGitRevision(t *testing.T, app *App, before uint64) uint64 {
317 t.Helper()
318 deadline := time.Now().Add(3 * time.Second)
319 for time.Now().Before(deadline) {
320 got := app.WorkspaceRevisionForTab("a").Revisions.GitMeta
321 if got > before {
322 return got
323 }
324 time.Sleep(10 * time.Millisecond)
325 }
326 t.Fatalf("git metadata revision did not advance beyond %d", before)
327 return before
328 }
329
330 func waitForWorkspaceGitRevisionToSettle(t *testing.T, app *App) uint64 {
331 t.Helper()
332 last := app.WorkspaceRevisionForTab("a").Revisions.GitMeta
333 stableSince := time.Now()
334 deadline := time.Now().Add(3 * time.Second)
335 for time.Now().Before(deadline) {
336 time.Sleep(10 * time.Millisecond)
337 got := app.WorkspaceRevisionForTab("a").Revisions.GitMeta
338 if got != last {
339 last = got
340 stableSince = time.Now()
341 continue
342 }
343 if time.Since(stableSince) >= 100*time.Millisecond {
344 return got
345 }
346 }
347 t.Fatalf("git metadata revision did not settle")
348 return last
349 }
350
350 lines GO