Files
Agent-System-Backend/internal/intentgate/gate_test.go
T
weichenandCursor ebeb6c599c Route a matched intent to its workflow after commands
Why: A hit intent should start the bound workflow, and a command that shares the same id still has to win.
Evidence: go test ./internal/intentgate/ ./internal/runmgr/ ./internal/server/ passed. Recognize keeps slots and the missing-slot question. StartWorkflowRun copies the definition and selects the workflow runner even when mock is on.
Impact: Ordinary turns are unchanged. The workflow runner body is still a stub until the executor lands.
Refs: feature/agent-harness-p4
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 16:04:49 +00:00

94 lines
2.9 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 intentgate
import (
"context"
"encoding/json"
"testing"
)
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)
}
}