266 lines
8.9 KiB
Go
266 lines
8.9 KiB
Go
package reasoningmode
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type fakeRepository struct {
|
|
state State
|
|
expired bool
|
|
enableCalls int
|
|
disableCalls int
|
|
completeCalls int
|
|
lastCompletion TurnCompletion
|
|
}
|
|
|
|
func (f *fakeRepository) ResolveState(context.Context, Scope, time.Time) (State, bool, error) {
|
|
return f.state, f.expired, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (f *fakeRepository) Disable(context.Context, Scope, time.Time) (State, bool, error) {
|
|
f.disableCalls++
|
|
wasReasoning := f.state.Mode == ModeReasoning
|
|
f.state.Mode = ModeQuick
|
|
f.state.UserSelected = true
|
|
f.state.RoundsUsed = 0
|
|
return f.state, wasReasoning, nil
|
|
}
|
|
|
|
func (f *fakeRepository) SetMaxRounds(context.Context, Scope, int, time.Time) (State, error) {
|
|
return f.state, nil
|
|
}
|
|
|
|
func (f *fakeRepository) SetHintEnabled(context.Context, Scope, bool, time.Time) (State, error) {
|
|
return f.state, nil
|
|
}
|
|
|
|
func (f *fakeRepository) CompleteTurn(_ context.Context, input TurnCompletion) (Completion, error) {
|
|
f.completeCalls++
|
|
f.lastCompletion = input
|
|
return Completion{Next: f.state}, nil
|
|
}
|
|
|
|
func newTestService(t *testing.T, repo Repository) *Service {
|
|
t.Helper()
|
|
service, err := NewService(repo, "builtin-quick-answer", "builtin-smart-reasoning")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service.now = func() time.Time { return time.Date(2026, 9, 9, 12, 0, 0, 0, time.UTC) }
|
|
return service
|
|
}
|
|
|
|
func testScope() Scope {
|
|
return Scope{TenantKey: "tenant", Channel: "dingtalk", ExternalActorID: "staff-1"}
|
|
}
|
|
|
|
func TestPrepareEnablesPersistentReasoningLocally(t *testing.T) {
|
|
repo := &fakeRepository{state: DefaultState()}
|
|
decision, err := newTestService(t, repo).Prepare(context.Background(), testScope(), "/think on")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !decision.Handled || decision.Query != "" || repo.enableCalls != 1 {
|
|
t.Fatalf("decision=%+v enableCalls=%d", decision, repo.enableCalls)
|
|
}
|
|
for _, want := range []string{"已开启【智能推理模式】", "计费标准更高", "10轮", "/think off"} {
|
|
if !strings.Contains(decision.Reply, want) {
|
|
t.Fatalf("reply=%q missing %q", decision.Reply, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPrepareDisablesPersistentReasoningLocally(t *testing.T) {
|
|
repo := &fakeRepository{state: State{Mode: ModeReasoning, MaxRounds: 10, HintEnabled: true}}
|
|
decision, err := newTestService(t, repo).Prepare(context.Background(), testScope(), "/rag")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !decision.Handled || repo.disableCalls != 1 || !strings.Contains(decision.Reply, "已关闭智能推理模式") {
|
|
t.Fatalf("decision=%+v disableCalls=%d", decision, repo.disableCalls)
|
|
}
|
|
}
|
|
|
|
func TestPrepareFriendlyReplyWhenReasoningAlreadyOff(t *testing.T) {
|
|
repo := &fakeRepository{state: DefaultState()}
|
|
decision, err := newTestService(t, repo).Prepare(context.Background(), testScope(), "/think off")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !decision.Handled || !strings.Contains(decision.Reply, "当前未开启推理模式") {
|
|
t.Fatalf("decision=%+v", decision)
|
|
}
|
|
}
|
|
|
|
func TestPrepareSingleReasoningDoesNotMutatePersistentState(t *testing.T) {
|
|
repo := &fakeRepository{state: DefaultState()}
|
|
decision, err := newTestService(t, repo).Prepare(context.Background(), testScope(), "/think 分析根因")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decision.Handled || decision.Query != "分析根因" || decision.AgentID != "builtin-smart-reasoning" || decision.Execution != ExecutionReasoningSingle {
|
|
t.Fatalf("decision=%+v", decision)
|
|
}
|
|
if repo.enableCalls != 0 || repo.disableCalls != 0 {
|
|
t.Fatalf("unexpected state mutation: %+v", repo)
|
|
}
|
|
}
|
|
|
|
func TestPrepareRoutesPersistentAndQuickQuestions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
state State
|
|
agentID string
|
|
execution ExecutionMode
|
|
}{
|
|
{name: "quick", state: DefaultState(), agentID: "builtin-quick-answer", execution: ExecutionQuick},
|
|
{name: "reasoning", state: State{Mode: ModeReasoning, MaxRounds: 10, HintEnabled: true}, agentID: "builtin-smart-reasoning", execution: ExecutionReasoning},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
decision, err := newTestService(t, &fakeRepository{state: test.state}).Prepare(context.Background(), testScope(), "普通问题")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decision.Handled || decision.Query != "普通问题" || decision.AgentID != test.agentID || decision.Execution != test.execution {
|
|
t.Fatalf("decision=%+v", decision)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPrepareRoutesExpiredReasoningToQuickWithNotice(t *testing.T) {
|
|
repo := &fakeRepository{state: DefaultState(), expired: true}
|
|
decision, err := newTestService(t, repo).Prepare(context.Background(), testScope(), "继续")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decision.AgentID != "builtin-quick-answer" || decision.Execution != ExecutionQuick || !decision.Expired {
|
|
t.Fatalf("decision=%+v", decision)
|
|
}
|
|
}
|
|
|
|
func TestPrepareInterceptsInvalidThinkCommand(t *testing.T) {
|
|
decision, err := newTestService(t, &fakeRepository{state: DefaultState()}).Prepare(context.Background(), testScope(), "/think on 多余内容")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !decision.Handled || !strings.Contains(decision.Reply, "用法") {
|
|
t.Fatalf("decision=%+v", decision)
|
|
}
|
|
}
|
|
|
|
func TestPrepareReturnsLocalHelpWithoutStateMutation(t *testing.T) {
|
|
repo := &fakeRepository{state: DefaultState()}
|
|
decision, err := newTestService(t, repo).Prepare(context.Background(), testScope(), "/help")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !decision.Handled {
|
|
t.Fatalf("decision=%+v", decision)
|
|
}
|
|
for _, required := range []string{"快速问答模式", "智能推理模式", "/think + 你的问题", "/think on", "/think off", "/setting"} {
|
|
if !strings.Contains(decision.Reply, required) {
|
|
t.Fatalf("help=%q missing %q", decision.Reply, required)
|
|
}
|
|
}
|
|
if repo.enableCalls != 0 || repo.disableCalls != 0 {
|
|
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)
|
|
}
|
|
}
|