返回 DeepSeek-Reasonix
tab_profile_test.go
根目录 / desktop / tab_profile_test.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "reasonix/internal/boot"
11 "reasonix/internal/control"
12 )
13
14 func testTab(id, root string) *WorkspaceTab {
15 return &WorkspaceTab{
16 ID: id,
17 Scope: "project",
18 WorkspaceRoot: root,
19 Ready: true,
20 Ctrl: control.New(control.Options{Label: id}),
21 model: "deepseek-flash/deepseek-v4-flash",
22 mode: "normal",
23 disabledMCP: map[string]ServerView{},
24 }
25 }
26
27 func TestSetEffortForTabIsTabLocal(t *testing.T) {
28 isolateDesktopUserDirs(t)
29
30 rootA := t.TempDir()
31 rootB := t.TempDir()
32 app := NewApp()
33 app.ctx = context.Background()
34 app.readyHook = func() {}
35 tabA := testTab("a", rootA)
36 tabB := testTab("b", rootB)
37 tabA.sink = &tabEventSink{tabID: tabA.ID, app: app}
38 tabB.sink = &tabEventSink{tabID: tabB.ID, app: app}
39 app.tabs = map[string]*WorkspaceTab{tabA.ID: tabA, tabB.ID: tabB}
40 app.tabOrder = []string{tabA.ID, tabB.ID}
41 app.activeTabID = tabA.ID
42 defer func() {
43 for _, tab := range app.tabs {
44 if tab.Ctrl != nil {
45 tab.Ctrl.Close()
46 }
47 }
48 }()
49
50 if err := app.SetEffortForTab(tabA.ID, "max"); err != nil {
51 t.Fatalf("SetEffortForTab: %v", err)
52 }
53 if got := app.EffortForTab(tabA.ID).Current; got != "max" {
54 t.Fatalf("tab A effort = %q, want max", got)
55 }
56 if got := app.EffortForTab(tabB.ID).Current; got != "auto" {
57 t.Fatalf("tab B effort = %q, want auto", got)
58 }
59 if tabB.effort != nil {
60 t.Fatalf("tab B stored effort = %q, want nil", *tabB.effort)
61 }
62 body, err := os.ReadFile(userConfigPathForTest())
63 if err == nil && strings.Contains(string(body), `effort`) {
64 t.Fatalf("tab-local effort should not write provider config:\n%s", body)
65 }
66 }
67
68 func TestEffortForTabResolvesProjectProviderConfig(t *testing.T) {
69 isolateDesktopUserDirs(t)
70
71 projectRoot := t.TempDir()
72 configBody := `default_model = "project-provider/deepseek-v4-flash"
73 [[providers]]
74 name = "project-provider"
75 kind = "openai"
76 base_url = "https://api.deepseek.com"
77 model = "deepseek-v4-flash"
78 api_key_env = "PROJECT_API_KEY"
79 effort = "max"
80 `
81 if err := os.WriteFile(filepath.Join(projectRoot, "reasonix.toml"), []byte(configBody), 0o644); err != nil {
82 t.Fatal(err)
83 }
84
85 app := NewApp()
86 tab := testTab("project", projectRoot)
87 tab.model = "project-provider/deepseek-v4-flash"
88 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
89 app.activeTabID = tab.ID
90 defer tab.Ctrl.Close()
91
92 got := app.EffortForTab(tab.ID)
93 if !got.Supported || got.Current != "max" {
94 t.Fatalf("EffortForTab project config = %+v, want supported max", got)
95 }
96 }
97
98 func TestEffortForTabUsesKnownModelRegistry(t *testing.T) {
99 isolateDesktopUserDirs(t)
100
101 projectRoot := t.TempDir()
102 configBody := `default_model = "project-provider/deepseek-v4-flash"
103 [[providers]]
104 name = "project-provider"
105 kind = "openai"
106 base_url = "https://proxy.example.com/v1"
107 model = "deepseek-v4-flash"
108 api_key_env = "PROJECT_API_KEY"
109 `
110 if err := os.WriteFile(filepath.Join(projectRoot, "reasonix.toml"), []byte(configBody), 0o644); err != nil {
111 t.Fatal(err)
112 }
113
114 app := NewApp()
115 tab := testTab("project", projectRoot)
116 tab.model = "project-provider/deepseek-v4-flash"
117 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
118 app.activeTabID = tab.ID
119 defer tab.Ctrl.Close()
120
121 got := app.EffortForTab(tab.ID)
122 if !got.Supported || got.Current != "auto" || got.Default != "high" {
123 t.Fatalf("EffortForTab model registry = %+v, want supported auto/high", got)
124 }
125 wantLevels := []string{"auto", "disabled", "low", "high", "max"}
126 if len(got.Levels) != len(wantLevels) {
127 t.Fatalf("levels = %v, want %v", got.Levels, wantLevels)
128 }
129 for i, want := range wantLevels {
130 if got.Levels[i] != want {
131 t.Fatalf("levels[%d] = %q, want %q", i, got.Levels[i], want)
132 }
133 }
134 }
135
136 func TestSaveTabsPersistsModelAndEffort(t *testing.T) {
137 isolateDesktopUserDirs(t)
138
139 effort := "max"
140 app := NewApp()
141 tab := testTab("a", t.TempDir())
142 tab.effort = &effort
143 tab.model = "deepseek/deepseek-v4-pro"
144 tab.mode = "plan"
145 tab.Ctrl.SetPlanMode(true)
146 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
147 app.tabOrder = []string{tab.ID}
148 app.activeTabID = tab.ID
149
150 app.mu.Lock()
151 app.saveTabsLocked()
152 app.mu.Unlock()
153
154 got := loadTabsFile()
155 if len(got.Tabs) != 1 {
156 t.Fatalf("tabs len = %d, want 1", len(got.Tabs))
157 }
158 if got.Tabs[0].Model != tab.model {
159 t.Fatalf("saved model = %q, want %q", got.Tabs[0].Model, tab.model)
160 }
161 if got.Tabs[0].Effort == nil || *got.Tabs[0].Effort != effort {
162 t.Fatalf("saved effort = %#v, want %q", got.Tabs[0].Effort, effort)
163 }
164 if got.Tabs[0].Mode != "plan" {
165 t.Fatalf("saved mode = %q, want plan", got.Tabs[0].Mode)
166 }
167 }
168
169 func TestBuildTabControllerMigratesPersistedBareMimoModel(t *testing.T) {
170 isolateDesktopUserDirs(t)
171
172 root := t.TempDir()
173 configBody := `default_model = "deepseek-flash"
174 [[providers]]
175 name = "deepseek-flash"
176 kind = "openai"
177 base_url = "https://example.invalid/v1"
178 model = "deepseek-v4-flash"
179 api_key_env = "REASONIX_TEST_KEY_UNSET"
180 `
181 if err := os.WriteFile(filepath.Join(root, "reasonix.toml"), []byte(configBody), 0o644); err != nil {
182 t.Fatal(err)
183 }
184
185 app := NewApp()
186 app.readyHook = func() {}
187 tab := testTab("mimo", root)
188 tab.model = "mimo-v2.5-pro"
189 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
190 app.tabOrder = []string{tab.ID}
191 app.activeTabID = tab.ID
192
193 app.buildTabController(tab)
194 if tab.StartupErr != "" {
195 t.Fatalf("tab startup error = %q", tab.StartupErr)
196 }
197 defer tab.Ctrl.Close()
198 if tab.model != "mimo-pro/mimo-v2.5-pro" {
199 t.Fatalf("tab model = %q, want migrated MiMo provider ref", tab.model)
200 }
201 }
202
203 func TestSaveTabsPersistsNonBalancedTokenModes(t *testing.T) {
204 isolateDesktopUserDirs(t)
205
206 app := NewApp()
207 tab := testTab("a", t.TempDir())
208 tab.tokenMode = "economy"
209 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
210 app.tabOrder = []string{tab.ID}
211 app.activeTabID = tab.ID
212
213 app.mu.Lock()
214 app.saveTabsLocked()
215 app.mu.Unlock()
216
217 got := loadTabsFile()
218 if len(got.Tabs) != 1 {
219 t.Fatalf("tabs len = %d, want 1", len(got.Tabs))
220 }
221 if got.Tabs[0].TokenMode != "economy" {
222 t.Fatalf("saved token mode = %q, want economy", got.Tabs[0].TokenMode)
223 }
224
225 tab.tokenMode = "delivery"
226 app.mu.Lock()
227 app.saveTabsLocked()
228 app.mu.Unlock()
229
230 got = loadTabsFile()
231 if got.Tabs[0].TokenMode != "delivery" {
232 t.Fatalf("saved token mode = %q, want delivery", got.Tabs[0].TokenMode)
233 }
234
235 tab.tokenMode = "full"
236 app.mu.Lock()
237 app.saveTabsLocked()
238 app.mu.Unlock()
239
240 got = loadTabsFile()
241 if got.Tabs[0].TokenMode != "" {
242 t.Fatalf("balanced/full token mode should be omitted from persistence, got %q", got.Tabs[0].TokenMode)
243 }
244 }
245
246 // TestSaveTabsPersistsYoloMode is the regression for #3517: yolo used to be
247 // dropped on save, so relaunching reverted to normal. It now round-trips through
248 // the real saveTabsLocked/loadTabsFile path.
249 func TestSaveTabsPersistsYoloMode(t *testing.T) {
250 isolateDesktopUserDirs(t)
251
252 app := NewApp()
253 tab := testTab("a", t.TempDir())
254 tab.mode = "yolo"
255 tab.Ctrl.SetAutoApproveTools(true)
256 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
257 app.tabOrder = []string{tab.ID}
258 app.activeTabID = tab.ID
259
260 app.mu.Lock()
261 app.saveTabsLocked()
262 app.mu.Unlock()
263
264 got := loadTabsFile()
265 if len(got.Tabs) != 1 {
266 t.Fatalf("tabs len = %d, want 1", len(got.Tabs))
267 }
268 if got.Tabs[0].Mode != "yolo" {
269 t.Fatalf("saved yolo mode = %q, want yolo (#3517)", got.Tabs[0].Mode)
270 }
271 }
272
273 func TestSaveTabsPersistsPlanYoloMode(t *testing.T) {
274 isolateDesktopUserDirs(t)
275
276 app := NewApp()
277 tab := testTab("a", t.TempDir())
278 tab.mode = "plan-yolo"
279 tab.Ctrl.SetMode(true, true)
280 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
281 app.tabOrder = []string{tab.ID}
282 app.activeTabID = tab.ID
283
284 app.mu.Lock()
285 app.saveTabsLocked()
286 app.mu.Unlock()
287
288 got := loadTabsFile()
289 if len(got.Tabs) != 1 {
290 t.Fatalf("tabs len = %d, want 1", len(got.Tabs))
291 }
292 if got.Tabs[0].Mode != "plan-yolo" {
293 t.Fatalf("saved mode = %q, want plan-yolo", got.Tabs[0].Mode)
294 }
295 }
296
297 func TestSaveTabsPersistsGoalAndToolApprovalMode(t *testing.T) {
298 isolateDesktopUserDirs(t)
299
300 app := NewApp()
301 tab := testTab("a", t.TempDir())
302 tab.goal = "finish the mode redesign"
303 tab.toolApprovalMode = control.ToolApprovalAuto
304 tab.Ctrl.SetGoal(tab.goal)
305 tab.Ctrl.SetToolApprovalMode(control.ToolApprovalAuto)
306 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
307 app.tabOrder = []string{tab.ID}
308 app.activeTabID = tab.ID
309
310 app.mu.Lock()
311 app.saveTabsLocked()
312 app.mu.Unlock()
313
314 got := loadTabsFile()
315 if len(got.Tabs) != 1 {
316 t.Fatalf("tabs len = %d, want 1", len(got.Tabs))
317 }
318 if got.Tabs[0].Goal != "finish the mode redesign" {
319 t.Fatalf("saved goal = %q", got.Tabs[0].Goal)
320 }
321 if got.Tabs[0].ToolApprovalMode != control.ToolApprovalAuto {
322 t.Fatalf("saved tool approval mode = %q, want auto", got.Tabs[0].ToolApprovalMode)
323 }
324 }
325
326 func TestCollaborationModesPreserveToolApprovalMode(t *testing.T) {
327 isolateDesktopUserDirs(t)
328
329 app := NewApp()
330 tab := testTab("a", t.TempDir())
331 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
332 app.tabOrder = []string{tab.ID}
333 app.activeTabID = tab.ID
334 defer tab.Ctrl.Close()
335
336 app.SetToolApprovalModeForTab(tab.ID, control.ToolApprovalAuto)
337 app.SetGoalForTab(tab.ID, "finish the approval redesign")
338 if got := currentTabCollaborationMode(tab); got != "goal" {
339 t.Fatalf("collaboration mode = %q, want goal", got)
340 }
341 if tab.Ctrl.PlanMode() {
342 t.Fatal("goal mode should leave plan mode off")
343 }
344 if got := tab.Ctrl.ToolApprovalMode(); got != control.ToolApprovalAuto {
345 t.Fatalf("tool approval after goal = %q, want auto", got)
346 }
347
348 app.SetCollaborationModeForTab(tab.ID, "plan")
349 if got := currentTabCollaborationMode(tab); got != "plan" {
350 t.Fatalf("collaboration mode = %q, want plan", got)
351 }
352 if got := tab.Ctrl.Goal(); got != "" {
353 t.Fatalf("plan mode should clear goal, got %q", got)
354 }
355 if got := tab.Ctrl.ToolApprovalMode(); got != control.ToolApprovalAuto {
356 t.Fatalf("tool approval after plan = %q, want auto", got)
357 }
358
359 app.SetCollaborationModeForTab(tab.ID, "normal")
360 if got := currentTabCollaborationMode(tab); got != "normal" {
361 t.Fatalf("collaboration mode = %q, want normal", got)
362 }
363 if tab.Ctrl.PlanMode() {
364 t.Fatal("normal mode should leave plan mode off")
365 }
366 if got := tab.Ctrl.ToolApprovalMode(); got != control.ToolApprovalAuto {
367 t.Fatalf("tool approval after normal = %q, want auto", got)
368 }
369 }
370
371 func TestToolApprovalModesPreserveCollaborationMode(t *testing.T) {
372 isolateDesktopUserDirs(t)
373
374 app := NewApp()
375 tab := testTab("a", t.TempDir())
376 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
377 app.tabOrder = []string{tab.ID}
378 app.activeTabID = tab.ID
379 defer tab.Ctrl.Close()
380
381 app.SetCollaborationModeForTab(tab.ID, "plan")
382 for _, mode := range []string{control.ToolApprovalAsk, control.ToolApprovalAuto, control.ToolApprovalYolo} {
383 app.SetToolApprovalModeForTab(tab.ID, mode)
384 if got := currentTabCollaborationMode(tab); got != "plan" {
385 t.Fatalf("collaboration after tool mode %q = %q, want plan", mode, got)
386 }
387 if got := tab.Ctrl.ToolApprovalMode(); got != mode {
388 t.Fatalf("tool approval = %q, want %q", got, mode)
389 }
390 }
391
392 app.SetGoalForTab(tab.ID, "ship the goal runner")
393 app.SetToolApprovalModeForTab(tab.ID, control.ToolApprovalAsk)
394 if got := currentTabCollaborationMode(tab); got != "goal" {
395 t.Fatalf("collaboration after ask = %q, want goal", got)
396 }
397 app.SetToolApprovalModeForTab(tab.ID, control.ToolApprovalAuto)
398 if got := currentTabCollaborationMode(tab); got != "goal" {
399 t.Fatalf("collaboration after auto = %q, want goal", got)
400 }
401 app.SetToolApprovalModeForTab(tab.ID, control.ToolApprovalYolo)
402 if got := currentTabCollaborationMode(tab); got != "goal" {
403 t.Fatalf("collaboration after yolo = %q, want goal", got)
404 }
405 }
406
407 func TestSetComposerProfileForTabAppliesAllAxes(t *testing.T) {
408 isolateDesktopUserDirs(t)
409
410 app := NewApp()
411 if drained, err := app.SetComposerProfileForTab("missing", "normal", control.ToolApprovalAsk, ""); err == nil || drained == nil {
412 t.Fatal("missing tab returned a nil drained-id slice")
413 }
414 tab := testTab("a", t.TempDir())
415 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
416 app.tabOrder = []string{tab.ID}
417 app.activeTabID = tab.ID
418 defer tab.Ctrl.Close()
419
420 if _, err := app.SetComposerProfileForTab(tab.ID, "plan", control.ToolApprovalYolo, ""); err != nil {
421 t.Fatal(err)
422 }
423 if got := currentTabCollaborationMode(tab); got != "plan" {
424 t.Fatalf("collaboration mode = %q, want plan", got)
425 }
426 if !tab.Ctrl.PlanMode() {
427 t.Fatal("controller plan mode is off")
428 }
429 if got := tab.Ctrl.ToolApprovalMode(); got != control.ToolApprovalYolo {
430 t.Fatalf("tool approval mode = %q, want yolo", got)
431 }
432
433 if _, err := app.SetComposerProfileForTab(tab.ID, "goal", control.ToolApprovalAuto, "finish the profile migration"); err != nil {
434 t.Fatal(err)
435 }
436 if got := currentTabCollaborationMode(tab); got != "goal" {
437 t.Fatalf("collaboration mode = %q, want goal", got)
438 }
439 if tab.Ctrl.PlanMode() {
440 t.Fatal("goal profile kept controller plan mode on")
441 }
442 if got := tab.Ctrl.ToolApprovalMode(); got != control.ToolApprovalAuto {
443 t.Fatalf("tool approval mode = %q, want auto", got)
444 }
445 if got := tab.Ctrl.Goal(); got != "finish the profile migration" {
446 t.Fatalf("controller goal = %q", got)
447 }
448
449 got := loadTabsFile()
450 if len(got.Tabs) != 1 || got.Tabs[0].Goal != "finish the profile migration" || got.Tabs[0].ToolApprovalMode != control.ToolApprovalAuto {
451 t.Fatalf("persisted profile = %+v", got.Tabs)
452 }
453 }
454
455 func TestMetaReportsGoalStatus(t *testing.T) {
456 isolateDesktopUserDirs(t)
457
458 app := NewApp()
459 tab := testTab("a", t.TempDir())
460 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
461 app.tabOrder = []string{tab.ID}
462 app.activeTabID = tab.ID
463 defer tab.Ctrl.Close()
464
465 meta := app.MetaForTab(tab.ID)
466 if meta.GoalStatus != control.GoalStatusStopped {
467 t.Fatalf("initial goal status = %q, want stopped", meta.GoalStatus)
468 }
469 if meta.CollaborationMode != "normal" {
470 t.Fatalf("initial collaboration mode = %q, want normal", meta.CollaborationMode)
471 }
472
473 app.SetGoalForTab(tab.ID, "finish the goal runner")
474 meta = app.MetaForTab(tab.ID)
475 if meta.Goal != "finish the goal runner" || meta.GoalStatus != control.GoalStatusRunning || meta.CollaborationMode != "goal" {
476 t.Fatalf("goal meta = %+v, want running goal", meta)
477 }
478
479 app.ClearGoalForTab(tab.ID)
480 meta = app.MetaForTab(tab.ID)
481 if meta.Goal != "" || meta.GoalStatus != control.GoalStatusStopped || meta.CollaborationMode != "normal" {
482 t.Fatalf("cleared goal meta = %+v, want stopped empty goal", meta)
483 }
484
485 app.SetCollaborationModeForTab(tab.ID, "plan")
486 tab.tokenMode = boot.TokenModeEconomy
487 meta = app.MetaForTab(tab.ID)
488 if meta.CollaborationMode != "plan" || meta.TokenMode != boot.TokenModeEconomy {
489 t.Fatalf("profile meta = %+v, want plan + economy", meta)
490 }
491 }
492
493 func TestAutoResearchStatusSurfaceForActiveTab(t *testing.T) {
494 isolateDesktopUserDirs(t)
495
496 root := t.TempDir()
497 if resolved, err := filepath.EvalSymlinks(root); err == nil {
498 root = resolved
499 }
500 app := NewApp()
501 tab := testTab("a", root)
502 tab.Ctrl = control.New(control.Options{Label: tab.ID, WorkspaceRoot: root})
503 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
504 app.tabOrder = []string{tab.ID}
505 app.activeTabID = tab.ID
506 defer tab.Ctrl.Close()
507
508 tab.Ctrl.SetGoalWithResearchMode("identify the root cause", control.GoalResearchOn)
509
510 meta := app.MetaForTab(tab.ID)
511 if meta.AutoResearch == nil || meta.AutoResearch.TaskID == "" {
512 t.Fatalf("MetaForTab AutoResearch = %+v, want compact active task summary", meta.AutoResearch)
513 }
514 if meta.AutoResearch.Status != control.GoalStatusRunning || meta.AutoResearch.Iteration != 0 {
515 t.Fatalf("compact AutoResearch summary = %+v", meta.AutoResearch)
516 }
517
518 current := app.AutoResearchCurrent()
519 if current.TaskID != meta.AutoResearch.TaskID || current.Goal != "identify the root cause" {
520 t.Fatalf("AutoResearchCurrent = %+v, want task %q", current, meta.AutoResearch.TaskID)
521 }
522 if current.TaskPath == "" || current.Status != control.GoalStatusRunning {
523 t.Fatalf("AutoResearchCurrent missing status/path: %+v", current)
524 }
525 heartbeatPath := filepath.Join(root, ".reasonix", "autoresearch", current.TaskID, "logs", "heartbeat.jsonl")
526 if err := os.WriteFile(heartbeatPath, []byte(`{"status":"turn_done","iteration":1,"created_at":"2026-06-30T00:00:00Z"}`+"\n"), 0o644); err != nil {
527 t.Fatalf("write heartbeat: %v", err)
528 }
529 current = app.AutoResearchCurrent()
530 if current.LastHeartbeatAt != "2026-06-30T00:00:00Z" || current.NextRequiredAction == "" {
531 t.Fatalf("AutoResearchCurrent missing runtime fields: %+v", current)
532 }
533
534 tabs := app.ListTabs()
535 if len(tabs) != 1 || tabs[0].AutoResearch == nil || tabs[0].AutoResearch.TaskID != current.TaskID {
536 t.Fatalf("ListTabs AutoResearch = %+v, want compact summary for task %q", tabs, current.TaskID)
537 }
538 }
539
540 func TestAutoResearchFindingsAreLoadedOnDemand(t *testing.T) {
541 isolateDesktopUserDirs(t)
542
543 root := t.TempDir()
544 if resolved, err := filepath.EvalSymlinks(root); err == nil {
545 root = resolved
546 }
547 app := NewApp()
548 tab := testTab("a", root)
549 tab.Ctrl = control.New(control.Options{Label: tab.ID, WorkspaceRoot: root})
550 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
551 app.tabOrder = []string{tab.ID}
552 app.activeTabID = tab.ID
553 defer tab.Ctrl.Close()
554
555 tab.Ctrl.SetGoalWithResearchMode("collect accepted findings", control.GoalResearchOn)
556 current := app.AutoResearchCurrent()
557 if current.TaskID == "" {
558 t.Fatal("expected active AutoResearch task")
559 }
560 findingsPath := filepath.Join(root, ".reasonix", "autoresearch", current.TaskID, "state", "findings.jsonl")
561 if err := os.WriteFile(findingsPath, []byte(
562 `{"id":"f1","kind":"test","summary":"old","source":"command","command":"go test ./...","accepted":true,"created_at":"2026-06-29T10:00:00Z"}`+"\n"+
563 `{"id":"f2","kind":"review","summary":"new","source":"manual","accepted":true,"created_at":"2026-06-29T11:00:00Z"}`+"\n",
564 ), 0o644); err != nil {
565 t.Fatalf("write findings: %v", err)
566 }
567
568 findings := app.AutoResearchFindings(tab.ID, 1)
569 if len(findings) != 1 || findings[0].ID != "f2" || findings[0].Summary != "new" {
570 t.Fatalf("AutoResearchFindings = %+v, want newest capped finding", findings)
571 }
572 if meta := app.MetaForTab(tab.ID); meta.AutoResearch == nil || meta.AutoResearch.TaskID != current.TaskID {
573 t.Fatalf("MetaForTab compact summary lost task id: %+v", meta.AutoResearch)
574 }
575 }
576
577 func TestAutoResearchListReturnsWorkspaceTasks(t *testing.T) {
578 isolateDesktopUserDirs(t)
579
580 root := t.TempDir()
581 if resolved, err := filepath.EvalSymlinks(root); err == nil {
582 root = resolved
583 }
584 app := NewApp()
585 tab := testTab("a", root)
586 tab.Ctrl = control.New(control.Options{Label: tab.ID, WorkspaceRoot: root})
587 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
588 app.tabOrder = []string{tab.ID}
589 app.activeTabID = tab.ID
590 defer tab.Ctrl.Close()
591
592 tab.Ctrl.SetGoalWithResearchMode("list active research task", control.GoalResearchOn)
593 list := app.AutoResearchList(tab.ID)
594 if len(list) != 1 || list[0].TaskID == "" || list[0].Goal != "list active research task" {
595 t.Fatalf("AutoResearchList = %+v, want active workspace task", list)
596 }
597 }
598
599 func TestAutoResearchOpenTaskRevealsTaskDirectory(t *testing.T) {
600 isolateDesktopUserDirs(t)
601
602 root := t.TempDir()
603 if resolved, err := filepath.EvalSymlinks(root); err == nil {
604 root = resolved
605 }
606 app := NewApp()
607 tab := testTab("a", root)
608 tab.Ctrl = control.New(control.Options{Label: tab.ID, WorkspaceRoot: root})
609 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
610 app.tabOrder = []string{tab.ID}
611 app.activeTabID = tab.ID
612 defer tab.Ctrl.Close()
613
614 tab.Ctrl.SetGoalWithResearchMode("open task folder", control.GoalResearchOn)
615 current := app.AutoResearchCurrent()
616 var revealed string
617 oldReveal := revealPath
618 revealPath = func(path string) error {
619 revealed = path
620 return nil
621 }
622 defer func() { revealPath = oldReveal }()
623
624 if err := app.AutoResearchOpenTask(tab.ID); err != nil {
625 t.Fatalf("AutoResearchOpenTask: %v", err)
626 }
627 if revealed != current.TaskPath {
628 t.Fatalf("revealed path = %q, want task path %q", revealed, current.TaskPath)
629 }
630 }
631
632 func TestAutoResearchRecordEvidenceThroughDesktopAPI(t *testing.T) {
633 isolateDesktopUserDirs(t)
634
635 root := t.TempDir()
636 if resolved, err := filepath.EvalSymlinks(root); err == nil {
637 root = resolved
638 }
639 app := NewApp()
640 tab := testTab("a", root)
641 tab.Ctrl = control.New(control.Options{Label: tab.ID, WorkspaceRoot: root})
642 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
643 app.tabOrder = []string{tab.ID}
644 app.activeTabID = tab.ID
645 defer tab.Ctrl.Close()
646
647 tab.Ctrl.SetGoalWithResearchMode("record evidence through desktop", control.GoalResearchOn)
648 err := app.AutoResearchRecordEvidence(tab.ID, "objective_evidence", AutoResearchEvidenceView{
649 ID: "f1",
650 Kind: "test",
651 Summary: "desktop evidence recorded",
652 Source: "manual",
653 Accepted: true,
654 })
655 if err != nil {
656 t.Fatalf("AutoResearchRecordEvidence: %v", err)
657 }
658 findings := app.AutoResearchFindings(tab.ID, 10)
659 if len(findings) != 1 || findings[0].ID != "f1" {
660 t.Fatalf("findings = %+v, want f1", findings)
661 }
662 }
663
664 func TestMetaReportsStoredCollaborationModeWhileControllerRebuilds(t *testing.T) {
665 isolateDesktopUserDirs(t)
666
667 app := NewApp()
668 tab := testTab("a", t.TempDir())
669 tab.Ctrl.Close()
670 tab.Ctrl = nil
671 tab.mode = "plan-yolo"
672 tab.toolApprovalMode = control.ToolApprovalYolo
673 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
674 app.tabOrder = []string{tab.ID}
675 app.activeTabID = tab.ID
676
677 meta := app.MetaForTab(tab.ID)
678 if meta.CollaborationMode != "plan" || meta.ToolApprovalMode != control.ToolApprovalYolo {
679 t.Fatalf("rebuilding plan-yolo meta = %+v, want plan/yolo", meta)
680 }
681
682 tab.mode = "normal"
683 tab.goal = "finish the goal runner"
684 meta = app.MetaForTab(tab.ID)
685 if meta.CollaborationMode != "goal" || meta.Goal != "finish the goal runner" || meta.GoalStatus != control.GoalStatusRunning {
686 t.Fatalf("rebuilding goal meta = %+v, want running goal", meta)
687 }
688 }
689
690 func TestSetPlanModePreservesAutoApproveTools(t *testing.T) {
691 isolateDesktopUserDirs(t)
692
693 app := NewApp()
694 tab := testTab("a", t.TempDir())
695 tab.Ctrl.SetAutoApproveTools(true)
696 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
697 app.tabOrder = []string{tab.ID}
698 app.activeTabID = tab.ID
699
700 app.SetPlanMode(true)
701 if !tab.Ctrl.PlanMode() || !tab.Ctrl.AutoApproveTools() {
702 t.Fatalf("after SetPlanMode(true): plan=%v autoApproveTools=%v, want true/true", tab.Ctrl.PlanMode(), tab.Ctrl.AutoApproveTools())
703 }
704 if got := currentTabMode(tab); got != "plan-yolo" {
705 t.Fatalf("current mode = %q, want plan-yolo", got)
706 }
707
708 app.SetPlanMode(false)
709 if tab.Ctrl.PlanMode() || !tab.Ctrl.AutoApproveTools() {
710 t.Fatalf("after SetPlanMode(false): plan=%v autoApproveTools=%v, want false/true", tab.Ctrl.PlanMode(), tab.Ctrl.AutoApproveTools())
711 }
712 }
713
714 func TestSetBypassPreservesPlanMode(t *testing.T) {
715 isolateDesktopUserDirs(t)
716
717 app := NewApp()
718 tab := testTab("a", t.TempDir())
719 tab.Ctrl.SetPlanMode(true)
720 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
721 app.tabOrder = []string{tab.ID}
722 app.activeTabID = tab.ID
723
724 app.SetBypass(true)
725 if !tab.Ctrl.PlanMode() || !tab.Ctrl.AutoApproveTools() {
726 t.Fatalf("after SetBypass(true): plan=%v autoApproveTools=%v, want true/true", tab.Ctrl.PlanMode(), tab.Ctrl.AutoApproveTools())
727 }
728 if got := currentTabMode(tab); got != "plan-yolo" {
729 t.Fatalf("current mode = %q, want plan-yolo", got)
730 }
731
732 app.SetBypass(false)
733 if !tab.Ctrl.PlanMode() || tab.Ctrl.AutoApproveTools() {
734 t.Fatalf("after SetBypass(false): plan=%v autoApproveTools=%v, want true/false", tab.Ctrl.PlanMode(), tab.Ctrl.AutoApproveTools())
735 }
736 }
737
738 func userConfigPathForTest() string {
739 if dir, err := os.UserConfigDir(); err == nil {
740 return dir + "/reasonix/reasonix.toml"
741 }
742 return ""
743 }
744
744 lines GO