Why: Refine could swap handlers or emit out-of-range scores, Ambiguous tops skipped slot extraction, and the process-global validator map coupled independent registries. Evidence: GOWORK=off go vet ./... && go test -count=1 ./... && go test -race ./... passed, including TestRecognizeClassifierRejectsBadRefinement, TestRecognizeClassifierRebuildsTamperedHandler, TestRecognizeClassifierRefinedTopGetsSlots, and TestScopedSlotValidatorsDoNotInterfere. error-code-system and logging-system advanced to a3ee67b and 49b5d8b (docs-only). Impact: illegal Refine results stay Ambiguous; a resolved Top is slotted; Options.SlotValidators wins over the deprecated global table. Refs: I1, I2, I6 Co-authored-by: Cursor <cursoragent@cursor.com>
117 lines
3.9 KiB
Go
117 lines
3.9 KiB
Go
package intent
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func validSpec() IntentSpec {
|
|
return IntentSpec{
|
|
ID: "demo.run.status_query",
|
|
Domain: "demo",
|
|
Version: 1,
|
|
Matcher: MatchSpec{IncludesAny: []string{"进度"}},
|
|
}
|
|
}
|
|
|
|
func TestValidateSpecAcceptsValid(t *testing.T) {
|
|
lim := Options{}.normalized().limits()
|
|
if err := validateSpec(validSpec(), lim, nil); err != nil {
|
|
t.Fatalf("valid spec rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateSpecDefaultsNoLimits(t *testing.T) {
|
|
// 零值 limits 跳过数量上限(供意图较少的宿主直接校验)。
|
|
if err := validateSpec(validSpec(), limits{}, nil); err != nil {
|
|
t.Fatalf("valid spec rejected without limits: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateSpecRejections(t *testing.T) {
|
|
lim := Options{}.normalized().limits()
|
|
cases := []struct {
|
|
name string
|
|
mut func(*IntentSpec)
|
|
field string
|
|
}{
|
|
{"empty_domain", func(s *IntentSpec) { s.Domain = "" }, "domain"},
|
|
{"bad_domain_case", func(s *IntentSpec) { s.Domain = "Demo" }, "domain"},
|
|
{"id_prefix_mismatch", func(s *IntentSpec) { s.ID = "other.status_query" }, "id"},
|
|
{"bad_mnemonic", func(s *IntentSpec) { s.ID = "demo.Status-Query" }, "id"},
|
|
{"zero_version", func(s *IntentSpec) { s.Version = 0 }, "version"},
|
|
{"bad_risk", func(s *IntentSpec) { s.Risk = "extreme" }, "risk"},
|
|
{"empty_matcher", func(s *IntentSpec) { s.Matcher = MatchSpec{} }, "matcher"},
|
|
{"empty_term", func(s *IntentSpec) { s.Matcher = MatchSpec{IncludesAny: []string{" "}} }, "matcher.includes_any"},
|
|
{"bad_pattern", func(s *IntentSpec) { s.Matcher = MatchSpec{IncludesAny: []string{"a"}, Patterns: []string{"["}} }, "matcher.patterns[0]"},
|
|
{"too_many_patterns", func(s *IntentSpec) {
|
|
s.Matcher = MatchSpec{IncludesAny: []string{"a"}, Patterns: make([]string, lim.maxPatternsPerSpec+1)}
|
|
for i := range s.Matcher.Patterns {
|
|
s.Matcher.Patterns[i] = "x"
|
|
}
|
|
}, "matcher.patterns"},
|
|
{"bad_slot_key", func(s *IntentSpec) {
|
|
s.Slots = []SlotSpec{{Key: "RunID", Pattern: "(x)"}}
|
|
}, "slots[0].key"},
|
|
{"bad_slot_pattern", func(s *IntentSpec) {
|
|
s.Slots = []SlotSpec{{Key: "run_id", Pattern: "["}}
|
|
}, "slots[0].pattern"},
|
|
{"slot_group_out_of_range", func(s *IntentSpec) {
|
|
s.Slots = []SlotSpec{{Key: "run_id", Pattern: "(x)", Group: 2}}
|
|
}, "slots[0].group"},
|
|
{"base_out_of_range", func(s *IntentSpec) {
|
|
s.Confidence = ConfidenceSpec{Base: 1.5}
|
|
}, "confidence.base"},
|
|
{"bonus_out_of_range", func(s *IntentSpec) {
|
|
s.Confidence = ConfidenceSpec{PatternBonus: 1.0}
|
|
}, "confidence.pattern_bonus"},
|
|
{"threshold_out_of_range", func(s *IntentSpec) {
|
|
s.Confidence = ConfidenceSpec{Threshold: -0.1}
|
|
}, "confidence.threshold"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
spec := validSpec()
|
|
tc.mut(&spec)
|
|
err := validateSpec(spec, lim, nil)
|
|
if err == nil {
|
|
t.Fatalf("expected rejection")
|
|
}
|
|
var e *Error
|
|
if !errors.As(err, &e) || e.Code != InvalidSpec {
|
|
t.Fatalf("expected intent.invalid_spec, got %v", err)
|
|
}
|
|
if got, _ := e.Fields["field"].(string); got != tc.field && !hasPrefix(got, tc.field) {
|
|
t.Fatalf("expected field %q, got %q (fields=%v)", tc.field, got, e.Fields)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func hasPrefix(s, p string) bool {
|
|
return len(s) >= len(p) && s[:len(p)] == p
|
|
}
|
|
|
|
func TestValidateSpecIncludesAllOnly(t *testing.T) {
|
|
// 仅 includes_all 也是合法正向条件。
|
|
spec := validSpec()
|
|
spec.Matcher = MatchSpec{IncludesAll: []string{"任务", "进度"}}
|
|
if err := validateSpec(spec, Options{}.normalized().limits(), nil); err != nil {
|
|
t.Fatalf("includes_all-only spec rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestErrorCodeDuckInterface(t *testing.T) {
|
|
err := newError(InvalidSpec, "msg", "id", "demo.x")
|
|
if err.ErrorCode() != InvalidSpec {
|
|
t.Fatalf("code mismatch")
|
|
}
|
|
fields := err.ErrorFields()
|
|
if fields["id"] != "demo.x" {
|
|
t.Fatalf("fields mismatch: %v", fields)
|
|
}
|
|
if err.Error() == "" {
|
|
t.Fatal("empty error string")
|
|
}
|
|
}
|