返回 DeepSeek-Reasonix
session_identity_compatibility_test.go
根目录 / desktop / session_identity_compatibility_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "os"
6 "testing"
7 "time"
8
9 "reasonix/internal/agent"
10 "reasonix/internal/control"
11 "reasonix/internal/event"
12 "reasonix/internal/session"
13 "reasonix/internal/taskmonitor"
14 )
15
16 func TestAuditCanonicalRestoreReturnsToSidebar(t *testing.T) {
17 isolateDesktopUserDirs(t)
18 app := NewApp()
19 app.ctx = t.Context()
20 pinDesktopSessionRoot(t, app)
21 installNoopRuntimeEvents(app, nil)
22 root := globalWorkspaceRoot()
23 dir := desktopSessionDir(root)
24 if err := os.MkdirAll(dir, 0755); err != nil {
25 t.Fatal(err)
26 }
27 service := app.desktopSessionService("")
28 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "restore-audit", CWD: root, Origin: session.SessionOriginNew})
29 if err != nil {
30 t.Fatal(err)
31 }
32 ref := runtime.Ref()
33 payload, _ := json.Marshal(map[string]any{"message": map[string]any{"id": "user-1", "role": "user", "content": "preserved after restore"}})
34 if _, err := runtime.Session().AppendBatch(t.Context(), "turn", []session.Event{{Kind: "message/complete", Payload: payload}}); err != nil {
35 t.Fatal(err)
36 }
37 if _, err := runtime.Session().Flush(t.Context()); err != nil {
38 t.Fatal(err)
39 }
40 workspaceID, err := app.attachDesktopSession(t.Context(), "global", "", ref)
41 if err != nil {
42 t.Fatal(err)
43 }
44 if err := service.Close(t.Context(), ref); err != nil {
45 t.Fatal(err)
46 }
47 installSessionCatalogForTest(t, app, dir, "global", "")
48 if err := app.ArchiveCanonicalSession(ref); err != nil {
49 t.Fatal(err)
50 }
51 archived, err := app.ListWorkspaceSessions(workspaceID, "", "", 10, true)
52 if err != nil || len(archived.Sessions) != 1 || !archived.Sessions[0].Archived {
53 t.Fatalf("archive fixture: %+v, %v", archived, err)
54 }
55 if err := app.RestoreCanonicalSession(ref); err != nil {
56 t.Fatal(err)
57 }
58 reconcileSessionCatalogForTest(t, app, dir, "global", "")
59 page, err := app.ListWorkspaceSessions(workspaceID, "", "", 10, true)
60 if err != nil || len(page.Sessions) != 1 || page.Sessions[0].Archived {
61 t.Fatalf("restore registry: %+v, %v", page, err)
62 }
63 history, err := app.ReadSessionHistory(ref, "", 10)
64 if err != nil || len(history.Messages) != 1 || history.Messages[0].Content != "preserved after restore" {
65 t.Fatalf("restore history: %+v, %v", history, err)
66 }
67 t.Log("restore removed archive marker and preserved the full test history")
68 if deleted := app.ListTrashedSessions(); len(deleted) != 0 {
69 t.Fatalf("unexpected trash: %+v", deleted)
70 }
71 topics, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 10})
72 if err != nil {
73 t.Fatal(err)
74 }
75 if len(topics.Items) == 0 {
76 t.Fatal("restore succeeded and history still exists, but sidebar has no restored session; archive and deleted lists are both empty")
77 }
78 }
79
80 func auditMigratedTab(t *testing.T) (*App, *WorkspaceTab, *control.Controller, string) {
81 t.Helper()
82 isolateDesktopUserDirs(t)
83 model, _ := configureSwitchableDefaultModels(t)
84 app := NewApp()
85 app.ctx = t.Context()
86 app.readyHook = func() {}
87 pinDesktopSessionRoot(t, app)
88 root := canonicalRuntimeRoot(t.TempDir())
89 dir := desktopSessionDir(root)
90 if err := os.MkdirAll(dir, 0755); err != nil {
91 t.Fatal(err)
92 }
93 if err := addProject(root, ""); err != nil {
94 t.Fatal(err)
95 }
96 if err := setTopicTitle(root, "audit-topic", "Audit"); err != nil {
97 t.Fatal(err)
98 }
99 path := writeTopicSessionWithPrompt(t, dir, "audit.jsonl", "audit-topic", "Audit", root, "preserved history", time.Now())
100 ctrl := control.New(control.Options{
101 WorkspaceRoot: root, SessionDir: dir,
102 Executor: agent.New(nil, nil, agent.NewSession("test"), agent.Options{}, event.Discard),
103 SessionService: app.desktopSessionService(dir), ExclusiveSession: true,
104 OnSessionRotation: app.prepareDesktopSessionRotation,
105 })
106 t.Cleanup(ctrl.Close)
107 ref, err := ctrl.ContinueLegacySessionWithOptions(t.Context(), path, "", desktopLegacyImportOptions(root))
108 if err != nil {
109 t.Fatal(err)
110 }
111 workspaceID, err := app.attachDesktopSession(t.Context(), "project", root, ref)
112 if err != nil {
113 t.Fatal(err)
114 }
115 tab := &WorkspaceTab{ID: "audit", Scope: "project", WorkspaceRoot: root, TopicID: "audit-topic", SessionID: ref.SessionID, Ctrl: ctrl, Ready: true, model: model, disabledMCP: map[string]ServerView{}}
116 tab.SessionWorkspace.ID = workspaceID
117 tab.sink = &tabEventSink{tabID: tab.ID, app: app}
118 installNoopRuntimeEvents(app, tab.sink)
119 app.tabs[tab.ID], app.tabOrder, app.activeTabID = tab, []string{tab.ID}, tab.ID
120 if err := tab.ensureSessionLease(path); err != nil {
121 t.Fatal(err)
122 }
123 t.Cleanup(tab.releaseSessionLease)
124 app.mu.Lock()
125 app.newSessionRuntimeLocked(tab, sessionRuntimeKey(tab.currentSessionIdentity()))
126 app.advanceSessionRuntimeEpochLocked(tab)
127 app.mu.Unlock()
128 return app, tab, ctrl, path
129 }
130
131 func TestAuditMigratedSessionReopen(t *testing.T) {
132 app, tab, ctrl, path := auditMigratedTab(t)
133 _, err := app.OpenTopicSession("project", tab.WorkspaceRoot, tab.TopicID, path)
134 if err != nil {
135 t.Fatalf("reopen current migrated session: %v", err)
136 }
137 if tab.Ctrl != ctrl {
138 t.Fatal("reopening current migrated session replaced its controller")
139 }
140 }
141
142 func TestAuditCanonicalIdleClearRegistry(t *testing.T) {
143 app, tab, ctrl, _ := auditMigratedTab(t)
144 oldRef, _ := ctrl.SessionRef()
145 result, err := app.ClearSessionForTab(tab.ID)
146 if err != nil {
147 t.Fatal(err)
148 }
149 newRef, _ := ctrl.SessionRef()
150 if newRef == oldRef || result.SessionID != newRef.SessionID {
151 t.Fatalf("rotation identities: old=%s new=%s result=%s", oldRef.SessionID, newRef.SessionID, result.SessionID)
152 }
153 app.mu.RLock()
154 stale := app.liveRuntimeTabMatchingLocked(nil, sessionRoute(oldRef.SessionID))
155 app.mu.RUnlock()
156 if stale != nil {
157 t.Errorf("old session lookup still returns controller now bound to %s", newRef.SessionID)
158 }
159 target := &WorkspaceTab{ID: "target", Scope: "project", WorkspaceRoot: tab.WorkspaceRoot, Ready: true}
160 target.sink = &tabEventSink{tabID: target.ID, app: app}
161 installNoopRuntimeEvents(app, target.sink)
162 app.tabs[target.ID] = target
163 app.tabOrder = append(app.tabOrder, target.ID)
164 app.activeTabID = target.ID
165 if _, err := app.OpenSession(oldRef); err != nil {
166 t.Fatalf("reopen old canonical session: %v", err)
167 }
168 t.Cleanup(target.Ctrl.Close)
169 actual, ok := target.Ctrl.(control.IdentityLifecycle).SessionRef()
170 if !ok || actual != oldRef {
171 t.Errorf("OpenSession returned success but controller=%s UI=%s requested=%s", actual.SessionID, target.SessionID, oldRef.SessionID)
172 }
173 }
174
175 func TestAuditCanonicalTaskFilter(t *testing.T) {
176 app, tab, _, _ := auditMigratedTab(t)
177 for _, id := range []string{tab.SessionID, "other-session"} {
178 if err := app.taskStore().SaveTask(t.Context(), tab.WorkspaceRoot, taskmonitor.TaskSnapshot{
179 SchemaVersion: 1, TaskID: id + "--task-1", JobID: "task-1", SessionID: id,
180 State: taskmonitor.TaskStateRunning, RuntimeState: taskmonitor.RuntimeStateAlive,
181 Version: 1, CreatedAt: time.Now(), UpdatedAt: time.Now(),
182 }); err != nil {
183 t.Fatal(err)
184 }
185 }
186 tasks, err := app.ListTasksForTab(tab.ID)
187 if err != nil {
188 t.Fatal(err)
189 }
190 if len(tasks) != 1 {
191 t.Errorf("current-session task list includes %d sessions, want 1", len(tasks))
192 }
193 _, sessionID, err := app.taskProjectKeys(TaskPageRequest{Scope: "session", TabID: tab.ID})
194 if err != nil {
195 t.Fatal(err)
196 }
197 if sessionID != tab.SessionID {
198 t.Errorf("catalog session filter = %q, want %q", sessionID, tab.SessionID)
199 }
200 }
201
202 type auditJobController struct {
203 *control.Controller
204 killed bool
205 }
206
207 func (c *auditJobController) CancelJob(string) bool { c.killed = true; return true }
208 func (c *auditJobController) TaskRuntimeOwnerID() string { return "audit-runtime-owner" }
209
210 func TestAuditCanonicalTaskStop(t *testing.T) {
211 app, tab, ctrl, _ := auditMigratedTab(t)
212 wrapper := &auditJobController{Controller: ctrl}
213 tab.Ctrl = wrapper
214 id := tab.SessionID + "--task-1"
215 if err := app.taskStore().SaveTask(t.Context(), tab.WorkspaceRoot, taskmonitor.TaskSnapshot{
216 SchemaVersion: 1, TaskID: id, JobID: "task-1", SessionID: tab.SessionID,
217 State: taskmonitor.TaskStateRunning, RuntimeState: taskmonitor.RuntimeStateAlive,
218 RuntimeOwnerID: wrapper.TaskRuntimeOwnerID(),
219 Version: 1, CreatedAt: time.Now(), UpdatedAt: time.Now(),
220 }); err != nil {
221 t.Fatal(err)
222 }
223 result, err := app.StopTaskForTab(tab.ID, id, 1, "audit", "audit-stop")
224 if err != nil {
225 t.Fatal(err)
226 }
227 if !result.Accepted || !wrapper.killed {
228 t.Fatalf("live canonical task was not stopped: accepted=%v error=%+v", result.Accepted, result.Error)
229 }
230 }
231
232 func TestTaskControlRejectsOldRecorderGeneration(t *testing.T) {
233 app, tab, ctrl, _ := auditMigratedTab(t)
234 wrapper := &auditJobController{Controller: ctrl}
235 tab.Ctrl = wrapper
236 for _, owner := range []string{"", "previous-runtime-owner"} {
237 id := tab.SessionID + "--task-1-" + owner
238 if err := app.taskStore().SaveTask(t.Context(), tab.WorkspaceRoot, taskmonitor.TaskSnapshot{
239 SchemaVersion: 1, TaskID: id, JobID: "task-1", SessionID: tab.SessionID,
240 RuntimeOwnerID: owner, State: taskmonitor.TaskStateRunning, RuntimeState: taskmonitor.RuntimeStateAlive,
241 Version: 1, CreatedAt: time.Now(), UpdatedAt: time.Now(),
242 }); err != nil {
243 t.Fatal(err)
244 }
245 result, err := app.StopTaskForTab(tab.ID, id, 1, "test", "old-owner-"+owner)
246 if err != nil || result.Accepted || wrapper.killed {
247 t.Fatalf("old owner accepted: %+v %v", result, err)
248 }
249 }
250 }
251
252 func TestAuditMigratedCloseReleasesLease(t *testing.T) {
253 app, tab, _, path := auditMigratedTab(t)
254 keep := &WorkspaceTab{ID: "keep", Scope: "project", WorkspaceRoot: tab.WorkspaceRoot, Ready: true}
255 app.tabs[keep.ID] = keep
256 app.tabOrder = append(app.tabOrder, keep.ID)
257 if err := app.CloseTab(tab.ID); err != nil {
258 t.Fatal(err)
259 }
260 lease, err := agent.TryAcquireSessionLease(path)
261 if err != nil {
262 t.Fatalf("closed migrated tab retained legacy lease: %v", err)
263 }
264 lease.Release()
265 }
266
267 func TestAuditTopicArchiveCanonicalVisibility(t *testing.T) {
268 app, tab, _, _ := auditMigratedTab(t)
269 keep := &WorkspaceTab{ID: "keep", Scope: "project", WorkspaceRoot: tab.WorkspaceRoot, TopicID: "keep", Ready: true}
270 app.tabs[keep.ID] = keep
271 app.tabOrder = append(app.tabOrder, keep.ID)
272 if err := app.TrashTopic(tab.TopicID); err != nil {
273 t.Fatal(err)
274 }
275 page, err := app.ListWorkspaceSessions(tab.SessionWorkspace.ID, "", "", 10, false)
276 if err != nil {
277 t.Fatal(err)
278 }
279 for _, row := range page.Sessions {
280 if row.Ref.SessionID == tab.SessionID {
281 t.Fatalf("archived topic remains visible as canonical session %s", row.Ref.SessionID)
282 }
283 }
284 }
285
285 lines GO