ci / vet-and-test (pull_request) Failing after 34s
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
150 lines
4.7 KiB
Go
150 lines
4.7 KiB
Go
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("提示词不应包含示例文本(注入面)")
|
||
}
|
||
}
|