返回 DeepSeek-Reasonix
session_removal_test.go
根目录 / internal / agent / session_removal_test.go
1 package agent
2
3 import (
4 "errors"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "reasonix/internal/provider"
10 "reasonix/internal/store"
11 )
12
13 func TestSessionRemovalGuardBlocksWhileLeaseHeld(t *testing.T) {
14 dir := t.TempDir()
15 path := filepath.Join(dir, "session.jsonl")
16 s := NewSession("sys")
17 s.Add(provider.Message{Role: provider.RoleUser, Content: "work"})
18 if err := s.SaveSnapshot(path); err != nil {
19 t.Fatalf("SaveSnapshot: %v", err)
20 }
21
22 lease, err := TryAcquireSessionLease(path)
23 if err != nil {
24 t.Fatalf("TryAcquireSessionLease: %v", err)
25 }
26 defer lease.Release()
27
28 if _, err := TryAcquireSessionRemovalGuard(path); !errors.Is(err, ErrSessionLeaseHeld) {
29 t.Fatalf("guard under live lease err = %v, want ErrSessionLeaseHeld", err)
30 }
31 if _, err := os.Stat(store.SessionLeaseLock(path)); err != nil {
32 t.Fatalf("lease lock disturbed by failed guard: %v", err)
33 }
34
35 lease.Release()
36 guard, err := TryAcquireSessionRemovalGuard(path)
37 if err != nil {
38 t.Fatalf("guard after release: %v", err)
39 }
40 // While the guard holds both locks, no new lease can be acquired: this is
41 // the window that used to allow probe-then-delete races.
42 if _, err := TryAcquireSessionLease(path); !errors.Is(err, ErrSessionLeaseHeld) {
43 guard.Release()
44 t.Fatalf("lease acquired while removal guard held, err = %v", err)
45 }
46 if err := guard.RemoveSidecarsAndRelease(); err != nil {
47 t.Fatalf("RemoveSidecarsAndRelease: %v", err)
48 }
49 for _, p := range []string{
50 store.SessionLockFile(path),
51 store.SessionLeaseLock(path),
52 store.SessionLeaseInfo(path),
53 } {
54 if _, err := os.Stat(p); !os.IsNotExist(err) {
55 t.Errorf("sidecar survived removal: %s (err=%v)", p, err)
56 }
57 }
58 // The path is free again for a normal acquire afterwards.
59 after, err := TryAcquireSessionLease(path)
60 if err != nil {
61 t.Fatalf("lease after removal: %v", err)
62 }
63 after.Release()
64 }
65
66 func TestSessionRemovalGuardReleaseKeepsSidecars(t *testing.T) {
67 dir := t.TempDir()
68 path := filepath.Join(dir, "session.jsonl")
69 guard, err := TryAcquireSessionRemovalGuard(path)
70 if err != nil {
71 t.Fatalf("guard: %v", err)
72 }
73 guard.Release()
74 // Abort path: locks released, files left in place for the next owner.
75 if _, err := os.Stat(store.SessionLeaseLock(path)); err != nil {
76 t.Fatalf("lease lock missing after abort: %v", err)
77 }
78 lease, err := TryAcquireSessionLease(path)
79 if err != nil {
80 t.Fatalf("lease after abort: %v", err)
81 }
82 lease.Release()
83 }
84
85 func TestSessionLeaseConvertsToRemovalGuardWithoutOwnershipGap(t *testing.T) {
86 dir := t.TempDir()
87 path := filepath.Join(dir, "session.jsonl")
88 lease, err := TryAcquireSessionLease(path)
89 if err != nil {
90 t.Fatalf("TryAcquireSessionLease: %v", err)
91 }
92 guard, err := lease.TryConvertToRemovalGuard()
93 if err != nil {
94 lease.Release()
95 t.Fatalf("TryConvertToRemovalGuard: %v", err)
96 }
97 defer guard.Release()
98
99 // Conversion consumes the original lease but retains its exact lease lock,
100 // so no competing runtime can acquire in the handoff window.
101 lease.Release()
102 if next, err := TryAcquireSessionLease(path); !errors.Is(err, ErrSessionLeaseHeld) {
103 if next != nil {
104 next.Release()
105 }
106 t.Fatalf("TryAcquireSessionLease during converted guard err = %v, want ErrSessionLeaseHeld", err)
107 }
108 if err := guard.RemoveSidecarsAndRelease(); err != nil {
109 t.Fatalf("RemoveSidecarsAndRelease: %v", err)
110 }
111 if next, err := TryAcquireSessionLease(path); err != nil {
112 t.Fatalf("TryAcquireSessionLease after guard release: %v", err)
113 } else {
114 next.Release()
115 }
116 }
117
118 func TestConvertedRemovalGuardRestoresSessionLease(t *testing.T) {
119 dir := t.TempDir()
120 path := filepath.Join(dir, "session.jsonl")
121 lease, err := TryAcquireSessionLease(path)
122 if err != nil {
123 t.Fatalf("TryAcquireSessionLease: %v", err)
124 }
125 guard, err := lease.TryConvertToRemovalGuard()
126 if err != nil {
127 lease.Release()
128 t.Fatalf("TryConvertToRemovalGuard: %v", err)
129 }
130 restored, err := guard.RestoreSessionLease()
131 if err != nil {
132 guard.Release()
133 t.Fatalf("RestoreSessionLease: %v", err)
134 }
135 defer restored.Release()
136 if !SessionLeaseHeldByCurrentRuntime(path) {
137 t.Fatal("restored lease is not registered as current runtime ownership")
138 }
139 if next, err := TryAcquireSessionLease(path); !errors.Is(err, ErrSessionLeaseHeld) {
140 if next != nil {
141 next.Release()
142 }
143 t.Fatalf("TryAcquireSessionLease during restored lease err = %v, want ErrSessionLeaseHeld", err)
144 }
145 restored.Release()
146 if next, err := TryAcquireSessionLease(path); err != nil {
147 t.Fatalf("TryAcquireSessionLease after restored lease release: %v", err)
148 } else {
149 next.Release()
150 }
151 }
152
153 func TestSessionLeaseConversionFailureKeepsLeaseActive(t *testing.T) {
154 dir := t.TempDir()
155 path := filepath.Join(dir, "session.jsonl")
156 lease, err := TryAcquireSessionLease(path)
157 if err != nil {
158 t.Fatalf("TryAcquireSessionLease: %v", err)
159 }
160 defer lease.Release()
161 saveLock, err := tryTakeSessionLockFile(store.SessionLockFile(path))
162 if err != nil {
163 t.Fatalf("take save lock: %v", err)
164 }
165 defer saveLock.Unlock()
166
167 if guard, err := lease.TryConvertToRemovalGuard(); !errors.Is(err, ErrSessionLeaseHeld) {
168 if guard != nil {
169 guard.Release()
170 }
171 t.Fatalf("TryConvertToRemovalGuard under save lock err = %v, want ErrSessionLeaseHeld", err)
172 }
173 if !SessionLeaseHeldByCurrentRuntime(path) {
174 t.Fatal("failed conversion revoked the original runtime lease")
175 }
176 if next, err := TryAcquireSessionLease(path); !errors.Is(err, ErrSessionLeaseHeld) {
177 if next != nil {
178 next.Release()
179 }
180 t.Fatalf("competing lease after failed conversion err = %v, want ErrSessionLeaseHeld", err)
181 }
182 }
183
183 lines GO