返回 DeepSeek-Reasonix
manager_test.go
根目录 / internal / sessiontemp / manager_test.go
1 package sessiontemp
2
3 import (
4 "errors"
5 "os"
6 "path/filepath"
7 "runtime"
8 "sync"
9 "testing"
10 "time"
11 )
12
13 func TestAcquireSharesGeneration(t *testing.T) {
14 m := newForTest(t.TempDir())
15 m.Retain()
16 defer m.Release()
17
18 a, err := m.Acquire()
19 if err != nil {
20 t.Fatal(err)
21 }
22 b, err := m.Acquire()
23 if err != nil {
24 t.Fatal(err)
25 }
26 if a.Dir() == "" || a.Dir() != b.Dir() {
27 t.Fatalf("dirs = %q, %q; want same non-empty dir", a.Dir(), b.Dir())
28 }
29 info, err := os.Stat(a.Dir())
30 if err != nil || !info.IsDir() {
31 t.Fatalf("dir stat: %v", err)
32 }
33 // Windows does not expose POSIX directory permission bits. The
34 // cross-platform contract is that the manager creates a private directory;
35 // the exact 0700 mode is meaningful only on Unix-like systems.
36 if runtime.GOOS != "windows" {
37 if perm := info.Mode().Perm(); perm != 0o700 {
38 t.Fatalf("dir perm = %o, want 0700", perm)
39 }
40 }
41 a.Release()
42 b.Release()
43 if _, err := os.Stat(a.Dir()); err != nil {
44 t.Fatalf("active generation should remain while manager owned: %v", err)
45 }
46 }
47
48 func TestRotateIsolatesNewCommands(t *testing.T) {
49 m := newForTest(t.TempDir())
50 m.Retain()
51 defer m.Release()
52
53 old, err := m.Acquire()
54 if err != nil {
55 t.Fatal(err)
56 }
57 oldDir := old.Dir()
58 if err := os.WriteFile(filepath.Join(oldDir, "keep.txt"), []byte("x"), 0o600); err != nil {
59 t.Fatal(err)
60 }
61
62 m.Rotate()
63 fresh, err := m.Acquire()
64 if err != nil {
65 t.Fatal(err)
66 }
67 if fresh.Dir() == oldDir {
68 t.Fatal("rotate should create a new directory")
69 }
70 if _, err := os.Stat(filepath.Join(fresh.Dir(), "keep.txt")); !os.IsNotExist(err) {
71 t.Fatalf("new generation must not see old files: %v", err)
72 }
73 // Old generation remains while leased.
74 if _, err := os.Stat(filepath.Join(oldDir, "keep.txt")); err != nil {
75 t.Fatalf("leased old generation deleted early: %v", err)
76 }
77 old.Release()
78 if _, err := os.Stat(oldDir); !os.IsNotExist(err) {
79 t.Fatalf("old generation should be removed after last lease: %v", err)
80 }
81 fresh.Release()
82 }
83
84 func TestLastLeaseDeletesRetiredGeneration(t *testing.T) {
85 m := newForTest(t.TempDir())
86 m.Retain()
87
88 lease, err := m.Acquire()
89 if err != nil {
90 t.Fatal(err)
91 }
92 dir := lease.Dir()
93 m.Release() // last controller owner retires current generation
94 if _, err := os.Stat(dir); err != nil {
95 t.Fatalf("retired generation with live lease must remain: %v", err)
96 }
97 lease.Release()
98 if _, err := os.Stat(dir); !os.IsNotExist(err) {
99 t.Fatalf("directory should be deleted after last lease: %v", err)
100 }
101 }
102
103 func TestHotRebuildRetainRelease(t *testing.T) {
104 m := newForTest(t.TempDir())
105 m.Retain() // old controller
106 lease, err := m.Acquire()
107 if err != nil {
108 t.Fatal(err)
109 }
110 dir := lease.Dir()
111 lease.Release()
112
113 m.Retain() // replacement controller
114 m.Release() // old controller closes — must not delete while new owns
115 if _, err := os.Stat(dir); err != nil {
116 t.Fatalf("hot rebuild must keep generation: %v", err)
117 }
118
119 again, err := m.Acquire()
120 if err != nil {
121 t.Fatal(err)
122 }
123 if again.Dir() != dir {
124 t.Fatalf("hot rebuild should reuse generation: got %q want %q", again.Dir(), dir)
125 }
126 again.Release()
127 m.Release()
128 if _, err := os.Stat(dir); !os.IsNotExist(err) {
129 t.Fatalf("final release should delete: %v", err)
130 }
131 }
132
133 func TestAcquireCreateFailureDoesNotFallback(t *testing.T) {
134 m := newForTest(t.TempDir())
135 m.Retain()
136 defer m.Release()
137 m.mkDir = func(string) (string, error) {
138 return "", os.ErrPermission
139 }
140 if _, err := m.Acquire(); err == nil {
141 t.Fatal("want create failure")
142 }
143 }
144
145 func TestAcquireAfterLastOwnerReleaseIsSealed(t *testing.T) {
146 m := newForTest(t.TempDir())
147 m.Retain()
148 lease, err := m.Acquire()
149 if err != nil {
150 t.Fatal(err)
151 }
152 dir := lease.Dir()
153
154 // Force Release → delayed Acquire ordering with a channel barrier.
155 released := make(chan struct{})
156 acquired := make(chan error, 1)
157 go func() {
158 <-released
159 _, err := m.Acquire()
160 acquired <- err
161 }()
162
163 m.Release() // last owner — seals
164 close(released)
165 err = <-acquired
166 if err == nil {
167 t.Fatal("Acquire after last Release must fail closed")
168 }
169 if !errors.Is(err, ErrUnavailable) {
170 t.Fatalf("error = %v, want ErrUnavailable", err)
171 }
172 if !m.Sealed() {
173 t.Fatal("manager should be sealed")
174 }
175
176 // Live lease still pins the directory until it releases.
177 if _, err := os.Stat(dir); err != nil {
178 t.Fatalf("leased generation deleted while sealed: %v", err)
179 }
180 lease.Release()
181 if _, err := os.Stat(dir); !os.IsNotExist(err) {
182 t.Fatalf("generation should delete after last lease on sealed manager: %v", err)
183 }
184
185 // Retain after seal must not reopen.
186 m.Retain()
187 if _, err := m.Acquire(); err == nil {
188 t.Fatal("Retain after seal must not reopen Acquire")
189 }
190 }
191
192 func TestAcquireWithoutOwnerFailsClosed(t *testing.T) {
193 m := newForTest(t.TempDir())
194 if _, err := m.Acquire(); err == nil {
195 t.Fatal("Acquire with zero owners must fail")
196 }
197 }
198
199 func TestProcessCleanupRunsOncePerRoot(t *testing.T) {
200 resetProcessCleanupForTest()
201 root := t.TempDir()
202 // Plant a stale dir that would be eligible if cleanup ran with an old now.
203 // We only count whether cleanupStaleOnce marks the root done.
204 cleanupStaleOnce(root, time.Now)
205 cleanupStaleOnce(root, time.Now)
206 processCleanup.Lock()
207 key := canonicalTempRoot(root)
208 _, ok := processCleanup.done[key]
209 n := len(processCleanup.done)
210 processCleanup.Unlock()
211 if !ok {
212 t.Fatal("root not marked cleaned")
213 }
214 if n != 1 {
215 t.Fatalf("cleanup map size = %d, want 1 entry for one root", n)
216 }
217 // A second New against the real TempDir should not panic; first process
218 // New still uses process-level once.
219 _ = New()
220 _ = New()
221 }
222
223 func TestConcurrentAcquireRotateReleaseRace(t *testing.T) {
224 m := newForTest(t.TempDir())
225 m.Retain()
226 defer m.Release()
227
228 var wg sync.WaitGroup
229 for i := 0; i < 32; i++ {
230 wg.Add(1)
231 go func() {
232 defer wg.Done()
233 for j := 0; j < 50; j++ {
234 lease, err := m.Acquire()
235 if err != nil {
236 t.Errorf("acquire: %v", err)
237 return
238 }
239 _ = os.WriteFile(filepath.Join(lease.Dir(), "x"), []byte("1"), 0o600)
240 if j%7 == 0 {
241 m.Rotate()
242 }
243 lease.Release()
244 }
245 }()
246 }
247 wg.Wait()
248 }
249
250 func TestStaleCleanup(t *testing.T) {
251 root := t.TempDir()
252 now := time.Now()
253
254 // Fresh dir — skip.
255 fresh, err := os.MkdirTemp(root, dirPrefix)
256 if err != nil {
257 t.Fatal(err)
258 }
259
260 // Active locked dir older than 24h — skip.
261 active, err := os.MkdirTemp(root, dirPrefix)
262 if err != nil {
263 t.Fatal(err)
264 }
265 release, err := filelockAcquire(filepath.Join(active, ownerLockName))
266 if err != nil {
267 t.Fatal(err)
268 }
269 defer release()
270 // Chtimes after lock creation: writing the lock file refreshes dir mtime.
271 if err := os.Chtimes(active, now.Add(-48*time.Hour), now.Add(-48*time.Hour)); err != nil {
272 t.Fatal(err)
273 }
274
275 // Stale unlocked dir — delete.
276 stale, err := os.MkdirTemp(root, dirPrefix)
277 if err != nil {
278 t.Fatal(err)
279 }
280 // Create and release lock file so TryAcquire can succeed, then age the dir.
281 lockPath := filepath.Join(stale, ownerLockName)
282 r, err := filelockAcquire(lockPath)
283 if err != nil {
284 t.Fatal(err)
285 }
286 r()
287 if err := os.Chtimes(stale, now.Add(-48*time.Hour), now.Add(-48*time.Hour)); err != nil {
288 t.Fatal(err)
289 }
290
291 // Unrelated directory — skip.
292 other := filepath.Join(root, "not-reasonix")
293 if err := os.Mkdir(other, 0o700); err != nil {
294 t.Fatal(err)
295 }
296 if err := os.Chtimes(other, now.Add(-48*time.Hour), now.Add(-48*time.Hour)); err != nil {
297 t.Fatal(err)
298 }
299
300 // Symlink to a foreign target — remove only the symlink entry, not the target.
301 foreign := filepath.Join(t.TempDir(), "foreign-target")
302 if err := os.Mkdir(foreign, 0o700); err != nil {
303 t.Fatal(err)
304 }
305 marker := filepath.Join(foreign, "marker")
306 if err := os.WriteFile(marker, []byte("safe"), 0o600); err != nil {
307 t.Fatal(err)
308 }
309 link := filepath.Join(root, dirPrefix+"link")
310 if err := os.Symlink(foreign, link); err != nil {
311 t.Fatal(err)
312 }
313 if err := os.Chtimes(link, now.Add(-48*time.Hour), now.Add(-48*time.Hour)); err != nil {
314 // Some platforms cannot chtimes symlinks; fall back to cleaning with a
315 // forced old now so age check uses Lstat mtime of the link when set.
316 _ = err
317 }
318
319 cleanupStale(root, func() time.Time { return now })
320
321 if _, err := os.Stat(fresh); err != nil {
322 t.Fatalf("fresh dir removed: %v", err)
323 }
324 if _, err := os.Stat(active); err != nil {
325 t.Fatalf("active locked dir removed: %v", err)
326 }
327 if _, err := os.Stat(stale); !os.IsNotExist(err) {
328 t.Fatalf("stale dir should be removed: %v", err)
329 }
330 if _, err := os.Stat(other); err != nil {
331 t.Fatalf("unrelated dir removed: %v", err)
332 }
333 if _, err := os.Stat(marker); err != nil {
334 t.Fatalf("symlink cleanup deleted foreign target: %v", err)
335 }
336 }
337
338 func filelockAcquire(path string) (func(), error) {
339 // Local import shim for tests in this package.
340 return tryLockForTest(path)
341 }
342
342 lines GO