返回 DeepSeek-Reasonix
session_lease_keeper_test.go
根目录 / internal / control / session_lease_keeper_test.go
1 package control
2
3 import (
4 "errors"
5 "os"
6 "path/filepath"
7 "strconv"
8 "strings"
9 "testing"
10 "time"
11
12 "reasonix/internal/agent"
13 "reasonix/internal/store"
14 )
15
16 func TestSessionLeaseKeeperRebindMovesLease(t *testing.T) {
17 dir := t.TempDir()
18 a := filepath.Join(dir, "a.jsonl")
19 b := filepath.Join(dir, "b.jsonl")
20
21 k := NewSessionLeaseKeeper()
22 defer k.Release()
23
24 if err := k.Rebind(a); err != nil {
25 t.Fatalf("Rebind(a): %v", err)
26 }
27 if got, want := k.HeldPath(), agent.CanonicalSessionPath(a); got != want {
28 t.Fatalf("HeldPath = %q, want %q", got, want)
29 }
30 if _, err := os.Stat(store.SessionLeaseInfo(agent.CanonicalSessionPath(a))); err != nil {
31 t.Fatalf("lease info for a missing: %v", err)
32 }
33 // a is held: an outside acquire must fail.
34 if _, err := agent.TryAcquireSessionLease(a); !errors.Is(err, agent.ErrSessionLeaseHeld) {
35 t.Fatalf("TryAcquireSessionLease(a) while kept = %v, want ErrSessionLeaseHeld", err)
36 }
37
38 if err := k.Rebind(b); err != nil {
39 t.Fatalf("Rebind(b): %v", err)
40 }
41 if got, want := k.HeldPath(), agent.CanonicalSessionPath(b); got != want {
42 t.Fatalf("HeldPath after rebind = %q, want %q", got, want)
43 }
44 // The old lease is released: a is acquirable again and its info is gone.
45 if _, err := os.Stat(store.SessionLeaseInfo(agent.CanonicalSessionPath(a))); !os.IsNotExist(err) {
46 t.Fatalf("lease info for a after rebind stat err = %v, want not exist", err)
47 }
48 lease, err := agent.TryAcquireSessionLease(a)
49 if err != nil {
50 t.Fatalf("TryAcquireSessionLease(a) after rebind: %v", err)
51 }
52 lease.Release()
53 }
54
55 func TestSessionLeaseKeeperRebindSamePathIsNoop(t *testing.T) {
56 dir := t.TempDir()
57 a := filepath.Join(dir, "a.jsonl")
58
59 k := NewSessionLeaseKeeper()
60 defer k.Release()
61 if err := k.Rebind(a); err != nil {
62 t.Fatalf("Rebind(a): %v", err)
63 }
64 // Same canonical path again must not trip over the keeper's own lease.
65 if err := k.Rebind(a); err != nil {
66 t.Fatalf("Rebind(a) again: %v", err)
67 }
68 if got, want := k.HeldPath(), agent.CanonicalSessionPath(a); got != want {
69 t.Fatalf("HeldPath = %q, want %q", got, want)
70 }
71 }
72
73 func TestSessionLeaseKeeperRefusesHeldPathAndKeepsCurrent(t *testing.T) {
74 dir := t.TempDir()
75 a := filepath.Join(dir, "a.jsonl")
76 b := filepath.Join(dir, "b.jsonl")
77
78 holder, err := agent.TryAcquireSessionLease(b)
79 if err != nil {
80 t.Fatalf("holder acquire: %v", err)
81 }
82 defer holder.Release()
83
84 k := NewSessionLeaseKeeper()
85 defer k.Release()
86 if err := k.Rebind(a); err != nil {
87 t.Fatalf("Rebind(a): %v", err)
88 }
89 err = k.Rebind(b)
90 if !errors.Is(err, agent.ErrSessionLeaseHeld) {
91 t.Fatalf("Rebind(held b) = %v, want ErrSessionLeaseHeld", err)
92 }
93 // Failure leaves the keeper on its previous session.
94 if got, want := k.HeldPath(), agent.CanonicalSessionPath(a); got != want {
95 t.Fatalf("HeldPath after refused rebind = %q, want %q", got, want)
96 }
97 }
98
99 func TestSessionLeaseKeeperEmptyPathReleases(t *testing.T) {
100 dir := t.TempDir()
101 a := filepath.Join(dir, "a.jsonl")
102
103 k := NewSessionLeaseKeeper()
104 defer k.Release()
105 if err := k.Rebind(a); err != nil {
106 t.Fatalf("Rebind(a): %v", err)
107 }
108 if err := k.Rebind(""); err != nil {
109 t.Fatalf("Rebind(empty): %v", err)
110 }
111 if got := k.HeldPath(); got != "" {
112 t.Fatalf("HeldPath after empty rebind = %q, want empty", got)
113 }
114 lease, err := agent.TryAcquireSessionLease(a)
115 if err != nil {
116 t.Fatalf("TryAcquireSessionLease(a) after empty rebind: %v", err)
117 }
118 lease.Release()
119 }
120
121 func TestSessionLeaseKeeperReleaseRemovesLeaseInfo(t *testing.T) {
122 dir := t.TempDir()
123 a := filepath.Join(dir, "a.jsonl")
124
125 k := NewSessionLeaseKeeper()
126 if err := k.Rebind(a); err != nil {
127 t.Fatalf("Rebind(a): %v", err)
128 }
129 k.Release()
130 k.Release() // idempotent
131 if _, err := os.Stat(store.SessionLeaseInfo(agent.CanonicalSessionPath(a))); !os.IsNotExist(err) {
132 t.Fatalf("lease info after Release stat err = %v, want not exist", err)
133 }
134 if got := k.HeldPath(); got != "" {
135 t.Fatalf("HeldPath after Release = %q, want empty", got)
136 }
137 }
138
139 func TestSessionInUseMessageNamesHolder(t *testing.T) {
140 acquired := time.Date(2026, 7, 6, 3, 4, 0, 0, time.UTC)
141 err := &agent.SessionLeaseError{
142 Path: "/tmp/x.jsonl",
143 Info: &agent.SessionLeaseInfo{
144 SessionPath: "/tmp/x.jsonl",
145 WriterID: "writer-nonce-should-not-appear",
146 PID: 12345,
147 Hostname: "devbox",
148 AcquiredAt: acquired,
149 },
150 }
151 msg := SessionInUseMessage(err)
152 if !strings.Contains(msg, "another Reasonix process") {
153 t.Fatalf("message %q missing holder wording", msg)
154 }
155 if !strings.Contains(msg, "pid 12345") || !strings.Contains(msg, "on devbox") {
156 t.Fatalf("message %q missing pid/host", msg)
157 }
158 if !strings.Contains(msg, "since "+acquired.Local().Format("15:04")) {
159 t.Fatalf("message %q missing local acquire time", msg)
160 }
161 if strings.Contains(msg, "writer-nonce-should-not-appear") {
162 t.Fatalf("message %q leaks the writer id", msg)
163 }
164 if strings.Contains(msg, "/tmp/x.jsonl") {
165 t.Fatalf("message %q leaks the session path", msg)
166 }
167 }
168
169 func TestSessionInUseMessageFallsBackWithoutInfo(t *testing.T) {
170 for name, err := range map[string]error{
171 "nil info": &agent.SessionLeaseError{Path: "/tmp/x.jsonl"},
172 "plain held": agent.ErrSessionLeaseHeld,
173 "zero pid": &agent.SessionLeaseError{Info: &agent.SessionLeaseInfo{PID: 0}},
174 } {
175 msg := SessionInUseMessage(err)
176 if msg != "this session is in use by another Reasonix window or process" {
177 t.Fatalf("%s: message = %q, want generic fallback", name, msg)
178 }
179 if strings.Contains(msg, "pid "+strconv.Itoa(os.Getpid())) {
180 t.Fatalf("%s: fallback should not invent a pid: %q", name, msg)
181 }
182 }
183 }
184
184 lines GO