返回 DeepSeek-Reasonix
work_mode_test.go
根目录 / internal / cli / work_mode_test.go
1 package cli
2
3 import (
4 "errors"
5 "strings"
6 "testing"
7 "time"
8
9 "reasonix/internal/agent"
10 "reasonix/internal/boot"
11 "reasonix/internal/command"
12 "reasonix/internal/control"
13 "reasonix/internal/event"
14 "reasonix/internal/i18n"
15 "reasonix/internal/provider"
16 "reasonix/internal/skill"
17 )
18
19 func TestRuntimeProfileDisplayLocalizesLabels(t *testing.T) {
20 defer i18n.DetectLanguage("en")
21 for _, tt := range []struct {
22 lang string
23 economy, balanced, delivery string
24 }{
25 {lang: "en", economy: "economy", balanced: "balanced", delivery: "delivery"},
26 {lang: "zh", economy: "轻量", balanced: "均衡", delivery: "交付"},
27 {lang: "zh-TW", economy: "輕量", balanced: "均衡", delivery: "交付"},
28 } {
29 t.Run(tt.lang, func(t *testing.T) {
30 i18n.DetectLanguage(tt.lang)
31 for profile, want := range map[string]string{
32 boot.TokenModeEconomy: tt.economy,
33 boot.TokenModeFull: tt.balanced,
34 "balanced": tt.balanced,
35 boot.TokenModeDelivery: tt.delivery,
36 } {
37 if got := runtimeProfileDisplay(profile); got != want {
38 t.Errorf("runtimeProfileDisplay(%q) = %q, want %q", profile, got, want)
39 }
40 }
41 })
42 }
43 }
44
45 func TestRenderWorkModesShowsAllOptionsAndCurrent(t *testing.T) {
46 out := renderWorkModes(100, boot.TokenModeFull)
47 for _, want := range []string{"economy", "balanced", "delivery", "current"} {
48 if !strings.Contains(out, want) {
49 t.Fatalf("renderWorkModes missing %q:\n%s", want, out)
50 }
51 }
52 }
53
54 func TestParseWorkModeKeepsBalancedAsFullInternally(t *testing.T) {
55 for input, want := range map[string]string{
56 "economy": boot.TokenModeEconomy,
57 "balanced": boot.TokenModeFull,
58 "full": boot.TokenModeFull,
59 "delivery": boot.TokenModeDelivery,
60 } {
61 got, ok := parseWorkMode(input)
62 if !ok || got != want {
63 t.Errorf("parseWorkMode(%q) = %q, %v; want %q, true", input, got, ok, want)
64 }
65 }
66 }
67
68 func TestWorkModeCompletionPublishesPrimaryCommandAndAliasArguments(t *testing.T) {
69 m := newTestChatTUI()
70 m.runtimeProfile = boot.TokenModeFull
71 if !hasLabel(m.slashItems(), "/work-mode") {
72 t.Fatal("slash completion missing /work-mode")
73 }
74 if hasLabel(m.slashItems(), "/profile") {
75 t.Fatal("technical /profile alias should not duplicate the primary command in the slash menu")
76 }
77 for _, input := range []string{"/work-mode ", "/profile "} {
78 items, _, ok := m.slashArgItems(input)
79 if !ok {
80 t.Fatalf("%q did not activate work-mode argument completion", input)
81 }
82 for _, want := range []string{"economy", "balanced", "delivery"} {
83 if !hasLabel(items, want) {
84 t.Errorf("%q completion missing %q: %v", input, want, labels(items))
85 }
86 }
87 }
88 }
89
90 func TestWorkModeHelpAndStatusUseUserFacingName(t *testing.T) {
91 if !hasLabel(builtinHelpItems(), "/work-mode") {
92 t.Fatal("built-in help missing /work-mode")
93 }
94 if hasLabel(builtinHelpItems(), "/profile") {
95 t.Fatal("built-in help should not duplicate the technical /profile alias")
96 }
97 m := newChatTUI(control.New(control.Options{Label: "model"}), "", make(chan event.Event, 1), 80)
98 m.runtimeProfile = boot.TokenModeDelivery
99 if got := m.workModeTag(); !strings.Contains(got, "delivery") {
100 t.Fatalf("work-mode status tag = %q, want delivery", got)
101 }
102 m.width = 30
103 if got := m.computeStatusLineCount(m.width); got < 2 {
104 t.Fatalf("status-line count with work-mode tag = %d, want at least 2", got)
105 }
106 }
107
108 func TestWorkModeSwitchBuildsTargetProfileAndSwapsAtomically(t *testing.T) {
109 oldCtrl := control.New(control.Options{Label: "old"})
110 oldCtrl.SetToolApprovalMode(control.ToolApprovalAuto)
111 oldCtrl.SetPlanMode(true)
112 newCtrl := control.New(control.Options{
113 Label: "new",
114 Commands: []command.Command{{Name: "fresh-command"}},
115 Skills: []skill.Skill{{Name: "Fresh Skill"}},
116 })
117 m := newChatTUI(oldCtrl, "", make(chan event.Event, 1), 100)
118 m.modelRef = "provider/model"
119 m.runtimeProfile = boot.TokenModeFull
120 var gotSpec controllerBuildSpec
121 var gotCarry []provider.Message
122 m.buildController = func(spec controllerBuildSpec, carry []provider.Message, _ string, _ control.SessionAPI) (*control.Controller, error) {
123 gotSpec = spec
124 gotCarry = carry
125 return newCtrl, nil
126 }
127
128 cmd := m.runWorkModeCommand("/work-mode delivery")
129 if cmd == nil {
130 t.Fatal("work-mode switch did not schedule a controller build")
131 }
132 if m.ctrl != oldCtrl || m.runtimeProfile != boot.TokenModeFull {
133 t.Fatal("controller or profile changed before the replacement build completed")
134 }
135 next, _ := m.Update(cmd())
136 m = next.(chatTUI)
137
138 if gotSpec.ModelRef != "provider/model" || gotSpec.RuntimeProfile != boot.TokenModeDelivery {
139 t.Fatalf("build spec = %+v, want current model and delivery profile", gotSpec)
140 }
141 if gotSpec.ToolApprovalMode != control.ToolApprovalAuto {
142 t.Fatalf("build spec approval mode = %q, want auto", gotSpec.ToolApprovalMode)
143 }
144 if !gotSpec.PlanMode {
145 t.Fatal("build spec did not preserve plan mode")
146 }
147 if len(gotCarry) != len(oldCtrl.History()) {
148 t.Fatalf("carried history length = %d, want %d", len(gotCarry), len(oldCtrl.History()))
149 }
150 if m.ctrl != newCtrl || m.runtimeProfile != boot.TokenModeDelivery {
151 t.Fatalf("successful switch did not install replacement controller/profile: ctrl=%p profile=%q", m.ctrl, m.runtimeProfile)
152 }
153 if m.label != newCtrl.Label() || len(m.commands) != 1 || len(m.skills) != 1 || m.host != newCtrl.Host() {
154 t.Fatalf("successful switch did not refresh controller metadata: label=%q commands=%d skills=%d", m.label, len(m.commands), len(m.skills))
155 }
156 if len(m.oldControllers) != 1 || m.oldControllers[0] != oldCtrl {
157 t.Fatal("successful switch did not retain the old controller for exit-time cleanup")
158 }
159 }
160
161 func TestWorkModeSwitchFailureKeepsOldControllerAndProfile(t *testing.T) {
162 session := agent.NewSession("system")
163 session.Add(provider.Message{Role: provider.RoleUser, Content: "keep this history"})
164 exec := agent.New(nil, nil, session, agent.Options{}, event.Discard)
165 oldCtrl := control.New(control.Options{Label: "old", Executor: exec})
166 m := newChatTUI(oldCtrl, "", make(chan event.Event, 1), 100)
167 m.modelRef = "provider/model"
168 m.runtimeProfile = boot.TokenModeEconomy
169 m.buildController = func(controllerBuildSpec, []provider.Message, string, control.SessionAPI) (*control.Controller, error) {
170 return nil, errors.New("build failed")
171 }
172
173 cmd := m.runSlashCommand("/profile delivery")
174 if cmd == nil {
175 t.Fatal("/profile alias did not schedule a controller build")
176 }
177 next, _ := m.Update(cmd())
178 m = next.(chatTUI)
179
180 if m.ctrl != oldCtrl || m.runtimeProfile != boot.TokenModeEconomy {
181 t.Fatalf("failed switch changed live runtime: ctrl=%p profile=%q", m.ctrl, m.runtimeProfile)
182 }
183 if history := m.ctrl.History(); len(history) != 2 || history[1].Content != "keep this history" {
184 t.Fatalf("failed switch lost history: %#v", history)
185 }
186 if m.modelSwitchPending || m.pendingModelSwitch != nil {
187 t.Fatal("failed switch left the runtime-switch gate pending")
188 }
189 if len(m.oldControllers) != 0 {
190 t.Fatal("failed switch retired the still-live old controller")
191 }
192 }
193
194 func TestWorkModeSwitchRejectsInvalidSameAndBusyRequests(t *testing.T) {
195 m := newTestChatTUI()
196 m.ctrl = control.New(control.Options{Label: "model"})
197 m.modelRef = "provider/model"
198 m.runtimeProfile = boot.TokenModeFull
199 builds := 0
200 m.buildController = func(controllerBuildSpec, []provider.Message, string, control.SessionAPI) (*control.Controller, error) {
201 builds++
202 return control.New(control.Options{Label: "new"}), nil
203 }
204
205 for _, input := range []string{
206 "/work-mode unknown",
207 "/work-mode balanced",
208 "/work-mode economy extra",
209 } {
210 if cmd := m.runWorkModeCommand(input); cmd != nil {
211 t.Fatalf("%q unexpectedly scheduled a build", input)
212 }
213 }
214 m.pendingApproval = &event.Approval{ID: "approval", Tool: "bash"}
215 if cmd := m.runWorkModeCommand("/work-mode economy"); cmd != nil {
216 t.Fatal("pending approval should block work-mode switching")
217 }
218 if builds != 0 {
219 t.Fatalf("rejected work-mode requests triggered %d builds", builds)
220 }
221 }
222
223 func TestWorkModeSwitchRejectsRunningTurn(t *testing.T) {
224 runner := &blockingTurnRunner{started: make(chan struct{})}
225 ctrl := control.New(control.Options{Runner: runner, Sink: event.Discard, SessionDir: t.TempDir(), Label: "model"})
226 ctrl.Send("keep running")
227 <-runner.started
228 t.Cleanup(func() {
229 ctrl.Cancel()
230 deadline := time.Now().Add(2 * time.Second)
231 for ctrl.Running() && time.Now().Before(deadline) {
232 time.Sleep(time.Millisecond)
233 }
234 })
235
236 m := newTestChatTUI()
237 m.ctrl = ctrl
238 m.modelRef = "provider/model"
239 m.runtimeProfile = boot.TokenModeFull
240 builds := 0
241 m.buildController = func(controllerBuildSpec, []provider.Message, string, control.SessionAPI) (*control.Controller, error) {
242 builds++
243 return control.New(control.Options{}), nil
244 }
245 if cmd := m.runWorkModeCommand("/work-mode delivery"); cmd != nil {
246 t.Fatal("running turn should block work-mode switching")
247 }
248 if builds != 0 {
249 t.Fatalf("running-turn guard triggered %d builds", builds)
250 }
251 }
252
253 func TestRuntimeRebuildCommandsCarryCurrentWorkMode(t *testing.T) {
254 isolateUserConfig(t)
255 m := newTestChatTUI()
256 m.ctrl = control.New(control.Options{Label: "deepseek-flash"})
257 m.modelRef = "deepseek-flash/deepseek-v4-flash"
258 m.runtimeProfile = boot.TokenModeDelivery
259 var specs []controllerBuildSpec
260 m.buildController = func(spec controllerBuildSpec, _ []provider.Message, _ string, _ control.SessionAPI) (*control.Controller, error) {
261 specs = append(specs, spec)
262 return control.New(control.Options{Label: "deepseek-flash"}), nil
263 }
264
265 cmd := m.runEffortCommand("/effort max")
266 if cmd == nil {
267 t.Fatal("effort switch did not schedule a rebuild")
268 }
269 next, _ := m.Update(cmd())
270 m = next.(chatTUI)
271
272 m.runModelSubcommand("/model deepseek-flash/another-model")
273 if m.pendingModelSwitch == nil {
274 t.Fatal("model switch did not schedule a rebuild")
275 }
276 next, _ = m.Update(m.pendingModelSwitch())
277 m = next.(chatTUI)
278
279 if !m.scheduleSkillSessionRefresh("test", "") {
280 t.Fatal("skill refresh did not schedule a rebuild")
281 }
282 next, _ = m.Update(m.pendingModelSwitch())
283 m = next.(chatTUI)
284
285 if len(specs) != 3 {
286 t.Fatalf("runtime rebuild count = %d, want 3", len(specs))
287 }
288 if specs[0].EffortOverride == nil || *specs[0].EffortOverride != "max" {
289 t.Fatalf("effort rebuild override = %v, want max", specs[0].EffortOverride)
290 }
291 if specs[1].EffortOverride != nil || specs[2].EffortOverride != nil {
292 t.Fatalf("non-effort rebuilds unexpectedly replaced effort override: %+v", specs)
293 }
294 for i, spec := range specs {
295 if spec.RuntimeProfile != boot.TokenModeDelivery {
296 t.Errorf("rebuild %d lost delivery profile: %+v", i, spec)
297 }
298 }
299 }
300
300 lines GO