返回 DeepSeek-Reasonix
session_key_test.go
根目录 / desktop / session_key_test.go
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "runtime"
7 "strings"
8 "testing"
9
10 "reasonix/internal/agent"
11 "reasonix/internal/control"
12 )
13
14 // The lease registry folds session paths through agent.CanonicalSessionPath
15 // (lowercased on Windows). Every desktop "same session" comparison must fold
16 // the same way, or a lease the tab itself holds looks foreign and every
17 // model/effort/token switch self-locks with the "already open in another
18 // window" error (#5999, #6006, #5996).
19
20 func TestSessionRuntimeKeyMatchesLeaseKey(t *testing.T) {
21 dir := t.TempDir()
22 // Mixed-case segment on every platform; on Windows the lease key folds it.
23 path := filepath.Join(dir, "Sessions-Dir", "20260705-Test.jsonl")
24 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
25 t.Fatalf("mkdir: %v", err)
26 }
27
28 lease, err := agent.TryAcquireSessionLease(path)
29 if err != nil {
30 t.Fatalf("acquire lease: %v", err)
31 }
32 defer lease.Release()
33
34 if got, want := sessionRuntimeKey(lease.Path()), sessionRuntimeKey(path); got != want {
35 t.Fatalf("lease path key %q != session path key %q; lease reuse checks will self-lock", got, want)
36 }
37 }
38
39 func TestSessionRuntimeKeyIsIdempotent(t *testing.T) {
40 dir := t.TempDir()
41 path := filepath.Join(dir, "Case-Mix", "20260705-Test.jsonl")
42 key := sessionRuntimeKey(path)
43 if key == "" {
44 t.Fatal("empty key for non-empty path")
45 }
46 if again := sessionRuntimeKey(key); again != key {
47 t.Fatalf("sessionRuntimeKey not idempotent: %q -> %q", key, again)
48 }
49 }
50
51 func TestSessionRuntimeKeyFoldsCaseOnWindows(t *testing.T) {
52 if runtime.GOOS != "windows" {
53 t.Skip("case-insensitive path identity is Windows-only")
54 }
55 dir := t.TempDir()
56 path := filepath.Join(dir, "Sessions", "20260705-Test.jsonl")
57 upper := strings.ToUpper(path)
58 if sessionRuntimeKey(path) != sessionRuntimeKey(upper) {
59 t.Fatalf("case variants of one file produced distinct keys: %q vs %q",
60 sessionRuntimeKey(path), sessionRuntimeKey(upper))
61 }
62 }
63
64 func TestCanonicalTabSessionPathNormalizesOutsideSessionDir(t *testing.T) {
65 // Project-scope sessions live outside config.SessionDir(); the fallback
66 // must still clean the shape so one file cannot split into two keys.
67 dir := t.TempDir()
68 base := filepath.Join(dir, "projects", "p1", "sessions", "20260705-a.jsonl")
69 messy := filepath.Join(dir, "projects", "p1", ".", "sessions") + string(filepath.Separator) + "20260705-a.jsonl"
70 if sessionRuntimeKey(base) != sessionRuntimeKey(messy) {
71 t.Fatalf("path shape variants split keys: %q vs %q",
72 sessionRuntimeKey(base), sessionRuntimeKey(messy))
73 }
74 }
75
76 func TestEnsureSessionLeaseReusesHeldLease(t *testing.T) {
77 dir := t.TempDir()
78 path := filepath.Join(dir, "Sessions", "20260705-Reuse.jsonl")
79 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
80 t.Fatalf("mkdir: %v", err)
81 }
82
83 tab := &WorkspaceTab{ID: "tab_reuse"}
84 if err := tab.ensureSessionLease(path); err != nil {
85 t.Fatalf("first ensure: %v", err)
86 }
87 defer tab.releaseSessionLease()
88
89 acquires := 0
90 sessionLeaseAcquireHookForTest = func() { acquires++ }
91 defer func() { sessionLeaseAcquireHookForTest = nil }()
92
93 // Same path again — and the lease's own (canonical) form: both must hit
94 // the fast path instead of re-acquiring against our own registry entry.
95 if err := tab.ensureSessionLease(path); err != nil {
96 t.Fatalf("re-ensure same path: %v", err)
97 }
98 if err := tab.ensureSessionLease(tab.sessionLeaseRuntimeKey()); err != nil {
99 t.Fatalf("re-ensure canonical form: %v", err)
100 }
101 if acquires != 0 {
102 t.Fatalf("expected fast-path reuse, got %d new acquires", acquires)
103 }
104 }
105
106 func TestCanReclaimAllowsMissingLeaseInfo(t *testing.T) {
107 // lease.json deleted or torn: Info is nil but nothing actually holds the
108 // session. The reclaim attempt must be allowed — the OS lock arbitrates —
109 // instead of wedging the session as busy on missing metadata (#5999's
110 // fresh-install variant).
111 app := NewApp()
112 tab := &WorkspaceTab{ID: "tab_nil_info"}
113 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
114 path := filepath.Join(t.TempDir(), "Sessions", "20260705-a.jsonl")
115
116 errNil := &agent.SessionLeaseError{Path: path}
117 if !app.canReclaimCurrentProcessSessionLease(tab, path, errNil) {
118 t.Fatal("nil-Info lease error must allow a reclaim attempt")
119 }
120
121 foreign := &agent.SessionLeaseError{Path: path, Info: &agent.SessionLeaseInfo{
122 SessionPath: path, WriterID: "other-host-1-deadbeef", PID: os.Getpid() + 1,
123 }}
124 if app.canReclaimCurrentProcessSessionLease(tab, path, foreign) {
125 t.Fatal("foreign-holder lease error must not allow reclaim")
126 }
127 }
128
129 func TestCanReclaimRejectsDetachedRuntimeOwner(t *testing.T) {
130 app := NewApp()
131 tab := &WorkspaceTab{ID: "visible"}
132 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
133 path := filepath.Join(t.TempDir(), "Sessions", "detached-owner.jsonl")
134 ctrl := control.New(control.Options{
135 SessionDir: filepath.Dir(path),
136 SessionPath: path,
137 Label: "detached",
138 })
139 t.Cleanup(ctrl.Close)
140 app.detachedSessions[sessionRuntimeKey(path)] = &WorkspaceTab{
141 ID: "detached",
142 SessionPath: path,
143 Ctrl: ctrl,
144 }
145 err := &agent.SessionLeaseError{Path: path, Info: &agent.SessionLeaseInfo{
146 SessionPath: path,
147 WriterID: agent.SessionWriterID(),
148 PID: os.Getpid(),
149 }}
150
151 if app.canReclaimCurrentProcessSessionLease(tab, path, err) {
152 t.Fatal("detached runtime owner must block same-process lease reclaim")
153 }
154 }
155
156 func TestSessionLeaseBusyErrorOmitsSettingClause(t *testing.T) {
157 generic := (&sessionLeaseBusyError{}).Error()
158 if strings.Contains(generic, "before changing") {
159 t.Fatalf("empty-setting busy error still names a setting: %q", generic)
160 }
161 withSetting := (&sessionLeaseBusyError{setting: "model"}).Error()
162 if !strings.Contains(withSetting, "before changing model") {
163 t.Fatalf("setting busy error lost its clause: %q", withSetting)
164 }
165 }
166
166 lines GO