From 16aa9810287248c0e746f4deabcfb206e7e6c8c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=9A=E7=AB=A0=E6=B5=A9?= Date: Tue, 15 Sep 2026 14:01:00 +0800 Subject: [PATCH] test(reasoning): cover default-mode routing, user pinning and 000022 contract --- ...22_reasoning_default_mode_contract_test.go | 34 ++++++++ internal/reasoningmode/service_test.go | 86 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 db/migrations/000022_reasoning_default_mode_contract_test.go diff --git a/db/migrations/000022_reasoning_default_mode_contract_test.go b/db/migrations/000022_reasoning_default_mode_contract_test.go new file mode 100644 index 0000000..19ccfe6 --- /dev/null +++ b/db/migrations/000022_reasoning_default_mode_contract_test.go @@ -0,0 +1,34 @@ +package migrations_test + +import ( + "os" + "strings" + "testing" +) + +func TestUp022AddsReasoningUserSelectedColumn(t *testing.T) { + data, err := os.ReadFile("000022_reasoning_default_mode.up.sql") + if err != nil { + t.Fatal(err) + } + source := strings.ToUpper(string(data)) + for _, required := range []string{ + "ALTER TABLE REASONING_MODE_STATES", + "ADD COLUMN USER_SELECTED BOOLEAN NOT NULL DEFAULT FALSE", + } { + if !strings.Contains(source, required) { + t.Errorf("up migration missing %q", required) + } + } +} + +func TestDown022DropsUserSelectedColumn(t *testing.T) { + data, err := os.ReadFile("000022_reasoning_default_mode.down.sql") + if err != nil { + t.Fatal(err) + } + source := strings.ToUpper(string(data)) + if !strings.Contains(source, "DROP COLUMN USER_SELECTED") { + t.Errorf("down migration missing %q", "DROP COLUMN USER_SELECTED") + } +} diff --git a/internal/reasoningmode/service_test.go b/internal/reasoningmode/service_test.go index 7d42ed4..17385c3 100644 --- a/internal/reasoningmode/service_test.go +++ b/internal/reasoningmode/service_test.go @@ -23,6 +23,7 @@ func (f *fakeRepository) ResolveState(context.Context, Scope, time.Time) (State, func (f *fakeRepository) Enable(context.Context, Scope, time.Time, time.Duration) (State, error) { f.enableCalls++ f.state.Mode = ModeReasoning + f.state.UserSelected = true f.state.RoundsUsed = 0 return f.state, nil } @@ -31,6 +32,7 @@ func (f *fakeRepository) Disable(context.Context, Scope, time.Time) (State, bool f.disableCalls++ wasReasoning := f.state.Mode == ModeReasoning f.state.Mode = ModeQuick + f.state.UserSelected = true f.state.RoundsUsed = 0 return f.state, wasReasoning, nil } @@ -177,3 +179,87 @@ func TestPrepareReturnsLocalHelpWithoutStateMutation(t *testing.T) { t.Fatalf("help mutated mode: %+v", repo) } } + +func newTestServiceWithDefault(t *testing.T, repo Repository, mode Mode) *Service { + t.Helper() + service, err := NewService(repo, "builtin-quick-answer", "builtin-smart-reasoning", WithDefaultMode(mode)) + if err != nil { + t.Fatal(err) + } + service.now = func() time.Time { return time.Date(2026, 9, 15, 12, 0, 0, 0, time.UTC) } + return service +} + +func TestNewServiceRejectsInvalidDefaultMode(t *testing.T) { + if _, err := NewService(&fakeRepository{}, "q", "r", WithDefaultMode("turbo")); err == nil { + t.Fatal("expected invalid default mode to fail") + } +} + +func TestPrepareDefaultReasoningRoutesUnselectedUserToReasoningSingle(t *testing.T) { + repo := &fakeRepository{state: DefaultState()} + decision, err := newTestServiceWithDefault(t, repo, ModeReasoning).Prepare(context.Background(), testScope(), "普通问题") + if err != nil { + t.Fatal(err) + } + if decision.AgentID != "builtin-smart-reasoning" || decision.Execution != ExecutionReasoningSingle { + t.Fatalf("decision=%+v", decision) + } +} + +func TestPrepareDefaultReasoningHonoursUserQuickSelection(t *testing.T) { + state := DefaultState() + state.UserSelected = true + decision, err := newTestServiceWithDefault(t, &fakeRepository{state: state}, ModeReasoning). + Prepare(context.Background(), testScope(), "普通问题") + if err != nil { + t.Fatal(err) + } + if decision.AgentID != "builtin-quick-answer" || decision.Execution != ExecutionQuick { + t.Fatalf("decision=%+v", decision) + } +} + +func TestPrepareDefaultQuickKeepsLegacyBehaviour(t *testing.T) { + decision, err := newTestServiceWithDefault(t, &fakeRepository{state: DefaultState()}, ModeQuick). + Prepare(context.Background(), testScope(), "普通问题") + if err != nil { + t.Fatal(err) + } + if decision.AgentID != "builtin-quick-answer" || decision.Execution != ExecutionQuick { + t.Fatalf("decision=%+v", decision) + } +} + +func TestThinkOffPinsQuickModeUnderReasoningDefault(t *testing.T) { + repo := &fakeRepository{state: DefaultState()} + service := newTestServiceWithDefault(t, repo, ModeReasoning) + decision, err := service.Prepare(context.Background(), testScope(), "/think off") + if err != nil { + t.Fatal(err) + } + if !decision.Handled || !strings.Contains(decision.Reply, "切回") { + t.Fatalf("decision=%+v", decision) + } + if !repo.state.UserSelected || repo.state.Mode != ModeQuick { + t.Fatalf("state=%+v", repo.state) + } + followUp, err := service.Prepare(context.Background(), testScope(), "后续问题") + if err != nil { + t.Fatal(err) + } + if followUp.AgentID != "builtin-quick-answer" { + t.Fatalf("follow-up escaped user quick selection: %+v", followUp) + } +} + +func TestHelpReflectsReasoningDefaultMode(t *testing.T) { + decision, err := newTestServiceWithDefault(t, &fakeRepository{state: DefaultState()}, ModeReasoning). + Prepare(context.Background(), testScope(), "/help") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(decision.Reply, "默认模式:智能推理模式") { + t.Fatalf("help=%q", decision.Reply) + } +}