Why: fullwidth text missed case-folded terms, SpecByID shared maps, the catalog prompt printed handlers and private metadata, overlapping slots kept both values, and negation was easy to mistake for semantics. Evidence: GOWORK=off go vet ./... && go test -count=1 -race ./... passed, including TestR2NFKCFoldsFullwidthButNotTraditional, TestR2SpecByIDDeepCopiesMetadata, TestR2CatalogHidesPrivateMetadataAndHandlerInMarkdown, TestR2EvalRulesDocumentsMaxSpecs, TestR2OverlappingSlotsAreAmbiguous, and TestR2NegationIsOnlyExcludesAny. Impact: NFKC is opt-in and does not convert traditional Chinese; catalog markdown omits handlers and non-allowlisted metadata; overlapping slot spans are invalid. Refs: I4, I7, I8, I10, I11, I12 Co-authored-by: Cursor <cursoragent@cursor.com>
174 lines
5.0 KiB
Go
174 lines
5.0 KiB
Go
package intent
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestR2NFKCFoldsFullwidthButNotTraditional(t *testing.T) {
|
||
spec := IntentSpec{
|
||
ID: "demo.greet.hello", Domain: "demo", Version: 1,
|
||
Matcher: MatchSpec{IncludesAny: []string{"hello"}},
|
||
}
|
||
b := NewRegistryBuilder(Options{NFKC: true})
|
||
if err := b.Register(spec); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
reg, err := b.Build()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
rec := NewRecognizer(Options{NFKC: true}, nil, nil)
|
||
res, err := rec.Recognize(context.Background(), "Hello", reg.Snapshot())
|
||
if err != nil || res.Top == nil || res.Top.ID != spec.ID {
|
||
t.Fatalf("fullwidth hello: %+v %v", res, err)
|
||
}
|
||
trad, err := rec.Recognize(context.Background(), "寫入", reg.Snapshot())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if trad.Branch != BranchFallback {
|
||
t.Fatalf("NFKC must not convert traditional Chinese, got %+v", trad)
|
||
}
|
||
}
|
||
|
||
func TestR2SpecByIDDeepCopiesMetadata(t *testing.T) {
|
||
b := NewRegistryBuilder(Options{})
|
||
if err := b.Register(IntentSpec{
|
||
ID: "demo.meta.copy", Domain: "demo", Version: 1,
|
||
Matcher: MatchSpec{IncludesAny: []string{"复制"}},
|
||
Metadata: map[string]string{"guidance": "keep"},
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
reg, err := b.Build()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, ok := reg.Snapshot().SpecByID("demo.meta.copy")
|
||
if !ok {
|
||
t.Fatal("missing spec")
|
||
}
|
||
got.Metadata["guidance"] = "mutated"
|
||
again, _ := reg.Snapshot().SpecByID("demo.meta.copy")
|
||
if again.Metadata["guidance"] != "keep" {
|
||
t.Fatalf("snapshot metadata changed: %q", again.Metadata["guidance"])
|
||
}
|
||
}
|
||
|
||
func TestR2CatalogHidesPrivateMetadataAndHandlerInMarkdown(t *testing.T) {
|
||
b := NewRegistryBuilder(Options{})
|
||
if err := b.Register(IntentSpec{
|
||
ID: "demo.catalog.public", Domain: "demo", Version: 1,
|
||
Handler: "secret.handler",
|
||
Matcher: MatchSpec{IncludesAny: []string{"目录"}},
|
||
Metadata: map[string]string{
|
||
"blueprint": "bp",
|
||
"token": "do-not-leak",
|
||
},
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
reg, err := b.Build()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
snap := reg.Snapshot()
|
||
cat := snap.Catalog()
|
||
if len(cat) != 1 {
|
||
t.Fatalf("catalog: %+v", cat)
|
||
}
|
||
if cat[0].Metadata["blueprint"] != "bp" || cat[0].Metadata["token"] != "" {
|
||
t.Fatalf("metadata allowlist: %+v", cat[0].Metadata)
|
||
}
|
||
md := snap.CatalogMarkdown()
|
||
if strings.Contains(md, "secret.handler") || strings.Contains(md, "do-not-leak") || strings.Contains(md, "handler:") {
|
||
t.Fatalf("markdown leaked private fields:\n%s", md)
|
||
}
|
||
}
|
||
|
||
func TestR2EvalRulesDocumentsMaxSpecs(t *testing.T) {
|
||
if DefaultMaxSpecs != 2048 {
|
||
t.Fatalf("linear scan cap changed: %d", DefaultMaxSpecs)
|
||
}
|
||
b := NewRegistryBuilder(Options{MaxSpecs: 1})
|
||
if err := b.Register(
|
||
IntentSpec{ID: "demo.cap.a", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"a"}}},
|
||
IntentSpec{ID: "demo.cap.b", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"b"}}},
|
||
); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := b.Build(); err == nil {
|
||
t.Fatal("build must reject specs above MaxSpecs")
|
||
}
|
||
}
|
||
|
||
func TestR2OverlappingSlotsAreAmbiguous(t *testing.T) {
|
||
b := NewRegistryBuilder(Options{})
|
||
if err := b.Register(IntentSpec{
|
||
ID: "demo.slot.overlap", Domain: "demo", Version: 1,
|
||
Matcher: MatchSpec{IncludesAny: []string{"abcd"}},
|
||
Slots: []SlotSpec{
|
||
{Key: "left", Pattern: "(abc)"},
|
||
{Key: "right", Pattern: "(bcd)"},
|
||
},
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
reg, err := b.Build()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res, err := NewRecognizer(Options{}, nil, nil).Recognize(context.Background(), "abcd", reg.Snapshot())
|
||
if err != nil || res.Top == nil {
|
||
t.Fatalf("recognize: %+v %v", res, err)
|
||
}
|
||
if len(res.Top.Slots) != 0 {
|
||
t.Fatalf("overlapping slots must not be kept: %+v", res.Top.Slots)
|
||
}
|
||
if res.Top.InvalidSlots["left"] != "span overlap" || res.Top.InvalidSlots["right"] != "span overlap" {
|
||
t.Fatalf("invalid slots: %+v", res.Top.InvalidSlots)
|
||
}
|
||
}
|
||
|
||
func TestR2NegationIsOnlyExcludesAny(t *testing.T) {
|
||
plain := IntentSpec{
|
||
ID: "demo.write.file", Domain: "demo", Version: 1,
|
||
Matcher: MatchSpec{IncludesAny: []string{"写入"}},
|
||
}
|
||
excluded := plain
|
||
excluded.ID = "demo.write.excluded"
|
||
excluded.Matcher.ExcludesAny = []string{"不要"}
|
||
rec := NewRecognizer(Options{}, nil, nil)
|
||
|
||
plainBuilder := NewRegistryBuilder(Options{})
|
||
if err := plainBuilder.Register(plain); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
built, err := plainBuilder.Build()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
hit, err := rec.Recognize(context.Background(), "不要写入文件", built.Snapshot())
|
||
if err != nil || hit.Top == nil || hit.Top.ID != plain.ID {
|
||
t.Fatalf("without excludes_any the negated phrase still matches: %+v %v", hit, err)
|
||
}
|
||
|
||
exBuilder := NewRegistryBuilder(Options{})
|
||
if err := exBuilder.Register(excluded); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
builtEx, err := exBuilder.Build()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
miss, err := rec.Recognize(context.Background(), "不要写入文件", builtEx.Snapshot())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if miss.Branch != BranchFallback {
|
||
t.Fatalf("excludes_any must reject, got %+v", miss)
|
||
}
|
||
}
|