Files
Agent-Intent/intent/command_test.go
T
weichen fa512ec03c
ci / vet-and-test (pull_request) Successful in 17s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Add command model, priority tie-break, and boundary matching
Why: I1 increment one - recognition output becomes a command list
for the host router, same-score ties resolve by priority (I5), and
CJK substring false positives get an opt-in boundary mode (I3).

Evidence: go vet clean; go test ./... all ok, including new command
(6), boundary (3), and tie-break tests; intentctl lint smoke run
shows boundary hints on short unbounded terms. Existing classifier
and ambiguity fixtures repointed to an equal-priority pair; the
greet corpus case now expects the I5 tie-break outcome.

Impact: Result gains Commands; recognizer derives them at one point
(guard/fallback stay empty); MatchSpec gains Boundary (default off,
legacy behavior unchanged); lint gains boundary hints. Top/Branch
stay for compatibility.

Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md I1 WP1/WP4/WP5
2026-10-07 07:21:55 +00:00

165 lines
5.2 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 intent
import (
"context"
"testing"
)
// buildCommandSnap 组装命令测试用快照:一个带必填槽位的意图与一个无槽位意图。
func buildCommandSnap(t *testing.T) *Snapshot {
t.Helper()
b := NewRegistryBuilder(Options{})
if err := b.Register(IntentSpec{
ID: "report.generate", Domain: "report", Version: 1,
Matcher: MatchSpec{IncludesAny: []string{"生成报告"}},
Slots: []SlotSpec{
{Key: "topic", Required: true, Pattern: `主题是(.+)`, Question: "要查询哪个主题?"},
},
Handler: "reportHandler",
}); err != nil {
t.Fatal(err)
}
if err := b.Register(IntentSpec{
ID: "report.chat", Domain: "report", Version: 1,
Matcher: MatchSpec{IncludesAny: []string{"闲聊一下"}},
}); err != nil {
t.Fatal(err)
}
reg, err := b.Build()
if err != nil {
t.Fatal(err)
}
return reg.Snapshot()
}
func recognize(t *testing.T, snap *Snapshot, input string) Result {
t.Helper()
res, err := NewRecognizer(Options{}, nil, nil).Recognize(context.Background(), input, snap)
if err != nil {
t.Fatal(err)
}
return res
}
// TestCommandsMatchedFullSlots:槽位齐全 → 单条 Start,ActionRef 取 Handler。
func TestCommandsMatchedFullSlots(t *testing.T) {
snap := buildCommandSnap(t)
res := recognize(t, snap, "帮我生成报告,主题是电机")
if res.Branch != BranchMatched {
t.Fatalf("branch = %s", res.Branch)
}
if len(res.Commands) != 1 {
t.Fatalf("commands = %+v", res.Commands)
}
cmd := res.Commands[0]
if cmd.Kind != CommandStart || cmd.Action.Kind != ActionHandler || cmd.Action.ID != "reportHandler" {
t.Fatalf("start = %+v", cmd)
}
if cmd.Slots["topic"] != "电机" {
t.Fatalf("slots = %v", cmd.Slots)
}
}
// TestCommandsMatchedMissingSlot:必填缺失 → Start(带已抽槽)+ Clarify(含建议提问)。
func TestCommandsMatchedMissingSlot(t *testing.T) {
snap := buildCommandSnap(t)
res := recognize(t, snap, "帮我生成报告")
if len(res.Commands) != 2 {
t.Fatalf("commands = %+v", res.Commands)
}
start, clarify := res.Commands[0], res.Commands[1]
if start.Kind != CommandStart {
t.Fatalf("first = %+v", start)
}
if clarify.Kind != CommandClarify || len(clarify.Missing) != 1 || clarify.Missing[0] != "topic" {
t.Fatalf("clarify = %+v", clarify)
}
if clarify.Question != "要查询哪个主题?" {
t.Fatalf("question = %q", clarify.Question)
}
}
// TestCommandsExpectationSetSlotPlusStart:预期命中 → SetSlot + Start(验收形态)。
func TestCommandsExpectationSetSlotPlusStart(t *testing.T) {
snap := buildCommandSnap(t)
exp := listExpectation{ids: []string{"report.generate"}}
res, err := NewRecognizer(Options{}, exp, nil).Recognize(context.Background(), "生成报告,主题是电机", snap)
if err != nil {
t.Fatal(err)
}
if res.Branch != BranchExpectationHit {
t.Fatalf("branch = %s", res.Branch)
}
var setSlot, start *Command
for i := range res.Commands {
switch res.Commands[i].Kind {
case CommandSetSlot:
setSlot = &res.Commands[i]
case CommandStart:
start = &res.Commands[i]
}
}
if setSlot == nil || start == nil {
t.Fatalf("commands = %+v", res.Commands)
}
if setSlot.Name != "topic" || setSlot.Value != "电机" {
t.Fatalf("set_slot = %+v", *setSlot)
}
if start.Action.Kind != ActionHandler || start.Slots["topic"] != "电机" {
t.Fatalf("start = %+v", *start)
}
}
// TestCommandsAmbiguousClarify:标歧 → 单条 Clarify 携带候选 ID。
func TestCommandsAmbiguousClarify(t *testing.T) {
b := NewRegistryBuilder(Options{})
if err := b.Register(IntentSpec{ID: "demo.alpha", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"模糊词"}}}); err != nil {
t.Fatal(err)
}
if err := b.Register(IntentSpec{ID: "demo.beta", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"模糊词"}}}); err != nil {
t.Fatal(err)
}
reg, err := b.Build()
if err != nil {
t.Fatal(err)
}
res := recognize(t, reg.Snapshot(), "模糊词")
if res.Branch != BranchAmbiguous {
t.Fatalf("branch = %s", res.Branch)
}
if len(res.Commands) != 1 || res.Commands[0].Kind != CommandClarify {
t.Fatalf("commands = %+v", res.Commands)
}
if len(res.Commands[0].Candidates) != 2 {
t.Fatalf("candidates = %v", res.Commands[0].Candidates)
}
}
// TestCommandsFallbackEmpty:fallback 不产出命令。
func TestCommandsFallbackEmpty(t *testing.T) {
snap := buildCommandSnap(t)
res := recognize(t, snap, "完全不相关的一句话")
if res.Branch != BranchFallback || len(res.Commands) != 0 {
t.Fatalf("fallback commands = %+v", res.Commands)
}
}
// TestActionRefDerivation:blueprint 键优先于 Handler;都没有回落 auto。
func TestActionRefDerivation(t *testing.T) {
if got := actionOf(Candidate{ID: "x", Metadata: map[string]string{"blueprint": "bp-1"}, Handler: "h"}); got.Kind != ActionBlueprint || got.ID != "bp-1" {
t.Fatalf("blueprint = %+v", got)
}
if got := actionOf(Candidate{ID: "x", Handler: "h"}); got.Kind != ActionHandler || got.ID != "h" {
t.Fatalf("handler = %+v", got)
}
if got := actionOf(Candidate{ID: "x"}); got.Kind != ActionAuto || got.ID != "x" {
t.Fatalf("auto = %+v", got)
}
}
type listExpectation struct{ ids []string }
func (s listExpectation) Pending(ctx context.Context) *PendingExpectation {
return &PendingExpectation{IntentIDs: s.ids}
}