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

161 lines
4.4 KiB
Go

package agent
import (
"context"
"testing"
"reasonix/internal/provider"
)
// TestHeuristicClassifier_IsTask 测试启发式分类器
func TestHeuristicClassifier_IsTask(t *testing.T) {
classifier := newHeuristicClassifier()
ctx := context.Background()
tests := []struct {
name string
input string
want bool
}{
// 问候语 - 聊天
{"hello", "hello", false},
{"hi", "hi", false},
{"你好", "你好", false},
{"thanks", "thanks", false},
{"谢谢", "谢谢", false},
{"ok", "ok", false},
{"好的", "好的", false},
{"收到", "收到", false},
{"我知道了", "我知道了", false},
{"先不用", "先不用", false},
// 明确的任务
{"fix bug", "fix the bug", true},
{"create component", "create a component", true},
{"修复问题", "修复这个问题", true},
{"run tests", "run tests", true},
{"看看", "帮我看看这个错误", true},
{"帮我看下", "帮我看下这个问题", true},
{"处理下", "处理下这个 issue", true},
{"排查", "排查一下启动失败", true},
{"定位", "定位这个异常", true},
// Conversational acknowledgements that contain task words.
{"thanks for fixing", "thanks for fixing that!", false},
{"check later", "I'll check later", false},
{"test later", "I'll test it later", false},
{"test was helpful", "that test was helpful", false},
{"辛苦了", "辛苦了", false},
// Actionable problem descriptions without imperative verbs.
{"auth not working", "the auth isn't working", true},
{"help with login", "can you help with login?", true},
{"问题严重", "这个问题很严重", true},
{"卡住了", "页面卡住了", true},
{"没反应", "按钮点击没反应", true},
{"不生效", "配置不生效", true},
{"异常退出", "程序异常退出", true},
// 文件引用
{"file reference", "what about @auth.go", true},
{"python file", "check main.py", true},
// 边界情况
{"empty", "", false},
{"spaces", " ", false},
{"question mark", "?", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := classifier.IsTask(ctx, tt.input)
if err != nil {
t.Fatalf("IsTask() error = %v", err)
}
if got != tt.want {
t.Errorf("IsTask(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
// fakeClassifierProvider 用于测试 LLM 分类器的 mock provider
type fakeClassifierProvider struct {
reply string
streamErr error
}
func (f *fakeClassifierProvider) Name() string { return "fake" }
func (f *fakeClassifierProvider) Stream(_ context.Context, req provider.Request) (<-chan provider.Chunk, error) {
ch := make(chan provider.Chunk, 2)
if f.streamErr != nil {
ch <- provider.Chunk{Type: provider.ChunkError, Err: f.streamErr}
close(ch)
return ch, nil
}
ch <- provider.Chunk{Type: provider.ChunkText, Text: f.reply}
ch <- provider.Chunk{Type: provider.ChunkDone}
close(ch)
return ch, nil
}
// TestLLMClassifier_IsTask 测试 LLM 分类器
func TestLLMClassifier_IsTask(t *testing.T) {
ctx := context.Background()
heuristic := newHeuristicClassifier()
tests := []struct {
name string
input string
llmReply string
want bool
streamErr error
}{
{"llm says task", "fix the bug", "task", true, nil},
{"llm says chat", "hello", "chat", false, nil},
{"llm error fallback", "fix bug", "", true, context.DeadlineExceeded},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prov := &fakeClassifierProvider{reply: tt.llmReply, streamErr: tt.streamErr}
classifier := newLLMClassifier(prov, heuristic)
got, err := classifier.IsTask(ctx, tt.input)
if err != nil {
t.Fatalf("IsTask() error = %v", err)
}
if got != tt.want {
t.Errorf("IsTask(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
// TestLLMClassifier_Cache 测试 LLM 分类器缓存
func TestLLMClassifier_Cache(t *testing.T) {
ctx := context.Background()
prov := &fakeClassifierProvider{reply: "task"}
heuristic := newHeuristicClassifier()
classifier := newLLMClassifier(prov, heuristic)
// 第一次调用应该调用 LLM
got1, err1 := classifier.IsTask(ctx, "fix bug")
if err1 != nil {
t.Fatalf("IsTask() error = %v", err1)
}
if !got1 {
t.Error("expected task classification")
}
// 第二次调用相同输入应该从缓存返回
got2, err2 := classifier.IsTask(ctx, "fix bug")
if err2 != nil {
t.Fatalf("IsTask() error = %v", err2)
}
if !got2 {
t.Error("expected cached task classification")
}
}