Files
Agent-Intent/intent/r2_regression_test.go
T
weichenandCursor 05e74d8e32
ci / vet-and-test (pull_request) Successful in 18s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Fold NFKC input and stop catalog metadata leaks
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>
2026-10-03 12:56:55 +00:00

174 lines
5.0 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"
"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)
}
}