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