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
179 lines
5.9 KiB
Go
179 lines
5.9 KiB
Go
package intentgate
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"testing"
|
||
|
||
agentintent "agent-intent/intent"
|
||
)
|
||
|
||
func raw(t *testing.T, v any) json.RawMessage {
|
||
t.Helper()
|
||
b, err := json.Marshal(v)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return b
|
||
}
|
||
|
||
// spec 采用 agent-intent overlay 形态(matcher 嵌套)。
|
||
func demoSpec(t *testing.T) json.RawMessage {
|
||
return raw(t, map[string]any{
|
||
"id": "demo.show", "domain": "demo", "version": 1,
|
||
"description": "示例意图",
|
||
"matcher": map[string]any{"includes_any": []string{"示例", "演示"}},
|
||
})
|
||
}
|
||
|
||
func TestGateMatchAndFallback(t *testing.T) {
|
||
g := New()
|
||
if _, id := g.Match(context.Background(), "任意"); id != "" || g.Enabled() {
|
||
t.Error("empty gate should passthrough")
|
||
}
|
||
if err := g.Rebuild(context.Background(), []json.RawMessage{demoSpec(t)}); err != nil {
|
||
t.Fatalf("rebuild: %v", err)
|
||
}
|
||
if !g.Enabled() || g.SpecCount() != 1 {
|
||
t.Fatalf("enabled=%v specs=%d", g.Enabled(), g.SpecCount())
|
||
}
|
||
branch, top := g.Match(context.Background(), "来个示例看看")
|
||
if branch != "matched" || top != "demo.show" {
|
||
t.Errorf("want matched/demo_intent, got %s/%s", branch, top)
|
||
}
|
||
branch, _ = g.Match(context.Background(), "完全无关的一句话")
|
||
if branch != "fallback" {
|
||
t.Errorf("want fallback, got %s", branch)
|
||
}
|
||
}
|
||
|
||
// fail-closed:坏 spec 保留上一快照。
|
||
func TestGateFailClosed(t *testing.T) {
|
||
g := New()
|
||
if err := g.Rebuild(context.Background(), []json.RawMessage{demoSpec(t)}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := g.Rebuild(context.Background(), []json.RawMessage{json.RawMessage(`{"id":`)}); err == nil {
|
||
t.Error("broken spec should error")
|
||
}
|
||
if !g.Enabled() || g.SpecCount() != 1 {
|
||
t.Error("previous snapshot must be kept on failed rebuild")
|
||
}
|
||
// 空重建 = 显式清空(卸载全部插件意图)。
|
||
if err := g.Rebuild(context.Background(), nil); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if g.Enabled() {
|
||
t.Error("empty rebuild should disable gate")
|
||
}
|
||
}
|
||
|
||
func TestRecognizeKeepsSlots(t *testing.T) {
|
||
g := New()
|
||
spec := raw(t, map[string]any{
|
||
"id": "demo.brief", "domain": "demo", "version": 1,
|
||
"matcher": map[string]any{"includes_any": []string{"简报"}},
|
||
"slots": []map[string]any{{
|
||
"key": "topic", "required": true, "pattern": `主题[::]\s*(\S+)`,
|
||
"question": "主题是什么?",
|
||
}},
|
||
})
|
||
if err := g.Rebuild(context.Background(), []json.RawMessage{spec}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
miss := g.Recognize(context.Background(), "写一份简报")
|
||
if miss.Branch != "matched" || miss.ID != "demo.brief" || len(miss.Missing) != 1 || miss.Missing[0].Question != "主题是什么?" {
|
||
t.Fatalf("%+v", miss)
|
||
}
|
||
hit := g.Recognize(context.Background(), "主题:核能 写一份简报")
|
||
if hit.Slots["topic"] != "核能" || len(hit.Missing) != 0 {
|
||
t.Fatalf("%+v", hit)
|
||
}
|
||
if branch, id := g.Match(context.Background(), "写一份简报"); branch != "matched" || id != "demo.brief" {
|
||
t.Fatalf("Match wrapper = %s %s", branch, id)
|
||
}
|
||
}
|
||
|
||
// ---- I2 慢通道接线(默认关;SetSemantic/SetGenerator 开启)----
|
||
|
||
// stubScorer 固定返回脚本分(按 examples[0] 查表)。
|
||
type stubScorer struct{ scores map[string]float64 }
|
||
|
||
func (s stubScorer) Score(_ context.Context, _ string, examples []string) (float64, error) {
|
||
if len(examples) == 0 {
|
||
return 0, nil
|
||
}
|
||
return s.scores[examples[0]], nil
|
||
}
|
||
|
||
// stubGenerator 固定返回脚本命令。
|
||
type stubGenerator struct{ cmds []Command }
|
||
|
||
func (g stubGenerator) Generate(_ context.Context, _ string, _ []agentintent.CatalogSpec) ([]agentintent.Command, error) {
|
||
return g.cmds, nil
|
||
}
|
||
|
||
func ambiguousOverlay(t *testing.T) []json.RawMessage {
|
||
t.Helper()
|
||
mk := func(id string, ex []string) json.RawMessage {
|
||
return raw(t, map[string]any{
|
||
"id": id, "domain": "demo", "version": 1, "priority": 5,
|
||
"matcher": map[string]any{"includes_any": []string{"模糊词"}},
|
||
"examples": ex,
|
||
})
|
||
}
|
||
return []json.RawMessage{mk("demo.alpha", []string{"查运行进度"}), mk("demo.beta", []string{"闲聊问候"})}
|
||
}
|
||
|
||
// 语义层:歧义被过阈打分裁决为 matched(默认关时不启用)。
|
||
func TestGateSemanticResolvesAmbiguous(t *testing.T) {
|
||
g := New()
|
||
if err := g.Rebuild(context.Background(), ambiguousOverlay(t)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 默认(未接线):规则层歧义原样透出。
|
||
if branch, _ := g.Match(context.Background(), "模糊词"); branch != "ambiguous" {
|
||
t.Fatalf("默认关:branch = %s", branch)
|
||
}
|
||
g.SetSemantic(stubScorer{scores: map[string]float64{"查运行进度": 0.95, "闲聊问候": 0.4}}, 0)
|
||
branch, top := g.Match(context.Background(), "模糊词")
|
||
if branch != "matched" || top != "demo.alpha" {
|
||
t.Fatalf("语义裁决:%s/%s", branch, top)
|
||
}
|
||
// 关闭后回到歧义。
|
||
g.SetSemantic(nil, 0)
|
||
if branch, _ := g.Match(context.Background(), "模糊词"); branch != "ambiguous" {
|
||
t.Fatalf("关闭语义层:branch = %s", branch)
|
||
}
|
||
}
|
||
|
||
// 生成层:fallback 时产出合法 start 命令 → matched + Commands。
|
||
func TestGateGeneratorProducesCommands(t *testing.T) {
|
||
g := New()
|
||
overlay := []json.RawMessage{raw(t, map[string]any{
|
||
"id": "report.generate", "domain": "report", "version": 1,
|
||
"description": "生成报告",
|
||
"matcher": map[string]any{"includes_any": []string{"生成报告"}},
|
||
"handler": "reportHandler",
|
||
})}
|
||
if err := g.Rebuild(context.Background(), overlay); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
g.SetGenerator(stubGenerator{cmds: []Command{{
|
||
Kind: CommandStart,
|
||
Action: ActionRef{Kind: ActionHandler, ID: "reportHandler"},
|
||
}}})
|
||
res := g.Recognize(context.Background(), "完全不相关的一句话")
|
||
if res.Branch != "matched" || res.ID != "report.generate" {
|
||
t.Fatalf("生成器命中:%+v", res)
|
||
}
|
||
if len(res.Commands) != 1 || res.Commands[0].Kind != CommandStart {
|
||
t.Fatalf("commands: %+v", res.Commands)
|
||
}
|
||
// 关闭后回到 fallback。
|
||
g.SetGenerator(nil)
|
||
if branch, _ := g.Match(context.Background(), "完全不相关的一句话"); branch != "fallback" {
|
||
t.Fatalf("关闭生成层:branch = %s", branch)
|
||
}
|
||
}
|