返回 DeepSeek-Reasonix
session_preset_restore_test.go
根目录 / desktop / session_preset_restore_test.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "path/filepath"
9 "runtime"
10 "strings"
11 "testing"
12
13 "reasonix/internal/control"
14 "reasonix/internal/session"
15 )
16
17 func requireTabPreset(t *testing.T, app *App, tab *WorkspaceTab, sessionID, want string) {
18 t.Helper()
19 app.mu.RLock()
20 bound, got := tab.SessionID, tab.toolApprovalMode
21 app.mu.RUnlock()
22 if bound != sessionID {
23 t.Fatalf("tab bound to %q, want %q", bound, sessionID)
24 }
25 if got != want {
26 t.Fatalf("%s tab preset = %q, want %q", sessionID, got, want)
27 }
28 if ctrl := app.controllerForTab(tab); ctrl != nil {
29 if live := normalizeToolApprovalMode(ctrl.ToolApprovalMode()); live != want {
30 t.Fatalf("%s controller preset = %q, want %q", sessionID, live, want)
31 }
32 }
33 }
34
35 // presetHost pins a host that offers every preset, so these tests assert the
36 // same thing on a runner with or without an OS sandbox.
37 func presetHost(t *testing.T) {
38 t.Helper()
39 t.Cleanup(control.SetPresetSandboxForTest(true))
40 }
41
42 func choosePreset(t *testing.T, app *App, tab *WorkspaceTab, preset string) {
43 t.Helper()
44 seen, err := app.PermissionSnapshotForTab(tab.ID)
45 if err != nil {
46 t.Fatal(err)
47 }
48 if _, err := app.SetPermissionPresetForTab(tab.ID, seen.SessionID, preset, seen.Revision); err != nil {
49 t.Fatal(err)
50 }
51 }
52
53 func restartDesktopFromTabsFile(t *testing.T, first *App, tab *WorkspaceTab) (*App, *WorkspaceTab) {
54 t.Helper()
55 if ctrl := first.controllerForTab(tab); ctrl != nil {
56 ctrl.Close()
57 }
58 if tab.SharedHostKey != "" {
59 first.releaseSharedHost(tab.SharedHostKey)
60 tab.SharedHostKey = ""
61 }
62 first.closeSessionServices()
63 app := NewApp()
64 app.ctx = context.Background()
65 installNoopRuntimeEvents(app)
66 t.Cleanup(app.closeSessionServices)
67 finishSavedTabMigration(app)
68 app.tabsRestored = make(chan struct{})
69 app.restoreOrBuildTabs()
70 restored := app.tabs[tab.ID]
71 if restored == nil {
72 t.Fatalf("tab %q was not restored from %s", tab.ID, tabsFileName)
73 }
74 waitFor(t, "restored tab runtime", func() bool { return app.controllerForTab(restored) != nil })
75 t.Cleanup(func() {
76 if live := app.controllerForTab(restored); live != nil {
77 live.Close()
78 }
79 if restored.SharedHostKey != "" {
80 app.releaseSharedHost(restored.SharedHostKey)
81 }
82 })
83 return app, restored
84 }
85
86 func TestCanonicalSessionPresetFollowsSessionAcrossNavigationAndRestart(t *testing.T) {
87 presetHost(t)
88 app, tab, target, rootB, _ := canonicalWorkspaceOpenFixture(t)
89 refA := session.SessionRef{HostID: localDesktopHostID, SessionID: tab.SessionID}
90 refB := target.Ref()
91 _, fresh := desktopNewSessionDefaults("project", rootB)
92 if fresh == control.ToolApprovalDangerFullAccess || fresh == control.ToolApprovalReadOnly {
93 t.Fatalf("new-session default %q must differ from both recorded presets", fresh)
94 }
95
96 choosePreset(t, app, tab, control.ToolApprovalDangerFullAccess)
97 requireTabPreset(t, app, tab, refA.SessionID, control.ToolApprovalDangerFullAccess)
98 if _, err := app.OpenSession(refB); err != nil {
99 t.Fatal(err)
100 }
101 requireTabPreset(t, app, tab, refB.SessionID, fresh)
102
103 choosePreset(t, app, tab, control.ToolApprovalReadOnly)
104 if _, err := app.OpenSession(refA); err != nil {
105 t.Fatal(err)
106 }
107 requireTabPreset(t, app, tab, refA.SessionID, control.ToolApprovalDangerFullAccess)
108
109 app, tab = restartDesktopFromTabsFile(t, app, tab)
110 requireTabPreset(t, app, tab, refA.SessionID, control.ToolApprovalDangerFullAccess)
111 if _, err := app.OpenSession(refB); err != nil {
112 t.Fatal(err)
113 }
114 requireTabPreset(t, app, tab, refB.SessionID, control.ToolApprovalReadOnly)
115 if _, err := app.OpenSession(refA); err != nil {
116 t.Fatal(err)
117 }
118 requireTabPreset(t, app, tab, refA.SessionID, control.ToolApprovalDangerFullAccess)
119 }
120
121 func TestRestartDoesNotTrustSurfacePresetForUnrecordedSession(t *testing.T) {
122 presetHost(t)
123 app, tab, _, _, _ := canonicalWorkspaceOpenFixture(t)
124 _, fresh := desktopNewSessionDefaults("project", tab.WorkspaceRoot)
125 entry := persistedDesktopTabEntry(tab)
126 entry.ToolApprovalMode = control.ToolApprovalDangerFullAccess
127 file := desktopTabsFile{Tabs: []desktopTabEntry{entry}, ActiveTab: entry.ID, TabOrder: []string{entry.ID}}
128 if err := os.MkdirAll(desktopConfigDir(), 0o700); err != nil {
129 t.Fatal(err)
130 }
131 app.mu.Lock()
132 app.tabs, app.tabOrder = map[string]*WorkspaceTab{}, nil
133 app.mu.Unlock()
134 if err := os.WriteFile(filepath.Join(desktopConfigDir(), tabsFileName), mustMarshalJSON(t, file), 0o600); err != nil {
135 t.Fatal(err)
136 }
137
138 app, tab = restartDesktopFromTabsFile(t, app, tab)
139 requireTabPreset(t, app, tab, entry.SessionID, fresh)
140 }
141
142 func recordedSessionPresets(t *testing.T) map[string]string {
143 t.Helper()
144 body, err := os.ReadFile(sessionPresetsPath())
145 if errors.Is(err, os.ErrNotExist) {
146 return map[string]string{}
147 }
148 if err != nil {
149 t.Fatal(err)
150 }
151 var file sessionPresetsFile
152 if err := json.Unmarshal(body, &file); err != nil {
153 t.Fatal(err)
154 }
155 return file.Sessions
156 }
157
158 func TestNewSessionInTabDoesNotInheritSourcePreset(t *testing.T) {
159 presetHost(t)
160 app, tab, _, _, _ := canonicalWorkspaceOpenFixture(t)
161 refA := session.SessionRef{HostID: localDesktopHostID, SessionID: tab.SessionID}
162 _, fresh := desktopNewSessionDefaults("project", tab.WorkspaceRoot)
163 if fresh == control.ToolApprovalDangerFullAccess {
164 t.Fatalf("new-session default %q must differ from the source preset", fresh)
165 }
166
167 choosePreset(t, app, tab, control.ToolApprovalDangerFullAccess)
168 if err := app.NewSessionForTab(tab.ID); err != nil {
169 t.Fatal(err)
170 }
171 app.mu.RLock()
172 fresher := tab.SessionID
173 app.mu.RUnlock()
174 if fresher == "" || fresher == refA.SessionID {
175 t.Fatalf("New Session did not rotate the tab: %q", fresher)
176 }
177 requireTabPreset(t, app, tab, fresher, fresh)
178 app.saveTabsFromRemote()
179 if preset, ok := recordedSessionPresets(t)[fresher]; ok {
180 t.Fatalf("new session %s was recorded as %q without a choice made in it", fresher, preset)
181 }
182
183 app, tab = restartDesktopFromTabsFile(t, app, tab)
184 requireTabPreset(t, app, tab, fresher, fresh)
185 if _, err := app.OpenSession(refA); err != nil {
186 t.Fatal(err)
187 }
188 requireTabPreset(t, app, tab, refA.SessionID, control.ToolApprovalDangerFullAccess)
189 if _, err := app.OpenSession(session.SessionRef{HostID: localDesktopHostID, SessionID: fresher}); err != nil {
190 t.Fatal(err)
191 }
192 requireTabPreset(t, app, tab, fresher, fresh)
193 }
194
195 func TestNewSessionOnBlankTabDropsItsEarlierChoice(t *testing.T) {
196 presetHost(t)
197 app, tab, _, _, _ := canonicalWorkspaceOpenFixture(t)
198 _, fresh := desktopNewSessionDefaults("project", tab.WorkspaceRoot)
199 if err := app.NewSessionForTab(tab.ID); err != nil {
200 t.Fatal(err)
201 }
202 app.mu.RLock()
203 blank := tab.SessionID
204 app.mu.RUnlock()
205 choosePreset(t, app, tab, control.ToolApprovalDangerFullAccess)
206 requireTabPreset(t, app, tab, blank, control.ToolApprovalDangerFullAccess)
207
208 if err := app.NewSessionForTab(tab.ID); err != nil {
209 t.Fatal(err)
210 }
211 app.mu.RLock()
212 reused := tab.SessionID
213 app.mu.RUnlock()
214 requireTabPreset(t, app, tab, reused, fresh)
215 if preset, ok := recordedSessionPresets(t)[reused]; ok {
216 t.Fatalf("session %s handed out as new still carries %q", reused, preset)
217 }
218 }
219
220 func TestForkStartsAtNewSessionDefault(t *testing.T) {
221 presetHost(t)
222 app, tab, _, _, _ := canonicalWorkspaceOpenFixture(t)
223 _, fresh := desktopNewSessionDefaults("project", tab.WorkspaceRoot)
224 child, err := app.desktopSessionService("").Create(t.Context(), session.CreateOptions{SessionID: "session-fork", CWD: tab.WorkspaceRoot, Origin: session.SessionOriginNew})
225 if err != nil {
226 t.Fatal(err)
227 }
228 if err := app.workspaceRegistry().AttachSession(t.Context(), "", tab.SessionWorkspace.ID, child.Ref().SessionID, ""); err != nil {
229 t.Fatal(err)
230 }
231
232 choosePreset(t, app, tab, control.ToolApprovalDangerFullAccess)
233 opened, err := app.openForkedSessionTabWithWorkspace(tab, forkedSessionLocator{SessionID: child.Ref().SessionID}, "")
234 if err != nil {
235 t.Fatal(err)
236 }
237 app.mu.RLock()
238 forked := app.tabs[opened.tab.ID]
239 app.mu.RUnlock()
240 if forked == nil {
241 t.Fatal("fork tab was not opened")
242 }
243 t.Cleanup(func() {
244 if live := app.controllerForTab(forked); live != nil {
245 live.Close()
246 }
247 })
248 app.mu.RLock()
249 got, mode := forked.toolApprovalMode, forked.mode
250 app.mu.RUnlock()
251 if got != fresh || mode != tabModeFromAxes(false, fresh == control.ToolApprovalDangerFullAccess) {
252 t.Fatalf("fork preset = %q (mode %q), want %q", got, mode, fresh)
253 }
254 app.saveTabsFromRemote()
255 if preset, ok := recordedSessionPresets(t)[child.Ref().SessionID]; ok {
256 t.Fatalf("fork %s was recorded as %q without a choice made in it", child.Ref().SessionID, preset)
257 }
258 }
259
260 func TestUnreadablePresetRecordsRestoreDefault(t *testing.T) {
261 presetHost(t)
262 app, tab, _, _, _ := canonicalWorkspaceOpenFixture(t)
263 _, fresh := desktopNewSessionDefaults("project", tab.WorkspaceRoot)
264 choosePreset(t, app, tab, control.ToolApprovalDangerFullAccess)
265 if err := os.WriteFile(sessionPresetsPath(), []byte("{not json"), 0o600); err != nil {
266 t.Fatal(err)
267 }
268 app, tab = restartDesktopFromTabsFile(t, app, tab)
269 requireTabPreset(t, app, tab, tab.SessionID, fresh)
270 }
271
272 func TestStalePresetChoiceDoesNotLandOnNavigatedSession(t *testing.T) {
273 presetHost(t)
274 app, tab, target, rootB, _ := canonicalWorkspaceOpenFixture(t)
275 refA := session.SessionRef{HostID: localDesktopHostID, SessionID: tab.SessionID}
276 refB := target.Ref()
277 _, fresh := desktopNewSessionDefaults("project", rootB)
278 choosePreset(t, app, tab, control.ToolApprovalWorkspaceWrite)
279 seen, err := app.PermissionSnapshotForTab(tab.ID)
280 if err != nil {
281 t.Fatal(err)
282 }
283 if seen.SessionID != refA.SessionID {
284 t.Fatalf("snapshot session = %q, want %q", seen.SessionID, refA.SessionID)
285 }
286 if _, err := app.OpenSession(refB); err != nil {
287 t.Fatal(err)
288 }
289 if live, err := app.PermissionSnapshotForTab(tab.ID); err != nil || live.Revision != seen.Revision {
290 t.Fatalf("fixture must reproduce a revision collision: A=%d B=%d err=%v", seen.Revision, live.Revision, err)
291 }
292
293 _, err = app.SetPermissionPresetForTab(tab.ID, seen.SessionID, control.ToolApprovalDangerFullAccess, seen.Revision)
294 if !errors.Is(err, errPermissionSessionChanged) {
295 t.Fatalf("stale choice for %s on %s: err = %v, want errPermissionSessionChanged", refA.SessionID, refB.SessionID, err)
296 }
297 if !strings.Contains(err.Error(), "reasonix_error:"+permissionSessionChangedCode) {
298 t.Fatalf("bridge error %q does not carry the refusal code", err)
299 }
300 if got, ok := recordedSessionPresets(t)[refB.SessionID]; ok {
301 t.Fatalf("session %s recorded as %q from a choice made in %s", refB.SessionID, got, refA.SessionID)
302 }
303 requireTabPreset(t, app, tab, refB.SessionID, fresh)
304 }
305
306 func TestUnfencedModeSettersDoNotRecord(t *testing.T) {
307 presetHost(t)
308 app, tab, _, _, _ := canonicalWorkspaceOpenFixture(t)
309 app.SetToolApprovalModeForTab(tab.ID, control.ToolApprovalDangerFullAccess)
310 app.SetModeForTab(tab.ID, "yolo")
311 if got, ok := recordedSessionPresets(t)[tab.SessionID]; ok {
312 t.Fatalf("session %s recorded as %q by a setter that names no session", tab.SessionID, got)
313 }
314 }
315
316 func TestPresetWriteFailureInReadOnlyDirFailsClosed(t *testing.T) {
317 presetHost(t)
318 if runtime.GOOS == "windows" || os.Geteuid() == 0 {
319 t.Skip("directory permissions do not refuse writes here")
320 }
321 app, tab, _, _, _ := canonicalWorkspaceOpenFixture(t)
322 _, fresh := desktopNewSessionDefaults("project", tab.WorkspaceRoot)
323 choosePreset(t, app, tab, control.ToolApprovalDangerFullAccess)
324 dir := filepath.Dir(sessionPresetsPath())
325 if err := os.Chmod(dir, 0o500); err != nil {
326 t.Fatal(err)
327 }
328 t.Cleanup(func() { _ = os.Chmod(dir, 0o700) })
329 choosePreset(t, app, tab, control.ToolApprovalReadOnly)
330 if err := os.Chmod(dir, 0o700); err != nil {
331 t.Fatal(err)
332 }
333
334 app, tab = restartDesktopFromTabsFile(t, app, tab)
335 app.mu.RLock()
336 got := tab.toolApprovalMode
337 app.mu.RUnlock()
338 if got == control.ToolApprovalDangerFullAccess {
339 t.Fatalf("narrowed to read-only, write failed, restart restored %q (default %q)", got, fresh)
340 }
341 }
342
343 func TestSnapshotBeforeBlankSessionReuseIsRefused(t *testing.T) {
344 presetHost(t)
345 app, tab, _, _, _ := canonicalWorkspaceOpenFixture(t)
346 if err := app.NewSessionForTab(tab.ID); err != nil {
347 t.Fatal(err)
348 }
349 seen, err := app.PermissionSnapshotForTab(tab.ID)
350 if err != nil {
351 t.Fatal(err)
352 }
353 if err := app.NewSessionForTab(tab.ID); err != nil {
354 t.Fatal(err)
355 }
356 if live, err := app.PermissionSnapshotForTab(tab.ID); err != nil || live.SessionID != seen.SessionID {
357 t.Fatalf("blank tab was not reused: before %q, after %q (err %v)", seen.SessionID, live.SessionID, err)
358 }
359
360 if _, err := app.SetPermissionPresetForTab(tab.ID, seen.SessionID, control.ToolApprovalDangerFullAccess, seen.Revision); err == nil {
361 t.Errorf("choice read before New Session reused %s was accepted", seen.SessionID)
362 }
363 if got, ok := recordedSessionPresets(t)[seen.SessionID]; ok {
364 t.Errorf("session %s handed out as new was recorded as %q from an earlier snapshot", seen.SessionID, got)
365 }
366 }
367
367 lines GO