Files
2026-07-13 13:00:08 +08:00

267 lines
9.0 KiB
Go

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)
}
}
}