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") } }