package cli import ( "errors" "strings" "testing" "time" "reasonix/internal/agent" "reasonix/internal/boot" "reasonix/internal/command" "reasonix/internal/control" "reasonix/internal/event" "reasonix/internal/provider" "reasonix/internal/skill" ) func TestRenderWorkModesShowsAllOptionsAndCurrent(t *testing.T) { out := renderWorkModes(100, boot.TokenModeFull) for _, want := range []string{"economy", "balanced", "delivery", "current"} { if !strings.Contains(out, want) { t.Fatalf("renderWorkModes missing %q:\n%s", want, out) } } } func TestParseWorkModeKeepsBalancedAsFullInternally(t *testing.T) { for input, want := range map[string]string{ "economy": boot.TokenModeEconomy, "balanced": boot.TokenModeFull, "full": boot.TokenModeFull, "delivery": boot.TokenModeDelivery, } { got, ok := parseWorkMode(input) if !ok || got != want { t.Errorf("parseWorkMode(%q) = %q, %v; want %q, true", input, got, ok, want) } } } func TestWorkModeCompletionPublishesPrimaryCommandAndAliasArguments(t *testing.T) { m := newTestChatTUI() m.runtimeProfile = boot.TokenModeFull if !hasLabel(m.slashItems(), "/work-mode") { t.Fatal("slash completion missing /work-mode") } if hasLabel(m.slashItems(), "/profile") { t.Fatal("technical /profile alias should not duplicate the primary command in the slash menu") } for _, input := range []string{"/work-mode ", "/profile "} { items, _, ok := m.slashArgItems(input) if !ok { t.Fatalf("%q did not activate work-mode argument completion", input) } for _, want := range []string{"economy", "balanced", "delivery"} { if !hasLabel(items, want) { t.Errorf("%q completion missing %q: %v", input, want, labels(items)) } } } } func TestWorkModeHelpAndStatusUseUserFacingName(t *testing.T) { if !hasLabel(builtinHelpItems(), "/work-mode") { t.Fatal("built-in help missing /work-mode") } if hasLabel(builtinHelpItems(), "/profile") { t.Fatal("built-in help should not duplicate the technical /profile alias") } m := newChatTUI(control.New(control.Options{Label: "model"}), "", make(chan event.Event, 1), 80) m.runtimeProfile = boot.TokenModeDelivery if got := m.workModeTag(); !strings.Contains(got, "delivery") { t.Fatalf("work-mode status tag = %q, want delivery", got) } m.width = 30 if got := m.computeStatusLineCount(m.width); got < 2 { t.Fatalf("status-line count with work-mode tag = %d, want at least 2", got) } } func TestWorkModeSwitchBuildsTargetProfileAndSwapsAtomically(t *testing.T) { oldCtrl := control.New(control.Options{Label: "old"}) oldCtrl.SetToolApprovalMode(control.ToolApprovalAuto) oldCtrl.SetPlanMode(true) newCtrl := control.New(control.Options{ Label: "new", Commands: []command.Command{{Name: "fresh-command"}}, Skills: []skill.Skill{{Name: "Fresh Skill"}}, }) m := newChatTUI(oldCtrl, "", make(chan event.Event, 1), 100) m.modelRef = "provider/model" m.runtimeProfile = boot.TokenModeFull var gotSpec controllerBuildSpec var gotCarry []provider.Message m.buildController = func(spec controllerBuildSpec, carry []provider.Message, _ string) (*control.Controller, error) { gotSpec = spec gotCarry = carry return newCtrl, nil } cmd := m.runWorkModeCommand("/work-mode delivery") if cmd == nil { t.Fatal("work-mode switch did not schedule a controller build") } if m.ctrl != oldCtrl || m.runtimeProfile != boot.TokenModeFull { t.Fatal("controller or profile changed before the replacement build completed") } next, _ := m.Update(cmd()) m = next.(chatTUI) if gotSpec.ModelRef != "provider/model" || gotSpec.RuntimeProfile != boot.TokenModeDelivery { t.Fatalf("build spec = %+v, want current model and delivery profile", gotSpec) } if gotSpec.ToolApprovalMode != control.ToolApprovalAuto { t.Fatalf("build spec approval mode = %q, want auto", gotSpec.ToolApprovalMode) } if !gotSpec.PlanMode { t.Fatal("build spec did not preserve plan mode") } if len(gotCarry) != len(oldCtrl.History()) { t.Fatalf("carried history length = %d, want %d", len(gotCarry), len(oldCtrl.History())) } if m.ctrl != newCtrl || m.runtimeProfile != boot.TokenModeDelivery { t.Fatalf("successful switch did not install replacement controller/profile: ctrl=%p profile=%q", m.ctrl, m.runtimeProfile) } if m.label != newCtrl.Label() || len(m.commands) != 1 || len(m.skills) != 1 || m.host != newCtrl.Host() { t.Fatalf("successful switch did not refresh controller metadata: label=%q commands=%d skills=%d", m.label, len(m.commands), len(m.skills)) } if len(m.oldControllers) != 1 || m.oldControllers[0] != oldCtrl { t.Fatal("successful switch did not retain the old controller for exit-time cleanup") } } func TestWorkModeSwitchFailureKeepsOldControllerAndProfile(t *testing.T) { session := agent.NewSession("system") session.Add(provider.Message{Role: provider.RoleUser, Content: "keep this history"}) exec := agent.New(nil, nil, session, agent.Options{}, event.Discard) oldCtrl := control.New(control.Options{Label: "old", Executor: exec}) m := newChatTUI(oldCtrl, "", make(chan event.Event, 1), 100) m.modelRef = "provider/model" m.runtimeProfile = boot.TokenModeEconomy m.buildController = func(controllerBuildSpec, []provider.Message, string) (*control.Controller, error) { return nil, errors.New("build failed") } cmd := m.runSlashCommand("/profile delivery") if cmd == nil { t.Fatal("/profile alias did not schedule a controller build") } next, _ := m.Update(cmd()) m = next.(chatTUI) if m.ctrl != oldCtrl || m.runtimeProfile != boot.TokenModeEconomy { t.Fatalf("failed switch changed live runtime: ctrl=%p profile=%q", m.ctrl, m.runtimeProfile) } if history := m.ctrl.History(); len(history) != 2 || history[1].Content != "keep this history" { t.Fatalf("failed switch lost history: %#v", history) } if m.modelSwitchPending || m.pendingModelSwitch != nil { t.Fatal("failed switch left the runtime-switch gate pending") } if len(m.oldControllers) != 0 { t.Fatal("failed switch retired the still-live old controller") } } func TestWorkModeSwitchRejectsInvalidSameAndBusyRequests(t *testing.T) { m := newTestChatTUI() m.ctrl = control.New(control.Options{Label: "model"}) m.modelRef = "provider/model" m.runtimeProfile = boot.TokenModeFull builds := 0 m.buildController = func(controllerBuildSpec, []provider.Message, string) (*control.Controller, error) { builds++ return control.New(control.Options{Label: "new"}), nil } for _, input := range []string{ "/work-mode unknown", "/work-mode balanced", "/work-mode economy extra", } { if cmd := m.runWorkModeCommand(input); cmd != nil { t.Fatalf("%q unexpectedly scheduled a build", input) } } m.pendingApproval = &event.Approval{ID: "approval", Tool: "bash"} if cmd := m.runWorkModeCommand("/work-mode economy"); cmd != nil { t.Fatal("pending approval should block work-mode switching") } if builds != 0 { t.Fatalf("rejected work-mode requests triggered %d builds", builds) } } func TestWorkModeSwitchRejectsRunningTurn(t *testing.T) { runner := &blockingTurnRunner{started: make(chan struct{})} ctrl := control.New(control.Options{Runner: runner, Sink: event.Discard, SessionDir: t.TempDir(), Label: "model"}) ctrl.Send("keep running") <-runner.started t.Cleanup(func() { ctrl.Cancel() deadline := time.Now().Add(2 * time.Second) for ctrl.Running() && time.Now().Before(deadline) { time.Sleep(time.Millisecond) } }) m := newTestChatTUI() m.ctrl = ctrl m.modelRef = "provider/model" m.runtimeProfile = boot.TokenModeFull builds := 0 m.buildController = func(controllerBuildSpec, []provider.Message, string) (*control.Controller, error) { builds++ return control.New(control.Options{}), nil } if cmd := m.runWorkModeCommand("/work-mode delivery"); cmd != nil { t.Fatal("running turn should block work-mode switching") } if builds != 0 { t.Fatalf("running-turn guard triggered %d builds", builds) } } func TestRuntimeRebuildCommandsCarryCurrentWorkMode(t *testing.T) { isolateUserConfig(t) m := newTestChatTUI() m.ctrl = control.New(control.Options{Label: "deepseek-flash"}) m.modelRef = "deepseek-flash/deepseek-v4-flash" m.runtimeProfile = boot.TokenModeDelivery var specs []controllerBuildSpec m.buildController = func(spec controllerBuildSpec, _ []provider.Message, _ string) (*control.Controller, error) { specs = append(specs, spec) return control.New(control.Options{Label: "deepseek-flash"}), nil } cmd := m.runEffortCommand("/effort max") if cmd == nil { t.Fatal("effort switch did not schedule a rebuild") } next, _ := m.Update(cmd()) m = next.(chatTUI) m.runModelSubcommand("/model deepseek-flash/another-model") if m.pendingModelSwitch == nil { t.Fatal("model switch did not schedule a rebuild") } next, _ = m.Update(m.pendingModelSwitch()) m = next.(chatTUI) if !m.scheduleSkillSessionRefresh("test", "") { t.Fatal("skill refresh did not schedule a rebuild") } next, _ = m.Update(m.pendingModelSwitch()) m = next.(chatTUI) if len(specs) != 3 { t.Fatalf("runtime rebuild count = %d, want 3", len(specs)) } for i, spec := range specs { if spec.RuntimeProfile != boot.TokenModeDelivery { t.Errorf("rebuild %d lost delivery profile: %+v", i, spec) } } }