返回 DeepSeek-Reasonix
session_lease_test.go
根目录 / internal / agent / session_lease_test.go
1 package agent
2
3 import (
4 "errors"
5 "os"
6 "path/filepath"
7 "sync"
8 "testing"
9 "time"
10
11 "reasonix/internal/store"
12 )
13
14 // leaseTestPath returns a session path in "user shape" — mixed case, exactly
15 // as desktop/CLI callers pass it — plus its canonical registry key for
16 // internal-state setup and assertions. Tests must feed the user shape to the
17 // API under test: feeding pre-canonicalized paths is how the Windows
18 // case-fold mismatch (#5999) escaped this suite. On non-Windows hosts the two
19 // forms are identical; on Windows they differ and exercise the fold.
20 func leaseTestPath(t *testing.T) (userPath, key string) {
21 t.Helper()
22 userPath = filepath.Join(t.TempDir(), "Sessions-Dir", "Session-Test.jsonl")
23 if err := os.MkdirAll(filepath.Dir(userPath), 0o755); err != nil {
24 t.Fatalf("mkdir: %v", err)
25 }
26 return userPath, canonicalSessionSavePath(userPath)
27 }
28
29 func TestSessionLeaseRejectsConcurrentWriterAndReleases(t *testing.T) {
30 userPath, _ := leaseTestPath(t)
31 first, err := TryAcquireSessionLease(userPath)
32 if err != nil {
33 t.Fatalf("first TryAcquireSessionLease: %v", err)
34 }
35 if first.Path() == "" {
36 t.Fatal("first lease path is empty")
37 }
38 info, err := LoadSessionLeaseInfo(userPath)
39 if err != nil {
40 t.Fatalf("LoadSessionLeaseInfo: %v", err)
41 }
42 if info.WriterID == "" || info.PID == 0 || info.SessionPath == "" {
43 t.Fatalf("lease info = %+v, want writer metadata", info)
44 }
45
46 second, err := TryAcquireSessionLease(userPath)
47 if !errors.Is(err, ErrSessionLeaseHeld) {
48 t.Fatalf("second TryAcquireSessionLease err = %v, want ErrSessionLeaseHeld", err)
49 }
50 if second != nil {
51 second.Release()
52 t.Fatal("second lease unexpectedly acquired")
53 }
54
55 first.Release()
56 third, err := TryAcquireSessionLease(userPath)
57 if err != nil {
58 t.Fatalf("third TryAcquireSessionLease after release: %v", err)
59 }
60 third.Release()
61 }
62
63 func TestSessionLeaseReclaimsCurrentProcessStaleOwner(t *testing.T) {
64 userPath, key := leaseTestPath(t)
65 sessionLeaseOwners.Store(key, struct{}{})
66 t.Cleanup(func() {
67 sessionLeaseOwners.Delete(key)
68 _ = os.Remove(sessionLeaseInfoPath(key))
69 })
70 if err := SaveSessionLeaseInfo(key, SessionLeaseInfo{
71 SessionPath: key,
72 WriterID: SessionWriterID(),
73 PID: os.Getpid(),
74 AcquiredAt: time.Now().UTC(),
75 }); err != nil {
76 t.Fatalf("SaveSessionLeaseInfo: %v", err)
77 }
78 if lease, err := TryAcquireSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
79 if lease != nil {
80 lease.Release()
81 }
82 t.Fatalf("TryAcquireSessionLease err = %v, want ErrSessionLeaseHeld", err)
83 }
84 lease, err := TryReclaimCurrentProcessSessionLease(userPath)
85 if err != nil {
86 t.Fatalf("TryReclaimCurrentProcessSessionLease: %v", err)
87 }
88 lease.Release()
89 }
90
91 func TestSessionLeaseReclaimsOrphanedEntryWithoutInfo(t *testing.T) {
92 // An orphaned in-process entry whose lease.json was deleted out from
93 // under it (manual cleanup, AV quarantine). Nothing actually holds the
94 // session — the OS lock is free — so reclaim must recover instead of
95 // wedging every rebuild as busy. Before the lock-arbiter rework this
96 // deadlocked: reclaim fell back to a plain acquire, which re-hit the
97 // orphaned map entry forever.
98 userPath, key := leaseTestPath(t)
99 sessionLeaseOwners.Store(key, uint64(1<<61))
100 t.Cleanup(func() { sessionLeaseOwners.Delete(key) })
101
102 if lease, err := TryAcquireSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
103 if lease != nil {
104 lease.Release()
105 }
106 t.Fatalf("TryAcquireSessionLease err = %v, want ErrSessionLeaseHeld", err)
107 }
108 lease, err := TryReclaimCurrentProcessSessionLease(userPath)
109 if err != nil {
110 t.Fatalf("TryReclaimCurrentProcessSessionLease without info: %v", err)
111 }
112 if _, err := LoadSessionLeaseInfo(userPath); err != nil {
113 t.Fatalf("reclaim should have rewritten lease info, load err = %v", err)
114 }
115 lease.Release()
116 }
117
118 func TestSessionLeaseReclaimsOrphanedEntryWithCorruptInfo(t *testing.T) {
119 // Same as above but the sidecar is torn (empty/undecodable) rather than
120 // missing: identity is unreadable, the lock is free, reclaim must win.
121 userPath, key := leaseTestPath(t)
122 sessionLeaseOwners.Store(key, uint64(1<<61))
123 t.Cleanup(func() {
124 sessionLeaseOwners.Delete(key)
125 _ = os.Remove(sessionLeaseInfoPath(key))
126 })
127 if err := os.WriteFile(sessionLeaseInfoPath(key), []byte("{torn"), 0o644); err != nil {
128 t.Fatalf("write corrupt lease info: %v", err)
129 }
130
131 lease, err := TryReclaimCurrentProcessSessionLease(userPath)
132 if err != nil {
133 t.Fatalf("TryReclaimCurrentProcessSessionLease with corrupt info: %v", err)
134 }
135 lease.Release()
136 }
137
138 func TestSessionLeaseReclaimRefusesForeignInfo(t *testing.T) {
139 // A readable info naming another runtime is never stolen by reclaim,
140 // even with the lock free — that separation belongs to
141 // SessionLeaseHeldByOtherRuntime's cleanup, not to reclaim.
142 userPath, key := leaseTestPath(t)
143 if err := SaveSessionLeaseInfo(key, SessionLeaseInfo{
144 SessionPath: key,
145 WriterID: "other-host-1234-deadbeef",
146 PID: os.Getpid() + 1,
147 AcquiredAt: time.Now().UTC(),
148 }); err != nil {
149 t.Fatalf("SaveSessionLeaseInfo: %v", err)
150 }
151 t.Cleanup(func() { _ = os.Remove(sessionLeaseInfoPath(key)) })
152
153 if lease, err := TryReclaimCurrentProcessSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
154 if lease != nil {
155 lease.Release()
156 }
157 t.Fatalf("TryReclaimCurrentProcessSessionLease err = %v, want ErrSessionLeaseHeld", err)
158 }
159 }
160
161 func TestSessionLeaseConcurrentReclaimSingleWinner(t *testing.T) {
162 userPath, key := leaseTestPath(t)
163 sessionLeaseOwners.Store(key, struct{}{})
164 t.Cleanup(func() {
165 sessionLeaseOwners.Delete(key)
166 _ = os.Remove(sessionLeaseInfoPath(key))
167 })
168 if err := SaveSessionLeaseInfo(key, SessionLeaseInfo{
169 SessionPath: key,
170 WriterID: SessionWriterID(),
171 PID: os.Getpid(),
172 AcquiredAt: time.Now().UTC(),
173 }); err != nil {
174 t.Fatalf("SaveSessionLeaseInfo: %v", err)
175 }
176
177 const attempts = 16
178 var wg sync.WaitGroup
179 leases := make(chan *SessionLease, attempts)
180 start := make(chan struct{})
181 for range attempts {
182 wg.Add(1)
183 go func() {
184 defer wg.Done()
185 <-start
186 if lease, err := TryReclaimCurrentProcessSessionLease(userPath); err == nil && lease != nil {
187 leases <- lease
188 }
189 }()
190 }
191 close(start)
192 wg.Wait()
193 close(leases)
194
195 var won []*SessionLease
196 for lease := range leases {
197 won = append(won, lease)
198 }
199 if len(won) != 1 {
200 t.Fatalf("concurrent reclaim produced %d leases, want exactly 1", len(won))
201 }
202 // The losers must not have evicted the winner's owner entry.
203 if _, ok := sessionLeaseOwners.Load(key); !ok {
204 t.Fatal("winner's owner entry was evicted by a failed concurrent reclaim")
205 }
206 if lease, err := TryAcquireSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
207 if lease != nil {
208 lease.Release()
209 }
210 t.Fatalf("TryAcquireSessionLease while reclaimed lease is held err = %v, want ErrSessionLeaseHeld", err)
211 }
212 won[0].Release()
213 lease, err := TryAcquireSessionLease(userPath)
214 if err != nil {
215 t.Fatalf("TryAcquireSessionLease after release: %v", err)
216 }
217 lease.Release()
218 }
219
220 func TestSessionLeaseReclaimRefusesActiveHolder(t *testing.T) {
221 userPath, key := leaseTestPath(t)
222 holder, err := TryAcquireSessionLease(userPath)
223 if err != nil {
224 t.Fatalf("TryAcquireSessionLease: %v", err)
225 }
226 defer holder.Release()
227
228 if lease, err := TryReclaimCurrentProcessSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
229 if lease != nil {
230 lease.Release()
231 }
232 t.Fatalf("TryReclaimCurrentProcessSessionLease err = %v, want ErrSessionLeaseHeld", err)
233 }
234 // The failed reclaim must leave the holder's owner entry intact.
235 if _, ok := sessionLeaseOwners.Load(key); !ok {
236 t.Fatal("active holder's owner entry was evicted by a failed reclaim")
237 }
238 if lease, err := TryAcquireSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
239 if lease != nil {
240 lease.Release()
241 }
242 t.Fatalf("TryAcquireSessionLease err = %v, want ErrSessionLeaseHeld", err)
243 }
244 }
245
246 func TestSessionLeaseReclaimAfterHolderReleased(t *testing.T) {
247 userPath, _ := leaseTestPath(t)
248 holder, err := TryAcquireSessionLease(userPath)
249 if err != nil {
250 t.Fatalf("TryAcquireSessionLease: %v", err)
251 }
252 holder.Release()
253
254 // The holder released between the caller's failed acquire and the
255 // reclaim: the lease info file is gone and the lock is free, so the
256 // reclaim must win the lease cleanly.
257 lease, err := TryReclaimCurrentProcessSessionLease(userPath)
258 if err != nil {
259 t.Fatalf("TryReclaimCurrentProcessSessionLease after release: %v", err)
260 }
261 lease.Release()
262 }
263
264 func TestSessionLeaseStaleReleaseKeepsNewOwnerEntry(t *testing.T) {
265 userPath, key := leaseTestPath(t)
266 stale, err := TryAcquireSessionLease(userPath)
267 if err != nil {
268 t.Fatalf("TryAcquireSessionLease: %v", err)
269 }
270 // Simulate a reclaim that took over the entry while the stale lease was
271 // still alive: the map now names a different owner.
272 sessionLeaseOwners.Store(key, uint64(1<<62))
273 t.Cleanup(func() { sessionLeaseOwners.Delete(key) })
274
275 stale.Release()
276 if _, ok := sessionLeaseOwners.Load(key); !ok {
277 t.Fatal("stale Release evicted the new owner's entry")
278 }
279 }
280
281 func TestSessionLeaseHeldByOtherRuntime(t *testing.T) {
282 t.Run("no lease", func(t *testing.T) {
283 userPath, _ := leaseTestPath(t)
284 if SessionLeaseHeldByOtherRuntime(userPath) {
285 t.Fatal("unheld session reported as held by another runtime")
286 }
287 })
288 t.Run("held by this process", func(t *testing.T) {
289 userPath, _ := leaseTestPath(t)
290 lease, err := TryAcquireSessionLease(userPath)
291 if err != nil {
292 t.Fatalf("TryAcquireSessionLease: %v", err)
293 }
294 defer lease.Release()
295 if SessionLeaseHeldByOtherRuntime(userPath) {
296 t.Fatal("own lease reported as held by another runtime")
297 }
298 })
299 t.Run("foreign info with live lock", func(t *testing.T) {
300 userPath, key := leaseTestPath(t)
301 unlock, err := tryLockSessionLeaseFile(key)
302 if err != nil {
303 t.Fatalf("tryLockSessionLeaseFile: %v", err)
304 }
305 defer unlock()
306 if err := SaveSessionLeaseInfo(key, SessionLeaseInfo{
307 SessionPath: key,
308 WriterID: "other-host-1234-deadbeef",
309 PID: os.Getpid() + 1,
310 AcquiredAt: time.Now().UTC(),
311 }); err != nil {
312 t.Fatalf("SaveSessionLeaseInfo: %v", err)
313 }
314 t.Cleanup(func() { _ = os.Remove(sessionLeaseInfoPath(key)) })
315 if !SessionLeaseHeldByOtherRuntime(userPath) {
316 t.Fatal("foreign-held session not reported as held by another runtime")
317 }
318 })
319 t.Run("foreign info from crashed process", func(t *testing.T) {
320 userPath, key := leaseTestPath(t)
321 if err := SaveSessionLeaseInfo(key, SessionLeaseInfo{
322 SessionPath: key,
323 WriterID: "other-host-1234-deadbeef",
324 PID: os.Getpid() + 1,
325 AcquiredAt: time.Now().UTC(),
326 }); err != nil {
327 t.Fatalf("SaveSessionLeaseInfo: %v", err)
328 }
329 t.Cleanup(func() { _ = os.Remove(sessionLeaseInfoPath(key)) })
330 // Info file left behind but the lock is free: the holder crashed, so
331 // the session is not considered held.
332 if SessionLeaseHeldByOtherRuntime(userPath) {
333 t.Fatal("crashed holder's leftover info reported as held")
334 }
335 if _, err := os.Stat(sessionLeaseInfoPath(key)); !os.IsNotExist(err) {
336 t.Fatalf("crashed holder's leftover info should be removed, stat err = %v", err)
337 }
338 })
339 t.Run("corrupt info from crashed process", func(t *testing.T) {
340 userPath, key := leaseTestPath(t)
341 if err := os.WriteFile(sessionLeaseInfoPath(key), nil, 0o644); err != nil {
342 t.Fatalf("write corrupt lease info: %v", err)
343 }
344 if SessionLeaseHeldByOtherRuntime(userPath) {
345 t.Fatal("corrupt crashed holder info reported as held")
346 }
347 if _, err := os.Stat(sessionLeaseInfoPath(key)); !os.IsNotExist(err) {
348 t.Fatalf("corrupt lease info should be removed, stat err = %v", err)
349 }
350 })
351 }
352
353 func TestSessionLeaseHeldByCurrentRuntime(t *testing.T) {
354 userPath, _ := leaseTestPath(t)
355 if SessionLeaseHeldByCurrentRuntime(userPath) {
356 t.Fatal("unheld session reported as owned by the current runtime")
357 }
358 lease, err := TryAcquireSessionLease(userPath)
359 if err != nil {
360 t.Fatalf("TryAcquireSessionLease: %v", err)
361 }
362 if !SessionLeaseHeldByCurrentRuntime(userPath) {
363 lease.Release()
364 t.Fatal("held session was not reported as owned by the current runtime")
365 }
366 lease.Release()
367 if SessionLeaseHeldByCurrentRuntime(userPath) {
368 t.Fatal("released session remained owned by the current runtime")
369 }
370 }
371
372 func TestSessionLeaseHeldByCurrentRuntimeRejectsPendingReservation(t *testing.T) {
373 userPath, key := leaseTestPath(t)
374 ownerID := sessionLeaseSeq.Add(1)
375 sessionLeaseOwners.Store(key, ownerID)
376 t.Cleanup(func() {
377 sessionLeaseOwners.CompareAndDelete(key, ownerID)
378 sessionLeaseActiveOwners.CompareAndDelete(key, ownerID)
379 })
380
381 if SessionLeaseHeldByCurrentRuntime(userPath) {
382 t.Fatal("pending acquisition reservation authorized ownership-sensitive repair")
383 }
384 }
385
386 func TestSessionLeaseReleaseRevokesRepairAuthorizationBeforeUnlock(t *testing.T) {
387 userPath, _ := leaseTestPath(t)
388 lease, err := TryAcquireSessionLease(userPath)
389 if err != nil {
390 t.Fatalf("TryAcquireSessionLease: %v", err)
391 }
392 unlock := lease.unlock
393 checked := false
394 lease.unlock = func() {
395 checked = true
396 if SessionLeaseHeldByCurrentRuntime(userPath) {
397 t.Error("release kept repair authorization active while unlocking the OS lease")
398 }
399 unlock()
400 }
401
402 lease.Release()
403 if !checked {
404 t.Fatal("release did not invoke the controlled unlock")
405 }
406 }
407
408 func TestSessionLeaseReleaseRetiresLockSidecars(t *testing.T) {
409 userPath, key := leaseTestPath(t)
410 lease, err := TryAcquireSessionLease(userPath)
411 if err != nil {
412 t.Fatalf("TryAcquireSessionLease: %v", err)
413 }
414 leaseLock := store.SessionLeaseLock(key)
415 if _, err := os.Stat(leaseLock); err != nil {
416 t.Fatalf("lease lock should exist while held: %v", err)
417 }
418 lease.Release()
419 if _, err := os.Stat(leaseLock); !os.IsNotExist(err) {
420 t.Fatalf("lease lock should be retired on release, stat err = %v", err)
421 }
422 if _, err := os.Stat(store.SessionLockFile(key)); !os.IsNotExist(err) {
423 t.Fatalf("save lock should be retired on release, stat err = %v", err)
424 }
425
426 // A release racing a live successor must not strip the successor's lock.
427 first, err := TryAcquireSessionLease(userPath)
428 if err != nil {
429 t.Fatalf("reacquire: %v", err)
430 }
431 second, err := TryAcquireSessionLease(userPath)
432 if !errors.Is(err, ErrSessionLeaseHeld) {
433 if second != nil {
434 second.Release()
435 }
436 t.Fatalf("second acquire err = %v, want ErrSessionLeaseHeld", err)
437 }
438 if _, err := os.Stat(leaseLock); err != nil {
439 t.Fatalf("holder's lease lock must survive a failed acquire: %v", err)
440 }
441 first.Release()
442 }
443
443 lines GO