返回 DeepSeek-Reasonix
new_session_inherit_test.go
根目录 / desktop / new_session_inherit_test.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "sync/atomic"
9 "testing"
10 "time"
11
12 "reasonix/internal/agent"
13 "reasonix/internal/config"
14 "reasonix/internal/control"
15 )
16
17 func TestEnsureBlankTabInheritsActiveTabLocalSettings(t *testing.T) {
18 isolateDesktopUserDirs(t)
19 workspace := robustTempDir(t)
20 if err := os.WriteFile(filepath.Join(workspace, "reasonix.toml"),
21 []byte(""), 0o644); err != nil {
22 t.Fatalf("write workspace config: %v", err)
23 }
24
25 effort := "max"
26 app := NewApp()
27 src := &WorkspaceTab{
28 ID: "src",
29 Scope: "project",
30 WorkspaceRoot: workspace,
31 TopicID: "topic_src",
32 SessionPath: filepath.Join(workspace, "src.jsonl"), // non-empty so src isn't reused as the blank tab
33 model: "inherit/model",
34 effort: &effort,
35 tokenMode: "economy",
36 mode: "plan",
37 toolApprovalMode: control.ToolApprovalYolo,
38 disabledMCP: map[string]ServerView{"srv-x": {}},
39 mcpOrder: []string{"srv-x", "srv-y"},
40 }
41 src.sink = &tabEventSink{tabID: "src", app: app}
42 app.tabs["src"] = src
43 app.tabOrder = []string{"src"}
44 app.activeTabID = "src"
45
46 meta, err := app.EnsureBlankTab("project", workspace)
47 if err != nil {
48 t.Fatalf("EnsureBlankTab: %v", err)
49 }
50 if meta.ID == "" || meta.ID == "src" {
51 t.Fatalf("expected a fresh tab, got meta.ID=%q", meta.ID)
52 }
53
54 created := app.tabs[meta.ID]
55 if created == nil {
56 t.Fatalf("new tab %q missing from app.tabs", meta.ID)
57 }
58 if created.effort == nil || *created.effort != "max" {
59 t.Fatalf("effort = %v, want inherited \"max\"", created.effort)
60 }
61 if created.tokenMode != "economy" {
62 t.Fatalf("tokenMode = %q, want inherited \"economy\"", created.tokenMode)
63 }
64 if created.toolApprovalMode != control.ToolApprovalAuto {
65 t.Fatalf("toolApprovalMode = %q, want global default auto", created.toolApprovalMode)
66 }
67 if created.mode != "normal" {
68 t.Fatalf("mode = %q, want global default normal", created.mode)
69 }
70 if _, ok := created.disabledMCP["srv-x"]; !ok {
71 t.Fatalf("disabledMCP = %v, want inherited key \"srv-x\"", created.disabledMCP)
72 }
73 if len(created.mcpOrder) != 2 || created.mcpOrder[0] != "srv-x" {
74 t.Fatalf("mcpOrder = %v, want inherited [srv-x srv-y]", created.mcpOrder)
75 }
76 }
77
78 func TestEnsureBlankTabUsesGlobalSessionDefaultsForModelAndToolApproval(t *testing.T) {
79 isolateDesktopUserDirs(t)
80 // Seed the key into Reasonix's user credentials store (the only source
81 // Configured() consults) so the default resolves as configured and is
82 // inherited verbatim regardless of the developer machine's environment.
83 seedUserCredentials(t, "DEEPSEEK_API_KEY=test-key\n")
84 workspace := robustTempDir(t)
85 if err := os.WriteFile(filepath.Join(workspace, "reasonix.toml"),
86 []byte(""), 0o644); err != nil {
87 t.Fatalf("write workspace config: %v", err)
88 }
89
90 cfg := config.LoadForEdit(config.UserConfigPath())
91 if err := cfg.SetDefaultModel("deepseek-pro/deepseek-v4-pro"); err != nil {
92 t.Fatalf("SetDefaultModel: %v", err)
93 }
94 if err := cfg.SetDesktopDefaultToolApprovalMode(control.ToolApprovalAuto); err != nil {
95 t.Fatalf("SetDesktopDefaultToolApprovalMode: %v", err)
96 }
97 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
98 t.Fatalf("save user config: %v", err)
99 }
100
101 app := NewApp()
102 src := &WorkspaceTab{
103 ID: "src",
104 Scope: "project",
105 WorkspaceRoot: workspace,
106 TopicID: "topic_src",
107 SessionPath: filepath.Join(workspace, "src.jsonl"),
108 model: "deepseek-flash/deepseek-v4-flash",
109 mode: "plan",
110 toolApprovalMode: control.ToolApprovalAsk,
111 disabledMCP: map[string]ServerView{},
112 }
113 src.sink = &tabEventSink{tabID: "src", app: app}
114 app.tabs["src"] = src
115 app.tabOrder = []string{"src"}
116 app.activeTabID = "src"
117
118 meta, err := app.EnsureBlankTab("project", workspace)
119 if err != nil {
120 t.Fatalf("EnsureBlankTab: %v", err)
121 }
122 created := app.tabs[meta.ID]
123 if created == nil {
124 t.Fatalf("new tab %q missing from app.tabs", meta.ID)
125 }
126 if created.model != "deepseek-pro/deepseek-v4-pro" {
127 t.Fatalf("new tab model = %q, want global default model", created.model)
128 }
129 if created.toolApprovalMode != control.ToolApprovalAuto {
130 t.Fatalf("new tab toolApprovalMode = %q, want global default auto", created.toolApprovalMode)
131 }
132 if src.model != "deepseek-flash/deepseek-v4-flash" || src.toolApprovalMode != control.ToolApprovalAsk {
133 t.Fatalf("existing tab should not be overwritten, got model=%q approval=%q", src.model, src.toolApprovalMode)
134 }
135 if !strings.Contains(filepath.Base(created.SessionPath), "deepseek-v4-pro") {
136 t.Fatalf("new session path = %q, want filename seeded by global default model", created.SessionPath)
137 }
138 }
139
140 func TestEnsureBlankTabRetargetsReusedSameNameModelToDefaultProvider(t *testing.T) {
141 isolateDesktopUserDirs(t)
142 model, oldRef, defaultRef := configureSameNameModelProviders(t)
143
144 globalRoot := globalWorkspaceRoot()
145 path, err := createEmptySessionFile(desktopSessionDir(globalRoot), model)
146 if err != nil {
147 t.Fatalf("create empty session: %v", err)
148 }
149 if err := agent.SetBranchModelPreserveUpdated(path, oldRef); err != nil {
150 t.Fatalf("seed old session model: %v", err)
151 }
152
153 app := NewApp()
154 app.ctx = context.Background()
155 tab := testTab("reused", globalRoot)
156 tab.Scope = "global"
157 tab.WorkspaceRoot = globalRoot
158 tab.SessionPath = path
159 tab.model = oldRef
160 tab.Ctrl.SetSessionPath(path)
161 tab.sink = &tabEventSink{tabID: tab.ID, app: app}
162 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
163 app.tabOrder = []string{tab.ID}
164 app.activeTabID = tab.ID
165 t.Cleanup(func() {
166 if tab.Ctrl != nil {
167 tab.Ctrl.Close()
168 }
169 tab.releaseSessionLease()
170 })
171
172 meta, err := app.EnsureBlankTab("global", "")
173 if err != nil {
174 t.Fatalf("EnsureBlankTab: %v", err)
175 }
176 if meta.ID != tab.ID {
177 t.Fatalf("reused tab = %q, want %q", meta.ID, tab.ID)
178 }
179 if tab.model != defaultRef || tab.Ctrl.ModelRef() != defaultRef {
180 t.Fatalf("reused runtime model = tab:%q controller:%q, want %q", tab.model, tab.Ctrl.ModelRef(), defaultRef)
181 }
182 if stored, ok := agent.LoadSessionModel(tab.currentSessionPath()); !ok || stored != defaultRef {
183 t.Fatalf("stored session model = %q, %v, want %q", stored, ok, defaultRef)
184 }
185
186 current := ""
187 for _, info := range app.ModelsForTab(tab.ID) {
188 if info.Current {
189 if current != "" {
190 t.Fatalf("multiple current models: %q and %q", current, info.Ref)
191 }
192 current = info.Ref
193 }
194 }
195 if current != defaultRef {
196 t.Fatalf("model switcher current ref = %q, want %q", current, defaultRef)
197 }
198 }
199
200 func TestEnsureBlankTabRepairsStaleStoredProviderWhenRuntimeAlreadyDefault(t *testing.T) {
201 isolateDesktopUserDirs(t)
202 model, oldRef, defaultRef := configureSameNameModelProviders(t)
203
204 globalRoot := globalWorkspaceRoot()
205 path, err := createEmptySessionFile(desktopSessionDir(globalRoot), model)
206 if err != nil {
207 t.Fatalf("create empty session: %v", err)
208 }
209
210 app := NewApp()
211 app.ctx = context.Background()
212 tab := testTab("stale-meta", globalRoot)
213 tab.Scope = "global"
214 tab.WorkspaceRoot = globalRoot
215 tab.SessionPath = path
216 tab.model = oldRef
217 tab.Ctrl.SetSessionPath(path)
218 tab.sink = &tabEventSink{tabID: tab.ID, app: app}
219 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
220 app.tabOrder = []string{tab.ID}
221 app.activeTabID = tab.ID
222 t.Cleanup(func() {
223 if tab.Ctrl != nil {
224 tab.Ctrl.Close()
225 }
226 tab.releaseSessionLease()
227 })
228
229 if err := app.SetModelForTab(tab.ID, defaultRef); err != nil {
230 t.Fatalf("seed default runtime: %v", err)
231 }
232 if err := agent.SetBranchModelPreserveUpdated(path, oldRef); err != nil {
233 t.Fatalf("seed stale stored model: %v", err)
234 }
235
236 if _, err := app.EnsureBlankTab("global", ""); err != nil {
237 t.Fatalf("EnsureBlankTab: %v", err)
238 }
239 if tab.model != defaultRef || tab.Ctrl.ModelRef() != defaultRef {
240 t.Fatalf("runtime model = tab:%q controller:%q, want %q", tab.model, tab.Ctrl.ModelRef(), defaultRef)
241 }
242 if stored, ok := agent.LoadSessionModel(tab.currentSessionPath()); !ok || stored != defaultRef {
243 t.Fatalf("stored session model = %q, %v, want repaired %q", stored, ok, defaultRef)
244 }
245 }
246
247 func TestEnsureBlankTabConcurrentModelSwitchKeepsLastSelection(t *testing.T) {
248 isolateDesktopUserDirs(t)
249 model, oldRef, defaultRef := configureSameNameModelProviders(t)
250
251 globalRoot := globalWorkspaceRoot()
252 path, err := createEmptySessionFile(desktopSessionDir(globalRoot), model)
253 if err != nil {
254 t.Fatalf("create empty session: %v", err)
255 }
256 if err := agent.SetBranchModelPreserveUpdated(path, oldRef); err != nil {
257 t.Fatalf("seed old session model: %v", err)
258 }
259
260 app := NewApp()
261 app.ctx = context.Background()
262 tab := testTab("last-click", globalRoot)
263 tab.Scope = "global"
264 tab.WorkspaceRoot = globalRoot
265 tab.SessionPath = path
266 tab.model = oldRef
267 tab.Ctrl.SetSessionPath(path)
268 tab.sink = &tabEventSink{tabID: tab.ID, app: app}
269 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
270 app.tabOrder = []string{tab.ID}
271 app.activeTabID = tab.ID
272 t.Cleanup(func() {
273 if tab.Ctrl != nil {
274 tab.Ctrl.Close()
275 }
276 tab.releaseSessionLease()
277 })
278
279 firstSwitchReturned := make(chan struct{})
280 releaseFirstSwitch := make(chan struct{})
281 var switchCount atomic.Int32
282 app.modelSwitchTimingHook = func(modelSwitchTiming) {
283 if switchCount.Add(1) == 1 {
284 close(firstSwitchReturned)
285 <-releaseFirstSwitch
286 }
287 }
288
289 ensureDone := make(chan error, 1)
290 go func() {
291 _, err := app.EnsureBlankTab("global", "")
292 ensureDone <- err
293 }()
294
295 select {
296 case <-firstSwitchReturned:
297 case <-time.After(5 * time.Second):
298 close(releaseFirstSwitch)
299 t.Fatal("timed out waiting for default model switch")
300 }
301
302 // Model selection remains available while EnsureBlankTab is completing.
303 // The explicit second switch is the user's last click and must own both the
304 // live runtime and the persisted provider identity.
305 lastSwitchErr := app.SetModelForTab(tab.ID, oldRef)
306 close(releaseFirstSwitch)
307 if lastSwitchErr != nil {
308 t.Fatalf("last model switch: %v", lastSwitchErr)
309 }
310 select {
311 case err := <-ensureDone:
312 if err != nil {
313 t.Fatalf("EnsureBlankTab: %v", err)
314 }
315 case <-time.After(5 * time.Second):
316 t.Fatal("timed out waiting for EnsureBlankTab")
317 }
318
319 if tab.model != oldRef || tab.Ctrl.ModelRef() != oldRef {
320 t.Fatalf("last selected runtime = tab:%q controller:%q, want %q (default was %q)", tab.model, tab.Ctrl.ModelRef(), oldRef, defaultRef)
321 }
322 if stored, ok := agent.LoadSessionModel(tab.currentSessionPath()); !ok || stored != oldRef {
323 t.Fatalf("stored session model = %q, %v, want last selection %q", stored, ok, oldRef)
324 }
325 }
326
327 func TestEnsureBlankTabRestartsStartingBlankWithDefaultProvider(t *testing.T) {
328 isolateDesktopUserDirs(t)
329 model, oldRef, defaultRef := configureSameNameModelProviders(t)
330
331 globalRoot := globalWorkspaceRoot()
332 path, err := createEmptySessionFile(desktopSessionDir(globalRoot), model)
333 if err != nil {
334 t.Fatalf("create empty session: %v", err)
335 }
336 if err := agent.SetBranchModelPreserveUpdated(path, oldRef); err != nil {
337 t.Fatalf("seed old session model: %v", err)
338 }
339
340 cancelled := false
341 app := NewApp()
342 tab := &WorkspaceTab{
343 ID: "starting",
344 Scope: "global",
345 WorkspaceRoot: globalRoot,
346 SessionPath: path,
347 model: oldRef,
348 buildCancel: func() { cancelled = true },
349 disabledMCP: map[string]ServerView{},
350 }
351 tab.sink = &tabEventSink{tabID: tab.ID, app: app}
352 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
353 app.tabOrder = []string{tab.ID}
354 app.activeTabID = tab.ID
355 t.Cleanup(func() {
356 if tab.Ctrl != nil {
357 tab.Ctrl.Close()
358 }
359 tab.releaseSessionLease()
360 })
361
362 meta, err := app.EnsureBlankTab("global", "")
363 if err != nil {
364 t.Fatalf("EnsureBlankTab: %v", err)
365 }
366 if meta.ID != tab.ID || !cancelled {
367 t.Fatalf("starting blank reuse = id:%q cancelled:%v, want %q and cancelled old build", meta.ID, cancelled, tab.ID)
368 }
369 if !tab.Ready || tab.Ctrl == nil || tab.model != defaultRef || tab.Ctrl.ModelRef() != defaultRef {
370 t.Fatalf("restarted blank runtime = ready:%v controller:%v tab:%q, want %q", tab.Ready, tab.Ctrl != nil, tab.model, defaultRef)
371 }
372 if stored, ok := agent.LoadSessionModel(tab.currentSessionPath()); !ok || stored != defaultRef {
373 t.Fatalf("stored session model = %q, %v, want %q", stored, ok, defaultRef)
374 }
375 }
376
377 func configureSameNameModelProviders(t *testing.T) (model, oldRef, defaultRef string) {
378 t.Helper()
379 seedUserCredentials(t, "DUPLICATE_MODEL_KEY=test-key\n")
380 model = "deepseek-v4-flash-0731"
381 oldRef = "qwen/" + model
382 defaultRef = "token-rhythm/" + model
383 cfg := config.LoadForEdit(config.UserConfigPath())
384 cfg.DefaultModel = defaultRef
385 cfg.Desktop.ProviderAccess = []string{"qwen", "token-rhythm"}
386 cfg.Providers = []config.ProviderEntry{
387 {Name: "qwen", Kind: "openai", BaseURL: "https://qwen.example.invalid/v1", Model: model, APIKeyEnv: "DUPLICATE_MODEL_KEY"},
388 {Name: "token-rhythm", Kind: "openai", BaseURL: "https://token-rhythm.example.invalid/v1", Model: model, APIKeyEnv: "DUPLICATE_MODEL_KEY"},
389 }
390 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
391 t.Fatalf("save config: %v", err)
392 }
393 return model, oldRef, defaultRef
394 }
395
396 func TestDesktopNewSessionDefaultsHonorExplicitAsk(t *testing.T) {
397 isolateDesktopUserDirs(t)
398 cfg := config.LoadForEdit(config.UserConfigPath())
399 if err := cfg.SetDesktopDefaultToolApprovalMode(control.ToolApprovalAsk); err != nil {
400 t.Fatalf("SetDesktopDefaultToolApprovalMode: %v", err)
401 }
402 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
403 t.Fatalf("save user config: %v", err)
404 }
405
406 _, approvalMode := desktopNewSessionDefaults("global", "")
407 if approvalMode != control.ToolApprovalAsk {
408 t.Fatalf("explicit desktop approval default = %q, want ask", approvalMode)
409 }
410 }
411
412 // seedUserCredentials writes key=value lines into Reasonix's user credentials
413 // store. ProviderEntry.Configured() resolves keys only from that store, not
414 // from process env, so tests must seed it explicitly.
415 func seedUserCredentials(t *testing.T, data string) {
416 t.Helper()
417 if err := os.MkdirAll(filepath.Dir(config.UserCredentialsPath()), 0o755); err != nil {
418 t.Fatalf("mkdir credentials dir: %v", err)
419 }
420 if err := os.WriteFile(config.UserCredentialsPath(), []byte(data), 0o600); err != nil {
421 t.Fatalf("write user credentials: %v", err)
422 }
423 }
424
425 func TestDesktopNewSessionDefaultsSkipsKeylessDefaultModel(t *testing.T) {
426 isolateDesktopUserDirs(t)
427 seedUserCredentials(t, "REASONIX_TEST_SESSION_KEY=test-key\n")
428
429 cfg := config.LoadForEdit(config.UserConfigPath())
430 cfg.Providers = append(cfg.Providers, config.ProviderEntry{
431 Name: "test-prov",
432 Kind: "openai",
433 BaseURL: "https://example.com",
434 Model: "test-model",
435 APIKeyEnv: "REASONIX_TEST_SESSION_KEY",
436 })
437 if err := cfg.SetDefaultModel("deepseek-pro/deepseek-v4-pro"); err != nil {
438 t.Fatalf("SetDefaultModel: %v", err)
439 }
440 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
441 t.Fatalf("save user config: %v", err)
442 }
443
444 model, _ := desktopNewSessionDefaults("global", "")
445 if model != "test-prov/test-model" {
446 t.Fatalf("new session model = %q, want fallback to configured provider test-prov/test-model", model)
447 }
448 }
449
450 func TestDesktopNewSessionDefaultsKeepsConfiguredDefaultModel(t *testing.T) {
451 isolateDesktopUserDirs(t)
452 seedUserCredentials(t, "DEEPSEEK_API_KEY=test-key\n")
453
454 cfg := config.LoadForEdit(config.UserConfigPath())
455 if err := cfg.SetDefaultModel("deepseek-pro/deepseek-v4-pro"); err != nil {
456 t.Fatalf("SetDefaultModel: %v", err)
457 }
458 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
459 t.Fatalf("save user config: %v", err)
460 }
461
462 model, _ := desktopNewSessionDefaults("global", "")
463 if model != "deepseek-pro/deepseek-v4-pro" {
464 t.Fatalf("new session model = %q, want configured default verbatim", model)
465 }
466 }
467
468 func TestDesktopNewSessionDefaultsKeepsKeylessDefaultWhenNothingConfigured(t *testing.T) {
469 isolateDesktopUserDirs(t)
470 // No credentials seeded: every provider resolves as unconfigured.
471
472 cfg := config.LoadForEdit(config.UserConfigPath())
473 if err := cfg.SetDefaultModel("deepseek-pro/deepseek-v4-pro"); err != nil {
474 t.Fatalf("SetDefaultModel: %v", err)
475 }
476 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
477 t.Fatalf("save user config: %v", err)
478 }
479
480 // With no configured provider at all, the raw default must survive so the
481 // boot-time missing-key notice still tells the user what to fix.
482 model, _ := desktopNewSessionDefaults("global", "")
483 if model != "deepseek-pro/deepseek-v4-pro" {
484 t.Fatalf("new session model = %q, want raw keyless default preserved", model)
485 }
486 }
487
488 func TestDesktopNewSessionDefaultsRejectsNonChatDefaultWithoutFallback(t *testing.T) {
489 isolateDesktopUserDirs(t)
490 seedUserCredentials(t, "REASONIX_TEST_AUDIO_KEY=test-key\n")
491
492 cfg := config.LoadForEdit(config.UserConfigPath())
493 cfg.Providers = []config.ProviderEntry{{
494 Name: "audio",
495 Kind: "openai",
496 BaseURL: "https://audio.example.com",
497 Model: "tts-1",
498 APIKeyEnv: "REASONIX_TEST_AUDIO_KEY",
499 }}
500 cfg.Desktop.ProviderAccess = []string{"audio"}
501 if err := cfg.SetDefaultModel("audio/tts-1"); err != nil {
502 t.Fatalf("SetDefaultModel: %v", err)
503 }
504 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
505 t.Fatalf("save user config: %v", err)
506 }
507
508 app := NewApp()
509 meta, err := app.EnsureBlankTab("global", "")
510 if err != nil {
511 t.Fatalf("EnsureBlankTab: %v", err)
512 }
513 created := app.tabs[meta.ID]
514 if created == nil {
515 t.Fatalf("new tab %q missing from app.tabs", meta.ID)
516 }
517 if created.model != "" {
518 t.Fatalf("new session model = %q, want no ineligible model selected", created.model)
519 }
520 if created.Ctrl != nil || !strings.Contains(created.StartupErr, "chat-capable provider") {
521 t.Fatalf("new session runtime = (%v, %q), want an actionable no-chat-model startup error", created.Ctrl, created.StartupErr)
522 }
523 }
524
525 func TestDesktopNewSessionDefaultsHonorExplicitEmptyProviderAccess(t *testing.T) {
526 isolateDesktopUserDirs(t)
527 seedUserCredentials(t, "REASONIX_TEST_SESSION_KEY=test-key\n")
528
529 cfg := config.LoadForEdit(config.UserConfigPath())
530 cfg.Providers = []config.ProviderEntry{{
531 Name: "test-prov",
532 Kind: "openai",
533 BaseURL: "https://example.com",
534 Model: "test-model",
535 APIKeyEnv: "REASONIX_TEST_SESSION_KEY",
536 }}
537 cfg.Desktop.ProviderAccess = []string{}
538 if err := cfg.SetDefaultModel("test-prov/test-model"); err != nil {
539 t.Fatalf("SetDefaultModel: %v", err)
540 }
541 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
542 t.Fatalf("save user config: %v", err)
543 }
544
545 app := NewApp()
546 meta, err := app.EnsureBlankTab("global", "")
547 if err != nil {
548 t.Fatalf("EnsureBlankTab: %v", err)
549 }
550 created := app.tabs[meta.ID]
551 if created == nil {
552 t.Fatalf("new tab %q missing from app.tabs", meta.ID)
553 }
554 if created.model != "" {
555 t.Fatalf("new session model = %q, want no provider selected", created.model)
556 }
557 if created.Ctrl != nil || !strings.Contains(created.StartupErr, "chat-capable provider") {
558 t.Fatalf("new session runtime = (%v, %q), want an actionable no-provider startup error", created.Ctrl, created.StartupErr)
559 }
560 if !app.NeedsOnboarding() {
561 t.Fatal("NeedsOnboarding should be true when provider access is explicitly empty")
562 }
563 }
564
565 func TestDesktopNewSessionDefaultsUsesProjectDefaultAndSkipsItsKeylessProvider(t *testing.T) {
566 isolateDesktopUserDirs(t)
567 seedUserCredentials(t, "REASONIX_TEST_SESSION_KEY=test-key\n")
568
569 userCfg := config.LoadForEdit(config.UserConfigPath())
570 userCfg.Providers = append(userCfg.Providers, config.ProviderEntry{
571 Name: "test-prov",
572 Kind: "openai",
573 BaseURL: "https://example.com",
574 Model: "test-model",
575 APIKeyEnv: "REASONIX_TEST_SESSION_KEY",
576 })
577 if err := userCfg.SetDefaultModel("test-prov/test-model"); err != nil {
578 t.Fatalf("SetDefaultModel: %v", err)
579 }
580 if err := userCfg.SaveTo(config.UserConfigPath()); err != nil {
581 t.Fatalf("save user config: %v", err)
582 }
583
584 workspace := robustTempDir(t)
585 if err := os.WriteFile(filepath.Join(workspace, "reasonix.toml"), []byte(`default_model = "deepseek-pro/deepseek-v4-pro"
586 `), 0o644); err != nil {
587 t.Fatalf("write project config: %v", err)
588 }
589
590 app := NewApp()
591 meta, err := app.EnsureBlankTab("project", workspace)
592 if err != nil {
593 t.Fatalf("EnsureBlankTab: %v", err)
594 }
595 created := app.tabs[meta.ID]
596 if created == nil {
597 t.Fatalf("new tab %q missing from app.tabs", meta.ID)
598 }
599 if created.model != "test-prov/test-model" {
600 t.Fatalf("new project session model = %q, want configured fallback test-prov/test-model", created.model)
601 }
602 if !strings.Contains(filepath.Base(created.SessionPath), "test-model") {
603 t.Fatalf("new project session path = %q, want filename seeded by fallback model", created.SessionPath)
604 }
605 }
606
606 lines GO