返回 DeepSeek-Reasonix
reload_runtime_test.go
根目录 / desktop / reload_runtime_test.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "reasonix/internal/config"
10 "reasonix/internal/control"
11 "reasonix/internal/event"
12 )
13
14 // reloadRuntimeFixture writes the config the ReloadRuntime tests share (one
15 // configured provider) and returns the isolated session dir.
16 func reloadRuntimeFixture(t *testing.T) string {
17 t.Helper()
18 isolateDesktopUserDirs(t)
19 setDesktopTestCredential(t, "OLD_MODEL_KEY", "sk-test")
20
21 cfg := config.Default()
22 cfg.DefaultModel = "old/old-model"
23 cfg.Desktop.ProviderAccess = []string{"old"}
24 cfg.Providers = []config.ProviderEntry{
25 {Name: "old", Kind: "openai", BaseURL: "https://example.invalid/v1", Model: "old-model", APIKeyEnv: "OLD_MODEL_KEY"},
26 }
27 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
28 t.Fatalf("save config: %v", err)
29 }
30
31 dir := config.SessionDir()
32 if err := os.MkdirAll(dir, 0o755); err != nil {
33 t.Fatalf("mkdir session dir: %v", err)
34 }
35 return dir
36 }
37
38 func reloadRuntimeTab(t *testing.T, app *App, dir string, oldCtrl *control.Controller) *WorkspaceTab {
39 t.Helper()
40 tab := &WorkspaceTab{
41 ID: "tab_a",
42 Scope: "global",
43 Ready: true,
44 model: "old/old-model",
45 Ctrl: oldCtrl,
46 sink: &tabEventSink{tabID: "tab_a", app: app},
47 disabledMCP: map[string]ServerView{},
48 }
49 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
50 app.tabOrder = []string{tab.ID}
51 app.activeTabID = tab.ID
52 t.Cleanup(func() {
53 tab.releaseSessionLease()
54 if tab.Ctrl != nil {
55 tab.Ctrl.Close()
56 }
57 })
58 return tab
59 }
60
61 // TestReloadRuntimeSwapsAndClosesOldAfterSwap covers the success path through
62 // boot.Rebuild: the tab's controller is replaced, the session (grants, file)
63 // migrates, the outgoing controller is closed only after the swap published
64 // the replacement, and the frontend fence is emitted.
65 // TestSubmitReloadRoutesToRuntimeReload pins the desktop slash-command entry:
66 // /reload is a local management action, not a user turn sent to the model.
67 // TestReloadRuntimeFailureKeepsOldController: a failed build leaves the tab on
68 // the outgoing controller, which stays open.
69 func TestReloadRuntimeFailureKeepsOldController(t *testing.T) {
70 isolateDesktopUserDirs(t)
71
72 // No providers at all: the build cannot resolve the tab's model.
73 cfg := config.Default()
74 cfg.DefaultModel = ""
75 cfg.Providers = []config.ProviderEntry{}
76 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
77 t.Fatalf("save config: %v", err)
78 }
79 dir := config.SessionDir()
80 if err := os.MkdirAll(dir, 0o755); err != nil {
81 t.Fatalf("mkdir session dir: %v", err)
82 }
83
84 closed := false
85 oldCtrl := control.New(control.Options{
86 SessionDir: dir,
87 SessionPath: filepath.Join(dir, "old.jsonl"),
88 Label: "old",
89 Sink: event.Discard,
90 Cleanup: func() { closed = true },
91 })
92 app := NewApp()
93 app.ctx = context.Background()
94 app.readyHook = func() {}
95 tab := reloadRuntimeTab(t, app, dir, oldCtrl)
96
97 if err := app.ReloadRuntime(tab.ID); err == nil {
98 t.Fatal("ReloadRuntime with an unresolvable model returned nil error")
99 }
100 if tab.Ctrl != oldCtrl {
101 t.Fatal("failed reload replaced the tab controller")
102 }
103 if closed {
104 t.Fatal("failed reload closed the outgoing controller")
105 }
106 if app.deferredRebuildPending(tab.ID) {
107 t.Fatal("hard failure was queued for retry")
108 }
109 }
110
111 // TestReloadRuntimeBusyQueuesDeferred covers the busy contract: active work
112 // queues exactly one reload on the deferred-rebuild loop (coalesced), and the
113 // retry runs the boot.Rebuild reload once the tab is idle.
114
114 lines GO