返回 DeepSeek-Reasonix
session_lease_test.go
根目录 / internal / agent / session_lease_test.go
1 package agent
2
3 import (
4 "encoding/json"
5 "errors"
6 "os"
7 "path/filepath"
8 "strings"
9 "sync"
10 "testing"
11 "time"
12
13 "reasonix/internal/store"
14 )
15
16 // leaseTestPath returns a session path in "user shape" — mixed case, exactly
17 // as desktop/CLI callers pass it — plus its canonical registry key for
18 // internal-state setup and assertions. Tests must feed the user shape to the
19 // API under test: feeding pre-canonicalized paths is how the Windows
20 // case-fold mismatch (#5999) escaped this suite. On non-Windows hosts the two
21 // forms are identical; on Windows they differ and exercise the fold.
22 func leaseTestPath(t *testing.T) (userPath, key string) {
23 t.Helper()
24 userPath = filepath.Join(t.TempDir(), "Sessions-Dir", "Session-Test.jsonl")
25 if err := os.MkdirAll(filepath.Dir(userPath), 0o755); err != nil {
26 t.Fatalf("mkdir: %v", err)
27 }
28 return userPath, CanonicalSessionPath(userPath)
29 }
30
31 func TestSessionLeaseRejectsConcurrentWriterAndReleases(t *testing.T) {
32 userPath, _ := leaseTestPath(t)
33 first, err := TryAcquireSessionLease(userPath)
34 if err != nil {
35 t.Fatalf("first TryAcquireSessionLease: %v", err)
36 }
37 if first.Path() == "" {
38 t.Fatal("first lease path is empty")
39 }
40 info, err := LoadSessionLeaseInfo(userPath)
41 if err != nil {
42 t.Fatalf("LoadSessionLeaseInfo: %v", err)
43 }
44 if info.WriterID == "" || info.PID == 0 || info.SessionPath == "" {
45 t.Fatalf("lease info = %+v, want writer metadata", info)
46 }
47
48 second, err := TryAcquireSessionLease(userPath)
49 if !errors.Is(err, ErrSessionLeaseHeld) {
50 t.Fatalf("second TryAcquireSessionLease err = %v, want ErrSessionLeaseHeld", err)
51 }
52 if second != nil {
53 second.Release()
54 t.Fatal("second lease unexpectedly acquired")
55 }
56
57 first.Release()
58 third, err := TryAcquireSessionLease(userPath)
59 if err != nil {
60 t.Fatalf("third TryAcquireSessionLease after release: %v", err)
61 }
62 third.Release()
63 }
64
65 func TestCompatibleSessionLeaseLocksCompeteWithLegacyName(t *testing.T) {
66 dir := t.TempDir()
67 primaryPath := filepath.Join(dir, "Current", "session.jsonl")
68 legacyPath := filepath.Join(dir, "Legacy", "session.jsonl")
69 for _, path := range []string{primaryPath, legacyPath} {
70 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
71 t.Fatal(err)
72 }
73 }
74
75 legacy, err := tryTakeSessionLeaseLock(legacyPath)
76 if err != nil {
77 t.Fatal(err)
78 }
79 if primary, compatibility, err := tryTakeCompatibleSessionLeaseLocks(primaryPath, legacyPath); !errors.Is(err, ErrSessionLeaseHeld) {
80 if compatibility != nil {
81 compatibility.Unlock()
82 }
83 if primary != nil {
84 primary.Unlock()
85 }
86 t.Fatalf("new acquisition against legacy holder = %v, want ErrSessionLeaseHeld", err)
87 }
88 legacy.Unlock()
89
90 primary, compatibility, err := tryTakeCompatibleSessionLeaseLocks(primaryPath, legacyPath)
91 if err != nil {
92 t.Fatal(err)
93 }
94 if compatibility == nil {
95 t.Fatal("distinct legacy lock was not acquired")
96 }
97 if old, err := tryTakeSessionLeaseLock(legacyPath); !errors.Is(err, ErrSessionLeaseHeld) {
98 if old != nil {
99 old.Unlock()
100 }
101 t.Fatalf("legacy acquisition against new holder = %v, want ErrSessionLeaseHeld", err)
102 }
103 compatibility.RemoveAndUnlock()
104 primary.RemoveAndUnlock()
105
106 // The failed combined acquisition above must not leave its primary lock held.
107 probe, err := tryTakeSessionLeaseLock(primaryPath)
108 if err != nil {
109 t.Fatalf("primary lock leaked after compatibility contention: %v", err)
110 }
111 probe.RemoveAndUnlock()
112 }
113
114 func TestSessionLeaseHandoffTargetsWriterAndGeneration(t *testing.T) {
115 userPath, _ := leaseTestPath(t)
116 originalWriterID := sessionWriterID
117 t.Cleanup(func() { sessionWriterID = originalWriterID })
118 sessionWriterID = "source-writer"
119 source, err := TryAcquireSessionLease(userPath)
120 if err != nil {
121 t.Fatal(err)
122 }
123 if err := source.ReleaseForHandoff("target-writer", "handoff-1"); err != nil {
124 t.Fatalf("ReleaseForHandoff: %v", err)
125 }
126 if lease, err := TryAcquireSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
127 if lease != nil {
128 lease.Release()
129 }
130 t.Fatalf("plain acquire during reservation = %v, want held", err)
131 }
132 sessionWriterID = "wrong-target"
133 if lease, err := TryAcquireSessionLeaseWithHandoff(userPath, "source-writer", "handoff-1"); !errors.Is(err, ErrSessionLeaseHeld) {
134 if lease != nil {
135 lease.Release()
136 }
137 t.Fatalf("wrong writer acquire = %v, want held", err)
138 }
139 sessionWriterID = "target-writer"
140 if lease, err := TryAcquireSessionLeaseWithHandoff(userPath, "wrong-source", "handoff-1"); !errors.Is(err, ErrSessionLeaseHeld) {
141 if lease != nil {
142 lease.Release()
143 }
144 t.Fatalf("wrong source acquire = %v, want held", err)
145 }
146 if lease, err := TryAcquireSessionLeaseWithHandoff(userPath, "source-writer", "stale-generation"); !errors.Is(err, ErrSessionLeaseHeld) {
147 if lease != nil {
148 lease.Release()
149 }
150 t.Fatalf("stale generation acquire = %v, want held", err)
151 }
152 target, err := TryAcquireSessionLeaseWithHandoff(userPath, "source-writer", "handoff-1")
153 if err != nil {
154 t.Fatalf("targeted acquire: %v", err)
155 }
156 info, err := LoadSessionLeaseInfo(userPath)
157 if err != nil {
158 t.Fatal(err)
159 }
160 if info.WriterID != "target-writer" || info.HandoffTo != "" || info.HandoffID != "" {
161 t.Fatalf("published owner = %+v, want target without reservation", info)
162 }
163 target.Release()
164 }
165
166 func TestSessionLeaseHandoffPersistenceFailureKeepsOwner(t *testing.T) {
167 userPath, _ := leaseTestPath(t)
168 lease, err := TryAcquireSessionLease(userPath)
169 if err != nil {
170 t.Fatal(err)
171 }
172 defer lease.Release()
173 want := errors.New("disk full")
174 lease.beforeHandoffWrite = func() error { return want }
175 if err := lease.ReleaseForHandoff("target", "generation"); !errors.Is(err, want) {
176 t.Fatalf("ReleaseForHandoff = %v, want %v", err, want)
177 }
178 if !SessionLeaseHeldByCurrentRuntime(userPath) {
179 t.Fatal("failed handoff revoked the current owner")
180 }
181 if other, err := TryAcquireSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
182 if other != nil {
183 other.Release()
184 }
185 t.Fatalf("outside acquire after failed handoff = %v, want held", err)
186 }
187 }
188
189 func TestSessionLeaseExpiredHandoffAllowsPlainAcquire(t *testing.T) {
190 userPath, key := leaseTestPath(t)
191 lock, err := tryTakeSessionLeaseLock(key)
192 if err != nil {
193 t.Fatal(err)
194 }
195 info := newSessionLeaseInfo(key)
196 info.HandoffTo = "expired-target"
197 info.HandoffID = "expired-generation"
198 info.HandoffExpiresAt = time.Now().UTC().Add(-time.Second)
199 if err := writeSessionLeaseInfo(lock, info); err != nil {
200 lock.Unlock()
201 t.Fatal(err)
202 }
203 lock.Unlock()
204 lease, err := TryAcquireSessionLease(userPath)
205 if err != nil {
206 t.Fatalf("plain acquire after expiry: %v", err)
207 }
208 lease.Release()
209 }
210
211 func TestSessionLeaseLegacyMetadataDefaultsHandoffFields(t *testing.T) {
212 info, err := decodeSessionLeaseInfo([]byte(`{"session_path":"x","writer_id":"legacy","pid":1,"acquired_at":"2026-01-01T00:00:00Z"}`))
213 if err != nil {
214 t.Fatal(err)
215 }
216 if info.HandoffTo != "" || info.HandoffID != "" || !info.HandoffExpiresAt.IsZero() {
217 t.Fatalf("legacy metadata decoded with reservation: %+v", info)
218 }
219 }
220
221 func TestSessionLeaseMetadataOmitsEmptyHandoffAndIgnoresUnknownFields(t *testing.T) {
222 encoded, err := json.Marshal(SessionLeaseInfo{
223 SessionPath: "x", WriterID: "writer", PID: 1, AcquiredAt: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
224 })
225 if err != nil {
226 t.Fatal(err)
227 }
228 text := string(encoded)
229 for _, field := range []string{"handoff_to", "handoff_id", "handoff_expires_at"} {
230 if strings.Contains(text, field) {
231 t.Fatalf("empty %s was serialized: %s", field, text)
232 }
233 }
234 info, err := decodeSessionLeaseInfo([]byte(`{"session_path":"x","writer_id":"legacy","pid":1,"acquired_at":"2026-01-01T00:00:00Z","future_field":{"nested":true}}`))
235 if err != nil {
236 t.Fatal(err)
237 }
238 if info.WriterID != "legacy" || info.HandoffTo != "" || info.HandoffID != "" || !info.HandoffExpiresAt.IsZero() {
239 t.Fatalf("unknown-field metadata decoded incorrectly: %+v", info)
240 }
241 }
242
243 func TestSessionLeaseReclaimsCurrentProcessStaleOwner(t *testing.T) {
244 userPath, key := leaseTestPath(t)
245 sessionLeaseOwners.Store(key, struct{}{})
246 t.Cleanup(func() {
247 sessionLeaseOwners.Delete(key)
248 _ = os.Remove(sessionLeaseInfoPath(key))
249 })
250 if err := SaveSessionLeaseInfo(key, SessionLeaseInfo{
251 SessionPath: key,
252 WriterID: SessionWriterID(),
253 PID: os.Getpid(),
254 AcquiredAt: time.Now().UTC(),
255 }); err != nil {
256 t.Fatalf("SaveSessionLeaseInfo: %v", err)
257 }
258 if lease, err := TryAcquireSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
259 if lease != nil {
260 lease.Release()
261 }
262 t.Fatalf("TryAcquireSessionLease err = %v, want ErrSessionLeaseHeld", err)
263 }
264 lease, err := TryReclaimCurrentProcessSessionLease(userPath)
265 if err != nil {
266 t.Fatalf("TryReclaimCurrentProcessSessionLease: %v", err)
267 }
268 lease.Release()
269 }
270
271 func TestSessionLeaseReclaimsOrphanedEntryWithoutInfo(t *testing.T) {
272 // An orphaned in-process entry whose lease.json was deleted out from
273 // under it (manual cleanup, AV quarantine). Nothing actually holds the
274 // session — the OS lock is free — so reclaim must recover instead of
275 // wedging every rebuild as busy. Before the lock-arbiter rework this
276 // deadlocked: reclaim fell back to a plain acquire, which re-hit the
277 // orphaned map entry forever.
278 userPath, key := leaseTestPath(t)
279 sessionLeaseOwners.Store(key, uint64(1<<61))
280 t.Cleanup(func() { sessionLeaseOwners.Delete(key) })
281
282 if lease, err := TryAcquireSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
283 if lease != nil {
284 lease.Release()
285 }
286 t.Fatalf("TryAcquireSessionLease err = %v, want ErrSessionLeaseHeld", err)
287 }
288 lease, err := TryReclaimCurrentProcessSessionLease(userPath)
289 if err != nil {
290 t.Fatalf("TryReclaimCurrentProcessSessionLease without info: %v", err)
291 }
292 if _, err := LoadSessionLeaseInfo(userPath); err != nil {
293 t.Fatalf("reclaim should have rewritten lease info, load err = %v", err)
294 }
295 lease.Release()
296 }
297
298 func TestSessionLeaseReclaimsOrphanedEntryWithCorruptInfo(t *testing.T) {
299 // Same as above but the sidecar is torn (empty/undecodable) rather than
300 // missing: identity is unreadable, the lock is free, reclaim must win.
301 userPath, key := leaseTestPath(t)
302 sessionLeaseOwners.Store(key, uint64(1<<61))
303 t.Cleanup(func() {
304 sessionLeaseOwners.Delete(key)
305 _ = os.Remove(sessionLeaseInfoPath(key))
306 })
307 if err := os.WriteFile(sessionLeaseInfoPath(key), []byte("{torn"), 0o644); err != nil {
308 t.Fatalf("write corrupt lease info: %v", err)
309 }
310
311 lease, err := TryReclaimCurrentProcessSessionLease(userPath)
312 if err != nil {
313 t.Fatalf("TryReclaimCurrentProcessSessionLease with corrupt info: %v", err)
314 }
315 lease.Release()
316 }
317
318 func TestSessionLeaseReclaimRefusesForeignInfo(t *testing.T) {
319 // A readable info naming another runtime is never stolen by reclaim,
320 // even with the lock free — that separation belongs to
321 // SessionLeaseHeldByOtherRuntime's cleanup, not to reclaim.
322 userPath, key := leaseTestPath(t)
323 if err := SaveSessionLeaseInfo(key, SessionLeaseInfo{
324 SessionPath: key,
325 WriterID: "other-host-1234-deadbeef",
326 PID: os.Getpid() + 1,
327 AcquiredAt: time.Now().UTC(),
328 }); err != nil {
329 t.Fatalf("SaveSessionLeaseInfo: %v", err)
330 }
331 t.Cleanup(func() { _ = os.Remove(sessionLeaseInfoPath(key)) })
332
333 if lease, err := TryReclaimCurrentProcessSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
334 if lease != nil {
335 lease.Release()
336 }
337 t.Fatalf("TryReclaimCurrentProcessSessionLease err = %v, want ErrSessionLeaseHeld", err)
338 }
339 }
340
341 func TestSessionLeaseConcurrentReclaimSingleWinner(t *testing.T) {
342 userPath, key := leaseTestPath(t)
343 sessionLeaseOwners.Store(key, struct{}{})
344 t.Cleanup(func() {
345 sessionLeaseOwners.Delete(key)
346 _ = os.Remove(sessionLeaseInfoPath(key))
347 })
348 if err := SaveSessionLeaseInfo(key, SessionLeaseInfo{
349 SessionPath: key,
350 WriterID: SessionWriterID(),
351 PID: os.Getpid(),
352 AcquiredAt: time.Now().UTC(),
353 }); err != nil {
354 t.Fatalf("SaveSessionLeaseInfo: %v", err)
355 }
356
357 const attempts = 16
358 var wg sync.WaitGroup
359 leases := make(chan *SessionLease, attempts)
360 start := make(chan struct{})
361 for range attempts {
362 wg.Go(func() {
363 <-start
364 if lease, err := TryReclaimCurrentProcessSessionLease(userPath); err == nil && lease != nil {
365 leases <- lease
366 }
367 })
368 }
369 close(start)
370 wg.Wait()
371 close(leases)
372
373 var won []*SessionLease
374 for lease := range leases {
375 won = append(won, lease)
376 }
377 if len(won) != 1 {
378 t.Fatalf("concurrent reclaim produced %d leases, want exactly 1", len(won))
379 }
380 // The losers must not have evicted the winner's owner entry.
381 if _, ok := sessionLeaseOwners.Load(key); !ok {
382 t.Fatal("winner's owner entry was evicted by a failed concurrent reclaim")
383 }
384 if lease, err := TryAcquireSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
385 if lease != nil {
386 lease.Release()
387 }
388 t.Fatalf("TryAcquireSessionLease while reclaimed lease is held err = %v, want ErrSessionLeaseHeld", err)
389 }
390 won[0].Release()
391 lease, err := TryAcquireSessionLease(userPath)
392 if err != nil {
393 t.Fatalf("TryAcquireSessionLease after release: %v", err)
394 }
395 lease.Release()
396 }
397
398 func TestSessionLeaseReclaimRefusesActiveHolder(t *testing.T) {
399 userPath, key := leaseTestPath(t)
400 holder, err := TryAcquireSessionLease(userPath)
401 if err != nil {
402 t.Fatalf("TryAcquireSessionLease: %v", err)
403 }
404 defer holder.Release()
405
406 if lease, err := TryReclaimCurrentProcessSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
407 if lease != nil {
408 lease.Release()
409 }
410 t.Fatalf("TryReclaimCurrentProcessSessionLease err = %v, want ErrSessionLeaseHeld", err)
411 }
412 // The failed reclaim must leave the holder's owner entry intact.
413 if _, ok := sessionLeaseOwners.Load(key); !ok {
414 t.Fatal("active holder's owner entry was evicted by a failed reclaim")
415 }
416 if lease, err := TryAcquireSessionLease(userPath); !errors.Is(err, ErrSessionLeaseHeld) {
417 if lease != nil {
418 lease.Release()
419 }
420 t.Fatalf("TryAcquireSessionLease err = %v, want ErrSessionLeaseHeld", err)
421 }
422 }
423
424 func TestSessionLeaseReclaimAfterHolderReleased(t *testing.T) {
425 userPath, _ := leaseTestPath(t)
426 holder, err := TryAcquireSessionLease(userPath)
427 if err != nil {
428 t.Fatalf("TryAcquireSessionLease: %v", err)
429 }
430 holder.Release()
431
432 // The holder released between the caller's failed acquire and the
433 // reclaim: the lease info file is gone and the lock is free, so the
434 // reclaim must win the lease cleanly.
435 lease, err := TryReclaimCurrentProcessSessionLease(userPath)
436 if err != nil {
437 t.Fatalf("TryReclaimCurrentProcessSessionLease after release: %v", err)
438 }
439 lease.Release()
440 }
441
442 func TestSessionLeaseStaleReleaseKeepsNewOwnerEntry(t *testing.T) {
443 userPath, key := leaseTestPath(t)
444 stale, err := TryAcquireSessionLease(userPath)
445 if err != nil {
446 t.Fatalf("TryAcquireSessionLease: %v", err)
447 }
448 // Simulate a reclaim that took over the entry while the stale lease was
449 // still alive: the map now names a different owner.
450 sessionLeaseOwners.Store(key, uint64(1<<62))
451 t.Cleanup(func() { sessionLeaseOwners.Delete(key) })
452
453 stale.Release()
454 if _, ok := sessionLeaseOwners.Load(key); !ok {
455 t.Fatal("stale Release evicted the new owner's entry")
456 }
457 }
458
459 func TestSessionLeaseHeldByOtherRuntime(t *testing.T) {
460 t.Run("no lease", func(t *testing.T) {
461 userPath, _ := leaseTestPath(t)
462 if SessionLeaseHeldByOtherRuntime(userPath) {
463 t.Fatal("unheld session reported as held by another runtime")
464 }
465 })
466 t.Run("held by this process", func(t *testing.T) {
467 userPath, _ := leaseTestPath(t)
468 lease, err := TryAcquireSessionLease(userPath)
469 if err != nil {
470 t.Fatalf("TryAcquireSessionLease: %v", err)
471 }
472 defer lease.Release()
473 if SessionLeaseHeldByOtherRuntime(userPath) {
474 t.Fatal("own lease reported as held by another runtime")
475 }
476 })
477 t.Run("foreign info with live lock", func(t *testing.T) {
478 userPath, key := leaseTestPath(t)
479 unlock, err := tryLockSessionLeaseFile(key)
480 if err != nil {
481 t.Fatalf("tryLockSessionLeaseFile: %v", err)
482 }
483 defer unlock()
484 if err := SaveSessionLeaseInfo(key, SessionLeaseInfo{
485 SessionPath: key,
486 WriterID: "other-host-1234-deadbeef",
487 PID: os.Getpid() + 1,
488 AcquiredAt: time.Now().UTC(),
489 }); err != nil {
490 t.Fatalf("SaveSessionLeaseInfo: %v", err)
491 }
492 t.Cleanup(func() { _ = os.Remove(sessionLeaseInfoPath(key)) })
493 if !SessionLeaseHeldByOtherRuntime(userPath) {
494 t.Fatal("foreign-held session not reported as held by another runtime")
495 }
496 })
497 t.Run("foreign info from crashed process", func(t *testing.T) {
498 userPath, key := leaseTestPath(t)
499 if err := SaveSessionLeaseInfo(key, SessionLeaseInfo{
500 SessionPath: key,
501 WriterID: "other-host-1234-deadbeef",
502 PID: os.Getpid() + 1,
503 AcquiredAt: time.Now().UTC(),
504 }); err != nil {
505 t.Fatalf("SaveSessionLeaseInfo: %v", err)
506 }
507 t.Cleanup(func() { _ = os.Remove(sessionLeaseInfoPath(key)) })
508 // Info file left behind but the lock is free: the holder crashed, so
509 // the session is not considered held.
510 if SessionLeaseHeldByOtherRuntime(userPath) {
511 t.Fatal("crashed holder's leftover info reported as held")
512 }
513 if _, err := os.Stat(sessionLeaseInfoPath(key)); !os.IsNotExist(err) {
514 t.Fatalf("crashed holder's leftover info should be removed, stat err = %v", err)
515 }
516 })
517 t.Run("corrupt info from crashed process", func(t *testing.T) {
518 userPath, key := leaseTestPath(t)
519 if err := os.WriteFile(sessionLeaseInfoPath(key), nil, 0o644); err != nil {
520 t.Fatalf("write corrupt lease info: %v", err)
521 }
522 if SessionLeaseHeldByOtherRuntime(userPath) {
523 t.Fatal("corrupt crashed holder info reported as held")
524 }
525 if _, err := os.Stat(sessionLeaseInfoPath(key)); !os.IsNotExist(err) {
526 t.Fatalf("corrupt lease info should be removed, stat err = %v", err)
527 }
528 })
529 }
530
531 func TestSessionLeaseHeldByCurrentRuntime(t *testing.T) {
532 userPath, _ := leaseTestPath(t)
533 if SessionLeaseHeldByCurrentRuntime(userPath) {
534 t.Fatal("unheld session reported as owned by the current runtime")
535 }
536 lease, err := TryAcquireSessionLease(userPath)
537 if err != nil {
538 t.Fatalf("TryAcquireSessionLease: %v", err)
539 }
540 if !SessionLeaseHeldByCurrentRuntime(userPath) {
541 lease.Release()
542 t.Fatal("held session was not reported as owned by the current runtime")
543 }
544 lease.Release()
545 if SessionLeaseHeldByCurrentRuntime(userPath) {
546 t.Fatal("released session remained owned by the current runtime")
547 }
548 }
549
550 func TestSessionLeaseHeldByCurrentRuntimeRejectsPendingReservation(t *testing.T) {
551 userPath, key := leaseTestPath(t)
552 ownerID := sessionLeaseSeq.Add(1)
553 sessionLeaseOwners.Store(key, ownerID)
554 t.Cleanup(func() {
555 sessionLeaseOwners.CompareAndDelete(key, ownerID)
556 sessionLeaseActiveOwners.CompareAndDelete(key, ownerID)
557 })
558
559 if SessionLeaseHeldByCurrentRuntime(userPath) {
560 t.Fatal("pending acquisition reservation authorized ownership-sensitive repair")
561 }
562 }
563
564 func TestSessionLeaseReleaseRevokesRepairAuthorizationBeforeUnlock(t *testing.T) {
565 userPath, _ := leaseTestPath(t)
566 lease, err := TryAcquireSessionLease(userPath)
567 if err != nil {
568 t.Fatalf("TryAcquireSessionLease: %v", err)
569 }
570 checked := false
571 lease.beforeReleaseLock = func() {
572 checked = true
573 if SessionLeaseHeldByCurrentRuntime(userPath) {
574 t.Error("release kept repair authorization active while unlocking the OS lease")
575 }
576 }
577
578 lease.Release()
579 if !checked {
580 t.Fatal("release did not invoke the controlled unlock")
581 }
582 }
583
584 func TestSessionLeaseReleaseRetiresLockSidecars(t *testing.T) {
585 userPath, key := leaseTestPath(t)
586 lease, err := TryAcquireSessionLease(userPath)
587 if err != nil {
588 t.Fatalf("TryAcquireSessionLease: %v", err)
589 }
590 leaseLock := store.SessionLeaseLock(key)
591 if _, err := os.Stat(leaseLock); err != nil {
592 t.Fatalf("lease lock should exist while held: %v", err)
593 }
594 lease.Release()
595 if _, err := os.Stat(leaseLock); !os.IsNotExist(err) {
596 t.Fatalf("lease lock should be retired on release, stat err = %v", err)
597 }
598 if _, err := os.Stat(store.SessionLockFile(key)); !os.IsNotExist(err) {
599 t.Fatalf("save lock should be retired on release, stat err = %v", err)
600 }
601
602 // A release racing a live successor must not strip the successor's lock.
603 first, err := TryAcquireSessionLease(userPath)
604 if err != nil {
605 t.Fatalf("reacquire: %v", err)
606 }
607 second, err := TryAcquireSessionLease(userPath)
608 if !errors.Is(err, ErrSessionLeaseHeld) {
609 if second != nil {
610 second.Release()
611 }
612 t.Fatalf("second acquire err = %v, want ErrSessionLeaseHeld", err)
613 }
614 if _, err := os.Stat(leaseLock); err != nil {
615 t.Fatalf("holder's lease lock must survive a failed acquire: %v", err)
616 }
617 first.Release()
618 }
619
619 lines GO