Files
Agent-System-Backend/internal/intentsem/intentsem_test.go
T
weichen 45b011af9f
ci / vet-and-test (pull_request) Failing after 34s
Wire the intent slow lane behind settings flags (I2 WP3)
Why: I2 increments 1-2 shipped the agent-intent ports (SemanticScorer,
CommandGenerator); the backend still had no implementation, so the slow
lane could not be enabled in the product.
Evidence: go test ./... green; lint-boundaries ok; the two new
runtime.intentSemantic / runtime.intentGenerator flags are registered
with default false and the gate defaults to the I1 recognizer when both
are off (TestIntentSlowLaneFlagsDefaultOff, TestGate*).
Impact: no behavior change by default; enabling runtime.intentSemantic
routes ambiguous rule matches through embedding cosine scoring, and
runtime.intentGenerator lets the lightweight model propose commands on
fallback with library-side fail-closed validation. Nested agent-intent
pin moves to 343b1e6 (I2 increments 1-2).
Refs: I2 increment 3
2026-10-07 11:20:41 +00:00

150 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package intentsem
import (
"context"
"errors"
"strings"
"testing"
agentintent "agent-intent/intent"
agentllm "agent-native/runtime/llm"
)
// fakeEmbedder 返回脚本化向量:texts[i] → vecs[i](循环取模)。
type fakeEmbedder struct {
vecs [][]float32
err error
}
func (f fakeEmbedder) Embed(_ context.Context, texts []string) ([][]float32, error) {
if f.err != nil {
return nil, f.err
}
out := make([][]float32, len(texts))
for i := range texts {
out[i] = f.vecs[i%len(f.vecs)]
}
return out, nil
}
func TestEmbeddingScorerMaxCosine(t *testing.T) {
// input 与 example2 同向(相似度 1),与 example1 正交(0)。
s := EmbeddingScorer{Provider: fakeEmbedder{vecs: [][]float32{
{1, 0}, {0, 1}, {2, 0},
}}}
got, err := s.Score(context.Background(), "输入", []string{"例一", "例二"})
if err != nil {
t.Fatal(err)
}
if got != 1 {
t.Fatalf("期望最大余弦 1,得到 %v", got)
}
}
func TestEmbeddingScorerErrorPropagates(t *testing.T) {
s := EmbeddingScorer{Provider: fakeEmbedder{err: errors.New("boom")}}
if _, err := s.Score(context.Background(), "x", []string{"y"}); err == nil {
t.Fatal("期望错误上抛(库侧保持 Ambiguous)")
}
}
func TestEmbeddingScorerEmptyExamples(t *testing.T) {
s := EmbeddingScorer{Provider: fakeEmbedder{vecs: [][]float32{{1}}}}
got, err := s.Score(context.Background(), "x", nil)
if err != nil || got != 0 {
t.Fatalf("空示例应得 0 分无错误,得到 %v / %v", got, err)
}
}
// scriptedStream 回放固定文本增量后关闭通道。
func scriptedStream(text string, err error) agentllm.StreamFunc {
return func(_ context.Context, _ string, msgs []agentllm.Message, _ []agentllm.ToolDecl, _ agentllm.StreamOptions) <-chan agentllm.Event {
ch := make(chan agentllm.Event, 3)
if err != nil {
ch <- agentllm.Event{Kind: agentllm.KindError, Err: err}
} else {
ch <- agentllm.Event{Kind: agentllm.KindTextDelta, Text: text}
}
close(ch)
return ch
}
}
func catalogFixture() []agentintent.CatalogSpec {
return []agentintent.CatalogSpec{
{ID: "deploy.run", Description: "部署应用", Slots: []agentintent.CatalogSlot{{Key: "env", Required: true}}},
}
}
func TestLightGeneratorParsesCommands(t *testing.T) {
g := LightGenerator{
Stream: scriptedStream(" ```json\n[{\"kind\":\"start\",\"action\":{\"kind\":\"workflow\",\"id\":\"deploy.run\"},\"slots\":{\"env\":\"prod\"}}]\n``` ", nil),
Model: "lite",
}
cmds, err := g.Generate(context.Background(), "把应用部署到生产", catalogFixture())
if err != nil {
t.Fatal(err)
}
if len(cmds) != 1 || cmds[0].Kind != agentintent.CommandStart {
t.Fatalf("期望 1 条 start 命令,得到 %+v", cmds)
}
if cmds[0].Action.Kind != "workflow" || cmds[0].Action.ID != "deploy.run" {
t.Fatalf("action 不符:%+v", cmds[0].Action)
}
if cmds[0].Slots["env"] != "prod" {
t.Fatalf("槽位不符:%+v", cmds[0].Slots)
}
}
func TestLightGeneratorEmptyCatalogNoCall(t *testing.T) {
called := false
g := LightGenerator{Stream: func(_ context.Context, _ string, _ []agentllm.Message, _ []agentllm.ToolDecl, _ agentllm.StreamOptions) <-chan agentllm.Event {
called = true
ch := make(chan agentllm.Event)
close(ch)
return ch
}}
cmds, err := g.Generate(context.Background(), "你好", nil)
if err != nil || cmds != nil || called {
t.Fatalf("空目录不应调用模型:cmds=%v err=%v called=%v", cmds, err, called)
}
}
func TestLightGeneratorStreamError(t *testing.T) {
g := LightGenerator{Stream: scriptedStream("", errors.New("dial refused"))}
if _, err := g.Generate(context.Background(), "部署", catalogFixture()); err == nil ||
!strings.Contains(err.Error(), "dial refused") {
t.Fatalf("期望流错误上抛,得到 %v", err)
}
}
func TestLightGeneratorBadJSON(t *testing.T) {
g := LightGenerator{Stream: scriptedStream("我无法判断", nil)}
if _, err := g.Generate(context.Background(), "部署", catalogFixture()); err == nil {
t.Fatal("期望解析失败返回错误(库侧 fail-closed 回 fallback)")
}
}
func TestParseCommandsFencedAndChatty(t *testing.T) {
raw := "好的,命令如下:\n```json\n[{\"kind\":\"chitchat\"}]\n```\n以上。"
cmds, err := parseCommands(raw)
if err != nil {
t.Fatal(err)
}
if len(cmds) != 1 || cmds[0].Kind != agentintent.CommandChitchat {
t.Fatalf("期望 chitchat,得到 %+v", cmds)
}
}
func TestGeneratorPromptContainsCatalogOnly(t *testing.T) {
p := generatorSystemPrompt(catalogFixture())
for _, want := range []string{"deploy.run", "部署应用", "env"} {
if !strings.Contains(p, want) {
t.Fatalf("提示词缺少 %q", want)
}
}
if strings.Contains(p, "正例") || strings.Contains(p, "examples") {
t.Fatal("提示词不应包含示例文本(注入面)")
}
}