返回 DeepSeek-Reasonix
session_prompt_test.go
根目录 / desktop / session_prompt_test.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "sync"
11 "testing"
12
13 "reasonix/internal/agent"
14 "reasonix/internal/control"
15 "reasonix/internal/event"
16 "reasonix/internal/provider"
17 "reasonix/internal/tool"
18 )
19
20 type promptResumeCtrl struct {
21 history []provider.Message
22 resumed *agent.Session
23 path string
24 }
25
26 func (c *promptResumeCtrl) History() []provider.Message {
27 return append([]provider.Message(nil), c.history...)
28 }
29
30 func (c *promptResumeCtrl) Resume(s *agent.Session, path string) {
31 c.resumed = s
32 c.path = path
33 }
34
35 func (c *promptResumeCtrl) SetSessionPath(path string) {
36 c.path = path
37 }
38
39 func TestSessionWithFreshSystemPromptPreservesLoadedRewriteBaseline(t *testing.T) {
40 path := filepath.Join(t.TempDir(), "session.jsonl")
41 s := agent.NewSession("old sys")
42 s.Add(provider.Message{Role: provider.RoleUser, Content: "first"})
43 s.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "tool-1", Name: "read_file", Arguments: "{}"}}})
44 s.Add(provider.Message{Role: provider.RoleTool, ToolCallID: "tool-1", Name: "read_file", Content: strings.Repeat("detail ", 100)})
45 s.Add(provider.Message{Role: provider.RoleAssistant, Content: "done"})
46 if err := s.Save(path); err != nil {
47 t.Fatalf("Save base: %v", err)
48 }
49
50 loaded, err := agent.LoadSession(path)
51 if err != nil {
52 t.Fatalf("LoadSession: %v", err)
53 }
54 resumed := sessionWithFreshSystemPrompt(loaded, "new sys")
55 msgs := resumed.Snapshot()
56 msgs[3].Content = "[elided tool result]"
57 resumed.Replace(msgs)
58 if err := resumed.SaveRewrite(path); err != nil {
59 t.Fatalf("SaveRewrite fresh-system resume: %v", err)
60 }
61
62 reloaded, err := agent.LoadSession(path)
63 if err != nil {
64 t.Fatalf("LoadSession rewritten: %v", err)
65 }
66 if got := reloaded.Messages[0].Content; got != "new sys" {
67 t.Fatalf("system prompt after rewrite = %q, want new sys", got)
68 }
69 if got := reloaded.Messages[3].Content; got != "[elided tool result]" {
70 t.Fatalf("tool result after rewrite = %q, want elided", got)
71 }
72 if matches, err := filepath.Glob(filepath.Join(filepath.Dir(path), "*-recovery-*.jsonl")); err != nil || len(matches) != 0 {
73 t.Fatalf("recovery branches after owned resume rewrite = %v err=%v, want none", matches, err)
74 }
75 }
76
77 func TestMigratedSessionWithoutSystemPromptPersistsThroughDesktopSwitch(t *testing.T) {
78 isolateDesktopUserDirs(t)
79 src := t.TempDir()
80 dest := t.TempDir()
81 const legacy = `{"role":"user","content":"recovered after downgrade"}
82 {"role":"assistant","content":"legacy answer"}
83 `
84 if err := os.WriteFile(filepath.Join(src, "desktop-legacy.jsonl"), []byte(legacy), 0o644); err != nil {
85 t.Fatalf("write legacy session: %v", err)
86 }
87 if n, err := agent.MigrateLegacySessions(src, dest, nil); err != nil || n != 1 {
88 t.Fatalf("MigrateLegacySessions: n=%d err=%v", n, err)
89 }
90
91 path := filepath.Join(dest, "desktop-legacy.jsonl")
92 loaded, err := agent.LoadSession(path)
93 if err != nil {
94 t.Fatalf("LoadSession migrated: %v", err)
95 }
96 const freshSystem = "current deterministic system prompt"
97 prov := &capturingProvider{}
98 exec := agent.New(prov, tool.NewRegistry(), agent.NewSession(freshSystem), agent.Options{}, event.Discard)
99 ctrl := control.New(control.Options{
100 Runner: exec,
101 Executor: exec,
102 SystemPrompt: freshSystem,
103 SessionDir: dest,
104 SessionPath: path,
105 Label: "migrated",
106 Sink: event.Discard,
107 })
108 defer ctrl.Close()
109 resumeLoadedSessionAndGoal(ctrl, loaded, path, "")
110
111 if history := ctrl.History(); len(history) == 0 || history[0].Role != provider.RoleSystem || history[0].Content != freshSystem {
112 t.Fatalf("resumed history does not start with the fresh system prompt: %+v", history)
113 }
114 if err := ctrl.RunTurn(context.Background(), "new desktop turn"); err != nil {
115 t.Fatalf("RunTurn: %v", err)
116 }
117
118 active := &WorkspaceTab{
119 ID: "legacy",
120 Ctrl: ctrl,
121 Scope: "global",
122 SessionPath: path,
123 Ready: true,
124 disabledMCP: map[string]ServerView{},
125 }
126 target := &WorkspaceTab{
127 ID: "target",
128 Scope: "global",
129 Ready: true,
130 disabledMCP: map[string]ServerView{},
131 }
132 app := &App{
133 tabs: map[string]*WorkspaceTab{"legacy": active, "target": target},
134 tabOrder: []string{"legacy", "target"},
135 activeTabID: "legacy",
136 }
137 if err := app.SetActiveTab("target"); err != nil {
138 t.Fatalf("SetActiveTab: %v", err)
139 }
140
141 reloaded, err := agent.LoadSession(path)
142 if err != nil {
143 t.Fatalf("LoadSession after switch: %v", err)
144 }
145 got := reloaded.Snapshot()
146 if len(got) != 5 {
147 t.Fatalf("reloaded message count = %d, want 5: %+v", len(got), got)
148 }
149 if got[0].Role != provider.RoleSystem || got[0].Content != freshSystem {
150 t.Fatalf("reloaded system prompt = %+v, want %q", got[0], freshSystem)
151 }
152 if got[3].Role != provider.RoleUser || got[3].Content != "new desktop turn" {
153 t.Fatalf("reloaded new user turn = %+v", got[3])
154 }
155 if got[4].Role != provider.RoleAssistant || got[4].Content != "ok" {
156 t.Fatalf("reloaded assistant turn = %+v", got[4])
157 }
158 if matches, err := filepath.Glob(filepath.Join(dest, "*-recovery-*.jsonl")); err != nil || len(matches) != 0 {
159 t.Fatalf("recovery branches after migrated switch = %v err=%v, want none", matches, err)
160 }
161 }
162
163 func TestParallelDesktopTabsPersistCompleteTranscriptsAcrossReload(t *testing.T) {
164 isolateDesktopUserDirs(t)
165 const (
166 tabCount = 12
167 turnsPerTab = 6
168 systemText = "stable desktop system prompt"
169 )
170 dir := t.TempDir()
171 tabs := make(map[string]*WorkspaceTab, tabCount)
172 tabOrder := make([]string, 0, tabCount)
173 controllers := make([]*control.Controller, 0, tabCount)
174
175 for tabIndex := 0; tabIndex < tabCount; tabIndex++ {
176 id := fmt.Sprintf("parallel-%02d", tabIndex)
177 path := filepath.Join(dir, id+".jsonl")
178 prov := &capturingProvider{}
179 exec := agent.New(prov, tool.NewRegistry(), agent.NewSession(systemText), agent.Options{}, event.Discard)
180 ctrl := control.New(control.Options{
181 Runner: exec,
182 Executor: exec,
183 SystemPrompt: systemText,
184 SessionDir: dir,
185 SessionPath: path,
186 Label: id,
187 Sink: event.Discard,
188 })
189 controllers = append(controllers, ctrl)
190 tabOrder = append(tabOrder, id)
191 tabs[id] = &WorkspaceTab{
192 ID: id,
193 Ctrl: ctrl,
194 Scope: "global",
195 SessionPath: path,
196 Ready: true,
197 disabledMCP: map[string]ServerView{},
198 }
199 }
200 app := &App{
201 tabs: tabs,
202 tabOrder: tabOrder,
203 activeTabID: tabOrder[0],
204 }
205
206 start := make(chan struct{})
207 errs := make(chan error, tabCount+1)
208 var wg sync.WaitGroup
209 for tabIndex, ctrl := range controllers {
210 tabIndex, ctrl := tabIndex, ctrl
211 wg.Add(1)
212 go func() {
213 defer wg.Done()
214 <-start
215 for turn := 0; turn < turnsPerTab; turn++ {
216 input := fmt.Sprintf("tab-%02d-turn-%02d", tabIndex, turn)
217 if err := ctrl.RunTurn(context.Background(), input); err != nil {
218 errs <- fmt.Errorf("%s: %w", input, err)
219 return
220 }
221 }
222 }()
223 }
224 wg.Add(1)
225 go func() {
226 defer wg.Done()
227 <-start
228 for round := 0; round < 3; round++ {
229 for _, id := range tabOrder {
230 if err := app.SetActiveTab(id); err != nil {
231 errs <- fmt.Errorf("switch to %s: %w", id, err)
232 return
233 }
234 }
235 }
236 }()
237
238 close(start)
239 wg.Wait()
240 close(errs)
241 for err := range errs {
242 t.Error(err)
243 }
244 if t.Failed() {
245 for _, ctrl := range controllers {
246 ctrl.Close()
247 }
248 return
249 }
250
251 for _, ctrl := range controllers {
252 if err := ctrl.Snapshot(); err != nil {
253 t.Fatalf("final snapshot %s: %v", ctrl.Label(), err)
254 }
255 ctrl.Close()
256 }
257
258 for tabIndex, id := range tabOrder {
259 path := tabs[id].SessionPath
260 reloaded, err := agent.LoadSession(path)
261 if err != nil {
262 t.Fatalf("LoadSession %s: %v", id, err)
263 }
264 msgs := reloaded.Snapshot()
265 wantMessages := 1 + turnsPerTab*2
266 if len(msgs) != wantMessages {
267 t.Fatalf("%s message count = %d, want %d: %+v", id, len(msgs), wantMessages, msgs)
268 }
269 if msgs[0].Role != provider.RoleSystem || msgs[0].Content != systemText {
270 t.Fatalf("%s system message = %+v", id, msgs[0])
271 }
272 for turn := 0; turn < turnsPerTab; turn++ {
273 user := msgs[1+turn*2]
274 assistant := msgs[2+turn*2]
275 wantUser := fmt.Sprintf("tab-%02d-turn-%02d", tabIndex, turn)
276 if user.Role != provider.RoleUser || user.Content != wantUser {
277 t.Fatalf("%s turn %d user = %+v, want %q", id, turn, user, wantUser)
278 }
279 if assistant.Role != provider.RoleAssistant || assistant.Content != "ok" {
280 t.Fatalf("%s turn %d assistant = %+v, want ok", id, turn, assistant)
281 }
282 }
283 }
284 }
285
286 func TestResumeWithFreshSystemPromptPreservesLoadedRewriteBaseline(t *testing.T) {
287 path := filepath.Join(t.TempDir(), "session.jsonl")
288 s := agent.NewSession("old sys")
289 s.Add(provider.Message{Role: provider.RoleUser, Content: "first"})
290 s.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "tool-1", Name: "read_file", Arguments: "{}"}}})
291 s.Add(provider.Message{Role: provider.RoleTool, ToolCallID: "tool-1", Name: "read_file", Content: strings.Repeat("detail ", 100)})
292 s.Add(provider.Message{Role: provider.RoleAssistant, Content: "done"})
293 if err := s.Save(path); err != nil {
294 t.Fatalf("Save base: %v", err)
295 }
296
297 loaded, err := agent.LoadSession(path)
298 if err != nil {
299 t.Fatalf("LoadSession: %v", err)
300 }
301 ctrl := &promptResumeCtrl{history: []provider.Message{{Role: provider.RoleSystem, Content: "new sys"}}}
302 resumeWithFreshSystemPrompt(ctrl, loaded.Snapshot(), path)
303 if ctrl.resumed == nil {
304 t.Fatalf("Resume was not called")
305 }
306
307 msgs := ctrl.resumed.Snapshot()
308 msgs[3].Content = "[elided tool result]"
309 ctrl.resumed.Replace(msgs)
310 if err := ctrl.resumed.SaveRewrite(path); err != nil {
311 t.Fatalf("SaveRewrite resumed history: %v", err)
312 }
313
314 if got := ctrl.path; got != path {
315 t.Fatalf("resume path = %q, want %q", got, path)
316 }
317 reloaded, err := agent.LoadSession(path)
318 if err != nil {
319 t.Fatalf("LoadSession rewritten: %v", err)
320 }
321 if got := reloaded.Messages[0].Content; got != "new sys" {
322 t.Fatalf("system prompt after rewrite = %q, want new sys", got)
323 }
324 if got := reloaded.Messages[3].Content; got != "[elided tool result]" {
325 t.Fatalf("tool result after rewrite = %q, want elided", got)
326 }
327 if matches, err := filepath.Glob(filepath.Join(filepath.Dir(path), "*-recovery-*.jsonl")); err != nil || len(matches) != 0 {
328 t.Fatalf("recovery branches after resume rewrite = %v err=%v, want none", matches, err)
329 }
330 }
331
332 func TestResumeWithFreshSystemPromptRejectsStaleCarriedHistoryBaseline(t *testing.T) {
333 path := filepath.Join(t.TempDir(), "session.jsonl")
334 current := agent.NewSession("old sys")
335 current.Add(provider.Message{Role: provider.RoleUser, Content: "first"})
336 current.Add(provider.Message{Role: provider.RoleAssistant, Content: "one"})
337 current.Add(provider.Message{Role: provider.RoleUser, Content: "disk second"})
338 current.Add(provider.Message{Role: provider.RoleAssistant, Content: "disk two"})
339 if err := current.Save(path); err != nil {
340 t.Fatalf("Save current: %v", err)
341 }
342
343 stale := []provider.Message{
344 {Role: provider.RoleSystem, Content: "old sys"},
345 {Role: provider.RoleUser, Content: "first"},
346 {Role: provider.RoleAssistant, Content: "one"},
347 }
348 ctrl := &promptResumeCtrl{history: []provider.Message{{Role: provider.RoleSystem, Content: "new sys"}}}
349 resumeWithFreshSystemPrompt(ctrl, stale, path)
350 if ctrl.resumed == nil {
351 t.Fatalf("Resume was not called")
352 }
353 if err := ctrl.resumed.SaveRewrite(path); !errors.Is(err, agent.ErrSessionSnapshotConflict) {
354 t.Fatalf("SaveRewrite stale carried history err = %v, want ErrSessionSnapshotConflict", err)
355 }
356
357 reloaded, err := agent.LoadSession(path)
358 if err != nil {
359 t.Fatalf("LoadSession current: %v", err)
360 }
361 if got := reloaded.Messages[len(reloaded.Messages)-1].Content; got != "disk two" {
362 t.Fatalf("original tail after stale resume rewrite = %q, want disk two", got)
363 }
364 }
365
365 lines GO