返回 DeepSeek-Reasonix
session_navigation_active_test.go
根目录 / desktop / session_navigation_active_test.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "reasonix/internal/boot"
10 "reasonix/internal/control"
11 "reasonix/internal/event"
12 "reasonix/internal/provider"
13 "reasonix/internal/session"
14 )
15
16 type activeNavigationController struct {
17 control.SessionAPI
18 control.IdentityLifecycle
19 status control.RuntimeStatus
20 closed bool
21 replays int
22 }
23
24 func (c *activeNavigationController) RuntimeStatus() control.RuntimeStatus { return c.status }
25 func (c *activeNavigationController) Close() { c.closed = true; c.SessionAPI.Close() }
26
27 func (c *activeNavigationController) ReplayPendingPrompts() {
28 c.replays++
29 c.SessionAPI.ReplayPendingPrompts()
30 }
31
32 func (c *activeNavigationController) SessionBinding() (*session.Service, *session.Runtime, bool) {
33 return c.SessionAPI.(persistedSessionBinding).SessionBinding()
34 }
35
36 type snapshotProbeController struct {
37 activeNavigationController
38 snapshot func() error
39 }
40
41 func (c *snapshotProbeController) Snapshot() error {
42 if c.snapshot != nil {
43 return c.snapshot()
44 }
45 return c.SessionAPI.Snapshot()
46 }
47
48 func navigationFreezeFixture(t *testing.T) (*App, *WorkspaceTab, string) {
49 t.Helper()
50 isolateDesktopUserDirs(t)
51 model, _ := configureSwitchableDefaultModels(t)
52 app := NewApp()
53 t.Cleanup(app.closeSessionServices)
54 app.ctx = context.Background()
55 rootA, rootB := filepath.Join(t.TempDir(), "project"), filepath.Join(t.TempDir(), "project")
56 for _, root := range []string{rootA, rootB} {
57 if err := os.MkdirAll(root, 0o700); err != nil {
58 t.Fatal(err)
59 }
60 }
61 workspaceA, err := app.ensureDesktopWorkspace(t.Context(), "project", rootA)
62 if err != nil {
63 t.Fatal(err)
64 }
65 service := app.desktopSessionService("")
66 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "session-A", CWD: rootA, Origin: session.SessionOriginNew})
67 if err != nil {
68 t.Fatal(err)
69 }
70 appendSessionTestModel(t, runtime, "session-A-model", model)
71 appendSessionTestMessage(t, runtime, "session-A-system", provider.Message{ID: "session-A-system", Role: provider.RoleSystem, Content: "test system"})
72 appendSessionTestMessage(t, runtime, "session-A-message", provider.Message{ID: "session-A-user", Role: provider.RoleUser, Content: "session-A"})
73 if err := app.workspaceRegistry().AttachSession(t.Context(), "", workspaceA, runtime.Ref().SessionID, ""); err != nil {
74 t.Fatal(err)
75 }
76 ctrl, err := app.buildTabControllerBoot(app.ctx, boot.Options{Model: model, WorkspaceRoot: rootA, SessionDir: desktopSessionDir(rootA), Sink: event.Discard})
77 if err != nil {
78 t.Fatal(err)
79 }
80 if _, err := ctrl.(control.IdentityLifecycle).OpenSession(t.Context(), runtime.Ref()); err != nil {
81 t.Fatal(err)
82 }
83 tab := &WorkspaceTab{ID: "source-tab", Scope: "project", WorkspaceRoot: rootA, SessionID: runtime.Ref().SessionID, Ready: true, Ctrl: ctrl, model: model, sink: &tabEventSink{tabID: "source-tab", app: app}, disabledMCP: map[string]ServerView{}}
84 tab.SessionWorkspace.ID = workspaceA
85 app.tabs[tab.ID], app.tabOrder, app.activeTabID = tab, []string{tab.ID}, tab.ID
86 app.newSessionRuntimeLocked(tab, sessionRuntimeKey(tab.currentSessionIdentity()))
87 t.Cleanup(func() {
88 if live := app.controllerForTab(tab); live != nil {
89 live.Close()
90 }
91 if tab.SharedHostKey != "" {
92 app.releaseSharedHost(tab.SharedHostKey)
93 }
94 })
95 return app, tab, rootB
96 }
97
98 func TestPruneRunningSessionDoesNotSnapshot(t *testing.T) {
99 app, tab, rootB := navigationFreezeFixture(t)
100 original := tab.Ctrl
101 snapped := false
102 tab.TopicID = "topic-a"
103 tab.Ctrl = &snapshotProbeController{
104 activeNavigationController: activeNavigationController{
105 SessionAPI: original, IdentityLifecycle: original.(control.IdentityLifecycle),
106 status: control.RuntimeStatus{Running: true},
107 },
108 snapshot: func() error {
109 snapped = true
110 return original.Snapshot()
111 },
112 }
113 t.Cleanup(original.Close)
114 if _, err := app.EnsureBlankSurface("project", rootB); err != nil {
115 t.Fatal(err)
116 }
117 if snapped {
118 t.Fatal("switching away snapshotted a running session")
119 }
120 }
121
122 func TestActivateTopicReattachesDetachedRunningSession(t *testing.T) {
123 app, tab, rootB := navigationFreezeFixture(t)
124 rootA := tab.WorkspaceRoot
125 sourceID := tab.SessionID
126 original := tab.Ctrl
127 active := &activeNavigationController{
128 SessionAPI: original, IdentityLifecycle: original.(control.IdentityLifecycle),
129 status: control.RuntimeStatus{Running: true},
130 }
131 tab.Ctrl = active
132 tab.TopicID = "topic-a"
133 t.Cleanup(original.Close)
134 if _, err := app.EnsureBlankSurface("project", rootB); err != nil {
135 t.Fatal(err)
136 }
137 meta, err := app.ActivateTopic("project", rootA, "topic-a", "")
138 if err != nil {
139 t.Fatal(err)
140 }
141 visible := app.tabs[meta.ID]
142 if visible == nil || visible.Ctrl != active || active.closed {
143 t.Fatal("ActivateTopic did not reattach the running owner")
144 }
145 if visible.SessionID != sourceID {
146 t.Fatalf("reattached session %q, want %q", visible.SessionID, sourceID)
147 }
148 }
149
150 func TestBlankOnOtherProjectThenOpenSessionReattaches(t *testing.T) {
151 app, tab, rootB := navigationFreezeFixture(t)
152 source := session.SessionRef{HostID: localDesktopHostID, SessionID: tab.SessionID}
153 original := tab.Ctrl
154 active := &activeNavigationController{
155 SessionAPI: original, IdentityLifecycle: original.(control.IdentityLifecycle),
156 status: control.RuntimeStatus{Running: true},
157 }
158 tab.Ctrl = active
159 t.Cleanup(original.Close)
160 if _, err := app.EnsureBlankSurface("project", rootB); err != nil {
161 t.Fatal(err)
162 }
163 if _, err := app.OpenSession(source); err != nil {
164 t.Fatal(err)
165 }
166 visible := app.tabs[app.activeTabID]
167 if visible == nil || visible.Ctrl != active || active.closed {
168 t.Fatal("OpenSession did not reattach the running owner")
169 }
170 }
171
172 func TestCanonicalNavigationPreservesActiveSource(t *testing.T) {
173 for name, status := range map[string]control.RuntimeStatus{
174 "running": {Running: true}, "approval": {PendingPrompt: true}, "background": {BackgroundJobs: 1},
175 } {
176 t.Run(name, func(t *testing.T) {
177 app, tab, target, _, _ := canonicalWorkspaceOpenFixture(t)
178 source := session.SessionRef{HostID: localDesktopHostID, SessionID: tab.SessionID}
179 original := tab.Ctrl
180 active := &activeNavigationController{SessionAPI: original, IdentityLifecycle: original.(control.IdentityLifecycle), status: status}
181 tab.Ctrl = active
182 t.Cleanup(original.Close)
183 sourceSink := tab.sink
184 sourceDisplay := tab.displayBufferState()
185 if _, err := app.OpenSession(target.Ref()); err != nil {
186 t.Fatal(err)
187 }
188 key := sessionRuntimeKey(sessionRoute(source.SessionID))
189 detached := app.detachedSessions[key]
190 if detached == nil || detached.Ctrl != active || active.closed {
191 t.Fatal("active source was not retained")
192 }
193 if tab.sink == sourceSink {
194 t.Fatal("target shares background source event sink")
195 }
196 if owner, _ := sourceSink.binding(); owner != detached.ID || sourceSink.context() != nil || tab.displayBufferState() == sourceDisplay {
197 t.Fatal("background events can reach the visible session")
198 }
199 if ref, _ := active.SessionRef(); ref != source {
200 t.Fatal("source writer was rebound")
201 }
202 if rt := app.runtimeBySessionKey[key]; rt == nil || rt.Owner != detached {
203 t.Fatal("source runtime registry lost its owner")
204 }
205 if _, err := app.OpenSession(source); err != nil {
206 t.Fatal(err)
207 }
208 if tab.Ctrl != active || tab.sink != sourceSink || active.closed || app.detachedSessions[key] != nil {
209 t.Fatal("return did not reattach the exact active source")
210 }
211 if active.replays != 1 || tab.displayBufferState() != sourceDisplay {
212 t.Fatal("return did not restore pending prompts and display state")
213 }
214 if _, err := app.OpenSession(source); err != nil {
215 t.Fatalf("active same-session open: %v", err)
216 }
217 })
218 }
219 }
220
221 func TestCanonicalNavigationBetweenTwoActiveOwners(t *testing.T) {
222 app, tab, target, _, _ := canonicalWorkspaceOpenFixture(t)
223 source := session.SessionRef{HostID: localDesktopHostID, SessionID: tab.SessionID}
224 wrap := func() *activeNavigationController {
225 ctrl := tab.Ctrl
226 active := &activeNavigationController{SessionAPI: ctrl, IdentityLifecycle: ctrl.(control.IdentityLifecycle), status: control.RuntimeStatus{Running: true}}
227 tab.Ctrl = active
228 t.Cleanup(ctrl.Close)
229 return active
230 }
231 first := wrap()
232 if _, err := app.OpenSession(target.Ref()); err != nil {
233 t.Fatal(err)
234 }
235 second := wrap()
236 for range 3 {
237 if _, err := app.OpenSession(source); err != nil {
238 t.Fatal(err)
239 }
240 if tab.Ctrl != first {
241 t.Fatal("source controller replaced")
242 }
243 if _, err := app.OpenSession(target.Ref()); err != nil {
244 t.Fatal(err)
245 }
246 if tab.Ctrl != second {
247 t.Fatal("target controller replaced")
248 }
249 }
250 if first.closed || second.closed || len(app.detachedSessions) != 1 {
251 t.Fatal("active navigation closed or duplicated an owner")
252 }
253 }
254
255 func TestCanonicalNavigationDoesNotSnapshotActiveSource(t *testing.T) {
256 for name, status := range map[string]control.RuntimeStatus{
257 "running": {Running: true}, "approval": {PendingPrompt: true}, "background": {BackgroundJobs: 1},
258 } {
259 t.Run(name, func(t *testing.T) {
260 app, tab, target, _, _ := canonicalWorkspaceOpenFixture(t)
261 original := tab.Ctrl
262 t.Cleanup(original.Close)
263 tab.Ctrl = &snapshotProbeController{
264 activeNavigationController: activeNavigationController{SessionAPI: original, IdentityLifecycle: original.(control.IdentityLifecycle), status: status},
265 snapshot: func() error { t.Error("navigation snapshotted an active source"); return nil },
266 }
267 if _, err := app.OpenSession(target.Ref()); err != nil {
268 t.Fatal(err)
269 }
270 })
271 }
272 }
273
273 lines GO