返回 DeepSeek-Reasonix
session_workspace_open_test.go
根目录 / desktop / session_workspace_open_test.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "path/filepath"
9 "testing"
10
11 "reasonix/internal/boot"
12 "reasonix/internal/control"
13 "reasonix/internal/event"
14 "reasonix/internal/provider"
15 "reasonix/internal/session"
16 )
17
18 func canonicalWorkspaceOpenFixture(t *testing.T) (*App, *WorkspaceTab, *session.Runtime, string, string) {
19 t.Helper()
20 isolateDesktopUserDirs(t)
21 model, _ := configureSwitchableDefaultModels(t)
22 app := NewApp()
23 t.Cleanup(app.closeSessionServices)
24 app.ctx = context.Background()
25 // Identical display names must not collapse distinct workspace identities.
26 rootA, rootB := filepath.Join(t.TempDir(), "project"), filepath.Join(t.TempDir(), "project")
27 for _, root := range []string{rootA, rootB} {
28 if err := os.MkdirAll(root, 0o700); err != nil {
29 t.Fatal(err)
30 }
31 }
32 workspaceA, err := app.ensureDesktopWorkspace(t.Context(), "project", rootA)
33 if err != nil {
34 t.Fatal(err)
35 }
36 workspaceB, err := app.ensureDesktopWorkspace(t.Context(), "project", rootB)
37 if err != nil {
38 t.Fatal(err)
39 }
40 service := app.desktopSessionService("")
41 create := func(id, cwd, workspace string) *session.Runtime {
42 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: id, CWD: cwd, Origin: session.SessionOriginNew})
43 if err != nil {
44 t.Fatal(err)
45 }
46 appendSessionTestModel(t, runtime, id+"-model", model)
47 appendSessionTestMessage(t, runtime, id+"-system", provider.Message{ID: id + "-system", Role: provider.RoleSystem, Content: "test system"})
48 appendSessionTestMessage(t, runtime, id+"-message", provider.Message{ID: id + "-user", Role: provider.RoleUser, Content: id})
49 if err := app.workspaceRegistry().AttachSession(t.Context(), "", workspace, id, ""); err != nil {
50 t.Fatal(err)
51 }
52 return runtime
53 }
54 source, target := create("session-A", rootA, workspaceA), create("session-B", rootB, workspaceB)
55 ctrl, err := app.buildTabControllerBoot(app.ctx, boot.Options{Model: model, WorkspaceRoot: rootA, SessionDir: desktopSessionDir(rootA), Sink: event.Discard})
56 if err != nil {
57 t.Fatal(err)
58 }
59 if _, err := ctrl.(control.IdentityLifecycle).OpenSession(t.Context(), source.Ref()); err != nil {
60 t.Fatal(err)
61 }
62 tab := &WorkspaceTab{ID: "source-tab", Scope: "project", WorkspaceRoot: rootA, SessionID: source.Ref().SessionID, Ready: true, Ctrl: ctrl, model: model, sink: &tabEventSink{tabID: "source-tab", app: app}, disabledMCP: map[string]ServerView{}}
63 tab.SessionWorkspace.ID = workspaceA
64 app.tabs[tab.ID], app.tabOrder, app.activeTabID = tab, []string{tab.ID}, tab.ID
65 app.newSessionRuntimeLocked(tab, sessionRuntimeKey(tab.currentSessionIdentity()))
66 t.Cleanup(func() {
67 if live := app.controllerForTab(tab); live != nil {
68 live.Close()
69 }
70 if tab.SharedHostKey != "" {
71 app.releaseSharedHost(tab.SharedHostKey)
72 }
73 })
74 return app, tab, target, rootB, workspaceB
75 }
76
77 func TestCanonicalOpenRefreshesSameWorkspaceIdentity(t *testing.T) {
78 app, tab, _, _, _ := canonicalWorkspaceOpenFixture(t)
79 want, ctrl := tab.SessionWorkspace.ID, tab.Ctrl
80 tab.SessionWorkspace.ID = "stale-workspace"
81 if _, err := app.OpenSession(session.SessionRef{HostID: localDesktopHostID, SessionID: tab.SessionID}); err != nil {
82 t.Fatal(err)
83 }
84 if tab.SessionWorkspace.ID != want || tab.Ctrl != ctrl {
85 t.Fatal("same-session navigation did not reconcile its workspace identity")
86 }
87 }
88
89 func TestCanonicalOpenFailurePreservesSource(t *testing.T) {
90 app, tab, target, _, _ := canonicalWorkspaceOpenFixture(t)
91 previous := persistedDesktopTabEntry(tab)
92 ctrl := tab.Ctrl
93 if err := app.desktopSessionService("").Close(t.Context(), target.Ref()); err != nil {
94 t.Fatal(err)
95 }
96 other, err := session.NewService(localDesktopHostID, session.NewFilesystemPersistence(app.desktopSessions.root))
97 if err != nil {
98 t.Fatal(err)
99 }
100 t.Cleanup(func() { _ = other.Shutdown(context.Background()) })
101 lease, err := other.Open(t.Context(), target.Ref())
102 if err != nil {
103 t.Fatal(err)
104 }
105 t.Cleanup(func() { _ = lease.Release(context.Background()) })
106 if _, err := app.OpenSession(target.Ref()); err == nil {
107 t.Fatal("external writer conflict accepted")
108 }
109 if tab.Ctrl != ctrl || tab.SessionID != previous.SessionID || tab.WorkspaceRoot != previous.WorkspaceRoot || !tab.Ready {
110 t.Fatal("failed open replaced source")
111 }
112 if err := ctrl.Snapshot(); err != nil {
113 t.Fatalf("source lost its writer: %v", err)
114 }
115 }
116
117 func TestCanonicalOpenRejectsConflictingMembership(t *testing.T) {
118 app, tab, target, _, workspaceB := canonicalWorkspaceOpenFixture(t)
119 ctrl := tab.Ctrl
120 state, err := app.workspaceRegistry().Load(t.Context())
121 if err != nil {
122 t.Fatal(err)
123 }
124 owner := state.Workspaces[workspaceB]
125 owner.Root = tab.WorkspaceRoot
126 state.Workspaces[workspaceB] = owner
127 body, err := json.Marshal(state)
128 if err != nil {
129 t.Fatal(err)
130 }
131 if err := os.WriteFile(app.workspaceRegistry().Path(), body, 0o600); err != nil {
132 t.Fatal(err)
133 }
134 if _, err := app.OpenSession(target.Ref()); !errors.Is(err, errSessionWorkspaceConflict) {
135 t.Fatalf("conflict: %v", err)
136 }
137 if tab.Ctrl != ctrl || tab.SessionID != "session-A" {
138 t.Fatal("conflict modified current runtime")
139 }
140 if got := friendlySessionLoadError(errSessionWorkspaceConflict); !errors.Is(got, errSessionWorkspaceConflict) {
141 t.Fatalf("conflict disguised as corruption: %v", got)
142 }
143 }
144
145 func TestCanonicalStartupRepairsPersistedWorkspace(t *testing.T) {
146 app, tab, target, rootB, workspaceB := canonicalWorkspaceOpenFixture(t)
147 tab.Ctrl.Close()
148 tab.Ctrl, tab.Ready = nil, false
149 tab.SessionID = target.Ref().SessionID
150 tab.TopicID, tab.TopicTitle = "old-topic", "old title"
151 tab.SessionPath = "/old/project/transcript.jsonl"
152 app.saveTabsFromRemote()
153 if !app.prepareTabControllerWorkspace(tab, t.Context(), 0, app.ctx) {
154 t.Fatal(tab.StartupErr)
155 }
156 if tab.WorkspaceRoot != rootB || tab.SessionWorkspace.ID != workspaceB || tab.TopicID != "" || tab.SessionPath != "" {
157 t.Fatalf("cold repair retained old binding: %+v", persistedDesktopTabEntry(tab))
158 }
159 app.buildTabController(tab)
160 if tab.Ctrl == nil || !tab.Ready || tab.StartupErr != "" {
161 t.Fatalf("restart failed: %s", tab.StartupErr)
162 }
163 if root, ok := safeControllerWorkspaceRoot(tab.Ctrl); !ok || !sameDesktopPath(root, rootB) {
164 t.Fatalf("restart root = %s", root)
165 }
166 }
167
168 func TestCanonicalOpenRecoversFailedSurface(t *testing.T) {
169 app, tab, target, rootB, _ := canonicalWorkspaceOpenFixture(t)
170 tab.Ctrl.Close()
171 tab.Ctrl, tab.Ready, tab.StartupErr = nil, false, errSessionWorkspaceConflict.Error()
172 if _, err := app.OpenSession(target.Ref()); err != nil {
173 t.Fatal(err)
174 }
175 if !tab.Ready || tab.StartupErr != "" || !sameDesktopPath(tab.WorkspaceRoot, rootB) {
176 t.Fatal("failed surface did not recover")
177 }
178 }
179
180 func TestCanonicalOpenCommitsTargetWorkspace(t *testing.T) {
181 app, tab, target, rootB, workspaceB := canonicalWorkspaceOpenFixture(t)
182 tab.HistoricalSource = &SessionSourceRef{Path: "/fixture/old.jsonl"}
183 if err := app.workspaceRegistry().EnsureSessionTopic(t.Context(), target.Ref().SessionID, "topic-B", "Target topic"); err != nil {
184 t.Fatal(err)
185 }
186 if _, err := app.OpenSession(target.Ref()); err != nil {
187 t.Fatalf("open: %v", err)
188 }
189 if tab.SessionID != target.Ref().SessionID || !sameDesktopPath(tab.WorkspaceRoot, rootB) || tab.SessionWorkspace.ID != workspaceB {
190 t.Fatalf("wrong destination binding; got session=%s root=%s workspace=%s", tab.SessionID, tab.WorkspaceRoot, tab.SessionWorkspace.ID)
191 }
192 if tab.TopicID != "topic-B" || tab.TopicTitle != "Target topic" {
193 t.Fatalf("wrong destination topic; got id=%q title=%q", tab.TopicID, tab.TopicTitle)
194 }
195 if tab.HistoricalSource != nil {
196 t.Fatal("canonical activation retained the preparation action")
197 }
198 persisted := loadTabsFile()
199 if len(persisted.Tabs) != 1 || persisted.Tabs[0].SessionID != "session-B" || persisted.Tabs[0].TopicID != "topic-B" || !sameDesktopPath(persisted.Tabs[0].WorkspaceRoot, rootB) {
200 t.Fatalf("destination binding not persisted: %+v", persisted)
201 }
202 err := app.validateDesktopWorkspaceMembership(t.Context(), workspaceB, target.Ref())
203 if err != nil {
204 t.Fatalf("restart validation: %v", err)
205 }
206 if root, ok := safeControllerWorkspaceRoot(tab.Ctrl); !ok || !sameDesktopPath(root, rootB) {
207 t.Fatalf("controller workspace = %q, available=%v", root, ok)
208 }
209 }
210
210 lines GO