Files
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

116 lines
3.7 KiB
Go

package intent
import (
"context"
"testing"
)
// TestBoundaryCJKFalsePositiveSuppressed:词界开启后,「配置」不命中「配置项」(I3)。
func TestBoundaryCJKFalsePositiveSuppressed(t *testing.T) {
mk := func(boundary bool) *Snapshot {
b := NewRegistryBuilder(Options{})
if err := b.Register(IntentSpec{
ID: "demo.configure", Domain: "demo", Version: 1,
Matcher: MatchSpec{IncludesAny: []string{"配置"}, Boundary: boundary},
}); err != nil {
t.Fatal(err)
}
reg, err := b.Build()
if err != nil {
t.Fatal(err)
}
return reg.Snapshot()
}
res, err := NewRecognizer(Options{}, nil, nil).Recognize(context.Background(), "配置项在哪里", mk(true))
if err != nil {
t.Fatal(err)
}
if res.Branch != BranchFallback {
t.Fatalf("boundary on: branch = %s, want fallback", res.Branch)
}
// 词界关闭(旧行为):子串命中依旧,证明差异来自 boundary。
res, err = NewRecognizer(Options{}, nil, nil).Recognize(context.Background(), "配置项在哪里", mk(false))
if err != nil {
t.Fatal(err)
}
if res.Branch != BranchMatched {
t.Fatalf("boundary off: branch = %s, want matched", res.Branch)
}
// 词界开启时,独立成词的「配置」照常命中。
res, err = NewRecognizer(Options{}, nil, nil).Recognize(context.Background(), "我要改配置", mk(true))
if err != nil {
t.Fatal(err)
}
if res.Branch != BranchMatched {
t.Fatalf("boundary on, standalone term: branch = %s, want matched", res.Branch)
}
}
// TestBoundaryLatinWordFlank:拉丁词界——write 不命中 rewrites。
func TestBoundaryLatinWordFlank(t *testing.T) {
b := NewRegistryBuilder(Options{})
if err := b.Register(IntentSpec{
ID: "demo.write", Domain: "demo", Version: 1,
Matcher: MatchSpec{IncludesAny: []string{"write"}, Boundary: true},
}); err != nil {
t.Fatal(err)
}
reg, err := b.Build()
if err != nil {
t.Fatal(err)
}
snap := reg.Snapshot()
res, err := NewRecognizer(Options{}, nil, nil).Recognize(context.Background(), "rewrites everything", snap)
if err != nil {
t.Fatal(err)
}
if res.Branch != BranchFallback {
t.Fatalf("branch = %s, want fallback", res.Branch)
}
res, err = NewRecognizer(Options{}, nil, nil).Recognize(context.Background(), "please write this", snap)
if err != nil {
t.Fatal(err)
}
if res.Branch != BranchMatched {
t.Fatalf("branch = %s, want matched", res.Branch)
}
}
// TestTieBreakByPriority:同分时高 Priority 胜出(I5);同分同 Priority 仍标歧。
func TestTieBreakByPriority(t *testing.T) {
mk := func(p1, p2 int) *Snapshot {
b := NewRegistryBuilder(Options{})
if err := b.Register(IntentSpec{ID: "demo.alpha", Domain: "demo", Version: 1, Priority: p1, Matcher: MatchSpec{IncludesAny: []string{"同分词"}}}); err != nil {
t.Fatal(err)
}
if err := b.Register(IntentSpec{ID: "demo.beta", Domain: "demo", Version: 1, Priority: p2, Matcher: MatchSpec{IncludesAny: []string{"同分词"}}}); err != nil {
t.Fatal(err)
}
reg, err := b.Build()
if err != nil {
t.Fatal(err)
}
return reg.Snapshot()
}
res, err := NewRecognizer(Options{}, nil, nil).Recognize(context.Background(), "同分词", mk(10, 0))
if err != nil {
t.Fatal(err)
}
if res.Branch != BranchMatched || res.Top == nil || res.Top.ID != "demo.alpha" {
t.Fatalf("tie-break: branch = %s top = %+v", res.Branch, res.Top)
}
// 同分同 Priority:仍 ambiguous。
res, err = NewRecognizer(Options{}, nil, nil).Recognize(context.Background(), "同分词", mk(5, 5))
if err != nil {
t.Fatal(err)
}
if res.Branch != BranchAmbiguous {
t.Fatalf("same priority: branch = %s, want ambiguous", res.Branch)
}
// Explain 与裁决同规则。
tr := mk(10, 0).Explain("同分词")
if tr.Branch != BranchMatched || tr.TopID != "demo.alpha" {
t.Fatalf("explain = %+v", tr)
}
}