package intent_test import ( "context" "encoding/json" "os" "path/filepath" "runtime" "strings" "testing" "time" "chat-agent-system/internal/intent" "chat-agent-system/internal/maturity" ) type goldenFile struct { Version string `json:"version"` Cases []goldenCase `json:"cases"` } type goldenCase struct { ID string `json:"id"` Input string `json:"input"` Snapshot *goldenSnapshot `json:"snapshot"` Pending *goldenPending `json:"pending"` ExpectP0 *goldenExpect `json:"expect_phase0"` ExpectP1 *goldenExpect `json:"expect_phase1"` } type goldenSnapshot struct { UserDocRole string `json:"user_doc_role"` DocLifecycleState string `json:"doc_lifecycle_state"` ActiveDocID string `json:"active_doc_id"` CurrentStage string `json:"current_stage"` } type goldenPending struct { ExpectedIntent string `json:"expected_intent"` AllowAffirmative bool `json:"allow_affirmative"` DefaultOption *goldenExpectOption `json:"default_option"` } type goldenExpectOption struct { OptionID string `json:"option_id"` Title string `json:"title"` Intent string `json:"intent"` Slots map[string]string `json:"slots"` } type goldenExpect struct { LegacyIntent string `json:"legacy_intent"` Branch string `json:"branch"` RequiresClarifying *bool `json:"requires_clarifying"` MustNotWrite bool `json:"must_not_write"` DialogueAct string `json:"dialogue_act"` ForbiddenIntents []string `json:"forbidden_intents"` MustNotIntent string `json:"must_not_intent"` MustNotBranch string `json:"must_not_branch"` PreferredIntent string `json:"preferred_intent"` ClarificationContains string `json:"clarification_contains"` ReasoningContains string `json:"reasoning_contains"` } func loadGoldenV1(t *testing.T) goldenFile { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("runtime.Caller failed") } path := filepath.Join(filepath.Dir(file), "testdata", "intent-golden-v1.json") raw, err := os.ReadFile(path) if err != nil { t.Fatalf("read golden file: %v", err) } var gf goldenFile if err := json.Unmarshal(raw, &gf); err != nil { t.Fatalf("parse golden file: %v", err) } if gf.Version != "intent-golden-v1" { t.Fatalf("unexpected golden version: %s", gf.Version) } return gf } func buildContextFromGolden(t *testing.T, snap *goldenSnapshot, pending *goldenPending) *intent.ContextSnapshot { t.Helper() ctx := &intent.ContextSnapshot{} if snap != nil { ctx.UserDocRole = snap.UserDocRole ctx.DocLifecycleState = snap.DocLifecycleState ctx.ActiveDocID = snap.ActiveDocID ctx.CurrentStage = snap.CurrentStage } if pending != nil { pe := &intent.PendingExpectation{ ExpectedIntent: intent.IntentType(pending.ExpectedIntent), AllowAffirmative: pending.AllowAffirmative, CreatedAt: time.Now(), ExpiresAt: time.Now().Add(time.Hour), } if pending.DefaultOption != nil { pe.DefaultOption = &intent.ExpectationOption{ OptionID: pending.DefaultOption.OptionID, Title: pending.DefaultOption.Title, Intent: intent.IntentType(pending.DefaultOption.Intent), Slots: pending.DefaultOption.Slots, } } ctx.PendingExpectation = pe } return ctx } func assertGoldenExpect(t *testing.T, caseID string, exp *goldenExpect, res *intent.IntentRecognitionResult) { t.Helper() if exp == nil { return } if exp.LegacyIntent != "" && string(res.TopIntent.Intent) != exp.LegacyIntent { t.Errorf("[%s] intent: want %s got %s", caseID, exp.LegacyIntent, res.TopIntent.Intent) } if exp.Branch != "" && string(res.Branch) != exp.Branch { t.Errorf("[%s] branch: want %s got %s", caseID, exp.Branch, res.Branch) } if exp.RequiresClarifying != nil && res.RequiresClarifying != *exp.RequiresClarifying { t.Errorf("[%s] requires_clarifying: want %v got %v", caseID, *exp.RequiresClarifying, res.RequiresClarifying) } if exp.MustNotIntent != "" && string(res.TopIntent.Intent) == exp.MustNotIntent { t.Errorf("[%s] must not intent %s", caseID, exp.MustNotIntent) } if exp.MustNotBranch != "" && string(res.Branch) == exp.MustNotBranch { t.Errorf("[%s] must not branch %s", caseID, exp.MustNotBranch) } if exp.PreferredIntent != "" && string(res.TopIntent.Intent) != exp.PreferredIntent { t.Errorf("[%s] preferred intent: want %s got %s", caseID, exp.PreferredIntent, res.TopIntent.Intent) } if exp.ClarificationContains != "" && !strings.Contains(res.TopIntent.ClarificationQuestion, exp.ClarificationContains) { t.Errorf("[%s] clarification should contain %q, got %q", caseID, exp.ClarificationContains, res.TopIntent.ClarificationQuestion) } if exp.ReasoningContains != "" && !strings.Contains(res.TopIntent.Reasoning, exp.ReasoningContains) { t.Errorf("[%s] reasoning should contain %q, got %q", caseID, exp.ReasoningContains, res.TopIntent.Reasoning) } for _, forbidden := range exp.ForbiddenIntents { if string(res.TopIntent.Intent) == forbidden { t.Errorf("[%s] forbidden intent %s", caseID, forbidden) } } if exp.MustNotWrite { writeIntents := map[intent.IntentType]bool{ intent.IntentDocGovClaim: true, intent.IntentDocGovSubmitReview: true, intent.IntentDocGovReviewApprove: true, intent.IntentDocGovReviewReject: true, intent.IntentDocGovFinalApprove: true, intent.IntentDocGovFinalReject: true, intent.IntentDocGovCommentAdd: true, intent.IntentDocGovCommentResolve: true, intent.IntentDocGovReopenRequest: true, intent.IntentDocGovReopenApprove: true, } if writeIntents[res.TopIntent.Intent] { t.Errorf("[%s] must not route to write intent %s", caseID, res.TopIntent.Intent) } } if exp.DialogueAct != "" && res.ShadowDialogueAct != exp.DialogueAct { t.Errorf("[%s] dialogue_act: want %s got %s", caseID, exp.DialogueAct, res.ShadowDialogueAct) } } // TestIntentGoldenV1_Phase1 运行 golden-v1 的 Phase 1 安全门禁期望 func TestIntentGoldenV1_Phase1(t *testing.T) { gf := loadGoldenV1(t) rec := intent.NewRecognizer() for _, c := range gf.Cases { t.Run(c.ID, func(t *testing.T) { ctx := buildContextFromGolden(t, c.Snapshot, c.Pending) res, err := rec.Recognize(context.Background(), c.Input, ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } assertGoldenExpect(t, c.ID, c.ExpectP1, res) }) } } // TestIntentGoldenV1_V2DualWrite 每条识别结果应双写 intent_frame_v2 func TestIntentGoldenV1_V2DualWrite(t *testing.T) { gf := loadGoldenV1(t) rec := intent.NewRecognizer() for _, c := range gf.Cases { t.Run(c.ID, func(t *testing.T) { ctx := buildContextFromGolden(t, c.Snapshot, c.Pending) res, err := rec.Recognize(context.Background(), c.Input, ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.IntentFrameV2 == nil { t.Fatal("expected intent_frame_v2") } if res.IntentFrameV2.LegacyIntent != res.TopIntent.Intent { t.Errorf("v2 legacy mismatch: %s vs %s", res.IntentFrameV2.LegacyIntent, res.TopIntent.Intent) } }) } } // TestIntentGoldenV1_ShadowFields shadow 观测字段应被填充 func TestIntentGoldenV1_ShadowFields(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{CurrentStage: string(maturity.StageV01_Triage)} res, err := rec.Recognize(context.Background(), "现在这个便携超声立项评估到哪一步了?", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.ShadowScore <= 0 { t.Errorf("expected shadow_score > 0, got %v", res.ShadowScore) } if res.ShadowDialogueAct == "" { t.Errorf("expected shadow_dialogue_act to be set") } if res.ShadowDecisionHint == "" { t.Errorf("expected shadow_decision_hint to be set") } }