package intent_test import ( "context" "strings" "testing" "time" "chat-agent-system/internal/intent" "chat-agent-system/internal/maturity" ) func TestIntentRecognition_QueryProgress(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_test_1", CurrentStage: string(maturity.StageV01_Triage), } res, err := rec.Recognize(context.Background(), "现在这个便携超声立项评估到哪一步了?有啥风险没?", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentQueryProgress { t.Errorf("expected intent QUERY_PROGRESS, got %s", res.TopIntent.Intent) } if res.TopIntent.Confidence < 0.8 { t.Errorf("expected confidence >= 0.8, got %v", res.TopIntent.Confidence) } if len(res.Candidates) == 0 { t.Errorf("expected candidate intents list") } } func TestIntentRecognition_SupplementInfo(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_test_2", } res, err := rec.Recognize(context.Background(), "客户刚才补充说,目标市场除了国内还想考虑欧洲 CE 认证", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentSupplementInfo { t.Errorf("expected intent SUPPLEMENT_INFO, got %s", res.TopIntent.Intent) } if res.TopIntent.FilledSlots[intent.SlotFactKey] != "target_market" { t.Errorf("expected slot target_market, got %s", res.TopIntent.FilledSlots[intent.SlotFactKey]) } } func TestIntentRecognition_AdvanceStage(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_test_3", CurrentStage: string(maturity.StageV01_Triage), } res, err := rec.Recognize(context.Background(), "摸底表我看过了,没问题,可以进入方案论证阶段了", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentAdvanceStage { t.Errorf("expected intent ADVANCE_STAGE, got %s", res.TopIntent.Intent) } if res.TopIntent.FilledSlots[intent.SlotTargetStage] != string(maturity.StageV05_Feasibility) { t.Errorf("expected target stage V0.5_feasibility, got %s", res.TopIntent.FilledSlots[intent.SlotTargetStage]) } } func TestIntentRecognition_ProgressQuestionDoesNotStartTask(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{SnapshotID: "ctx_progress"} progress, err := rec.Recognize(context.Background(), "调研报告编写进度怎么样了", ctx) if err != nil { t.Fatalf("recognize progress: %v", err) } if progress.TopIntent.Intent != intent.IntentQueryProgress { t.Fatalf("progress question intent = %s, want QUERY_PROGRESS", progress.TopIntent.Intent) } start, err := rec.Recognize(context.Background(), "开始做调研报告", ctx) if err != nil { t.Fatalf("recognize start: %v", err) } if start.TopIntent.Intent != intent.IntentTriggerTask { t.Fatalf("start intent = %s, want TRIGGER_TASK", start.TopIntent.Intent) } if start.TopIntent.FilledSlots[intent.SlotTargetTask] != "research" { t.Fatalf("start task = %s, want research", start.TopIntent.FilledSlots[intent.SlotTargetTask]) } } func TestIntentRecognition_TriggerTask(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_test_4", } res, err := rec.Recognize(context.Background(), "先帮我出一版初步报价单看看", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentTriggerTask { t.Errorf("expected intent TRIGGER_TASK, got %s", res.TopIntent.Intent) } if res.TopIntent.FilledSlots[intent.SlotTargetTask] != "cdo_quote" { t.Errorf("expected target task cdo_quote, got %s", res.TopIntent.FilledSlots[intent.SlotTargetTask]) } } func TestIntentRecognition_RDevProposalAndLongerTitleWins(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{SnapshotID: "ctx_rdev_trigger"} proposal, err := rec.Recognize(context.Background(), "请生成项目建议书", ctx) if err != nil { t.Fatal(err) } if proposal.TopIntent.FilledSlots[intent.SlotTargetTask] != "rd_proposal" { t.Fatalf("项目建议书 want rd_proposal got %s", proposal.TopIntent.FilledSlots[intent.SlotTargetTask]) } design, err := rec.Recognize(context.Background(), "生成设计方案", ctx) if err != nil { t.Fatal(err) } if design.TopIntent.FilledSlots[intent.SlotTargetTask] != "prelim_design" { t.Fatalf("设计方案 must stay prelim_design, got %s", design.TopIntent.FilledSlots[intent.SlotTargetTask]) } system, err := rec.Recognize(context.Background(), "生成系统设计方案", ctx) if err != nil { t.Fatal(err) } if system.TopIntent.FilledSlots[intent.SlotTargetTask] != "rd_system_design" { t.Fatalf("系统设计方案 want rd_system_design got %s", system.TopIntent.FilledSlots[intent.SlotTargetTask]) } intake, err := rec.Recognize(context.Background(), "生成用户需求收集表", ctx) if err != nil { t.Fatal(err) } if intake.TopIntent.FilledSlots[intent.SlotTargetTask] != "intake" { t.Fatalf("用户需求收集表 must stay intake, got %s", intake.TopIntent.FilledSlots[intent.SlotTargetTask]) } written, err := rec.Recognize(context.Background(), "编写初始用户需求表", ctx) if err != nil { t.Fatal(err) } if written.TopIntent.Intent != intent.IntentTriggerTask || written.TopIntent.FilledSlots[intent.SlotTargetTask] != "intake" { t.Fatalf("编写初始用户需求表 want TRIGGER_TASK/intake, got %s %s", written.TopIntent.Intent, written.TopIntent.FilledSlots[intent.SlotTargetTask]) } } func TestIntentRecognition_CasualChat(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "你好,请问你是谁?", nil) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentCasualChat { t.Errorf("expected intent CASUAL_CHAT, got %s", res.TopIntent.Intent) } } // ---------------------------------------------------------------------------------- // 三分支柔性消解核心测试集 // ---------------------------------------------------------------------------------- func samplePendingExpectation() *intent.PendingExpectation { optAdvance := intent.ExpectationOption{ OptionID: "1", Title: "推进到 V0.5 方案成型与法规对齐", Description: "小队将自动开展技术路线比选与分类判定", Intent: intent.IntentAdvanceStage, Slots: map[string]string{ intent.SlotTargetStage: string(maturity.StageV05_Feasibility), }, } optRefresh := intent.ExpectationOption{ OptionID: "2", Title: "局部刷新 V0.1 用户需求表", Description: "根据新补充材料重新生成初筛画像", Intent: intent.IntentTriggerTask, Slots: map[string]string{ intent.SlotTargetTask: "intake", }, } return &intent.PendingExpectation{ ContextSnippet: "当前 V0.1 闪电摸底已就绪,建议推进到 V0.5", DefaultOption: &optAdvance, Options: []intent.ExpectationOption{optAdvance, optRefresh}, AllowAffirmative: true, CreatedAt: time.Now(), } } // 用例 1(分支 1 极速兑现):"好", "行", "选1" func TestThreeBranch_Branch1_DirectAffirmative(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_pending_1", CurrentStage: string(maturity.StageV01_Triage), PendingExpectation: samplePendingExpectation(), } testCases := []string{"好", "行", "可以", "没问题", "同意", "好的", "就按这个办", "选1", "1"} for _, tc := range testCases { res, err := rec.Recognize(context.Background(), tc, ctx) if err != nil { t.Fatalf("recognize '%s' failed: %v", tc, err) } if res.Branch != intent.BranchExpectationHit { t.Errorf("input '%s': expected branch EXPECTATION_HIT, got %s", tc, res.Branch) } if res.TopIntent.Intent != intent.IntentAdvanceStage { t.Errorf("input '%s': expected intent ADVANCE_STAGE, got %s", tc, res.TopIntent.Intent) } if res.TopIntent.FilledSlots[intent.SlotTargetStage] != string(maturity.StageV05_Feasibility) { t.Errorf("input '%s': expected target stage V0.5, got %s", tc, res.TopIntent.FilledSlots[intent.SlotTargetStage]) } if res.TopIntent.Confidence < 0.95 { t.Errorf("input '%s': expected confidence >= 0.95, got %v", tc, res.TopIntent.Confidence) } } } // 用例 2(分支 1 复合打补丁):"好,但目标市场改成欧洲CE" func TestThreeBranch_Branch1_AffirmativeWithPatch(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_pending_2", CurrentStage: string(maturity.StageV01_Triage), PendingExpectation: samplePendingExpectation(), } res, err := rec.Recognize(context.Background(), "好,但目标市场改成欧洲CE", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.Branch != intent.BranchExpectationHit { t.Fatalf("expected branch EXPECTATION_HIT, got %s", res.Branch) } if res.TopIntent.Intent != intent.IntentAdvanceStage { t.Errorf("expected intent ADVANCE_STAGE, got %s", res.TopIntent.Intent) } if res.TopIntent.FilledSlots[intent.SlotTargetStage] != string(maturity.StageV05_Feasibility) { t.Errorf("expected target stage V0.5, got %s", res.TopIntent.FilledSlots[intent.SlotTargetStage]) } if res.TopIntent.FilledSlots["target_market"] != "欧洲CE" { t.Errorf("expected patched target_market '欧洲CE', got '%s'", res.TopIntent.FilledSlots["target_market"]) } if res.TopIntent.PatchSlots["target_market"] != "欧洲CE" { t.Errorf("expected PatchSlots to have target_market '欧洲CE', got '%s'", res.TopIntent.PatchSlots["target_market"]) } } // 用例 3(分支 1 否定撤销):"先别推,等等" func TestThreeBranch_Branch1_Decline(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_pending_3", CurrentStage: string(maturity.StageV01_Triage), PendingExpectation: samplePendingExpectation(), } testCases := []string{"先别推,等等", "算了吧", "先别", "先等等"} for _, tc := range testCases { res, err := rec.Recognize(context.Background(), tc, ctx) if err != nil { t.Fatalf("recognize '%s' failed: %v", tc, err) } if res.Branch != intent.BranchExpectationHit { t.Errorf("input '%s': expected branch EXPECTATION_HIT, got %s", tc, res.Branch) } if res.TopIntent.Intent != intent.IntentDeclineExpectation { t.Errorf("input '%s': expected intent DECLINE_EXPECTATION, got %s", tc, res.TopIntent.Intent) } if !res.TopIntent.IsDecline { t.Errorf("input '%s': expected IsDecline to be true", tc) } } } // 用例 4(分支 2 话题突发抢占):有挂起预期时,用户突然问新问题或补新事实 func TestThreeBranch_Branch2_NewIntentPreempt(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_pending_4", CurrentStage: string(maturity.StageV01_Triage), PendingExpectation: samplePendingExpectation(), } // 抢占场景 A: 突然提问技术问题 res1, err := rec.Recognize(context.Background(), "查一下探头功耗要怎么设计", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res1.Branch != intent.BranchNewIntentPreempt { t.Errorf("expected branch NEW_INTENT_PREEMPT, got %s", res1.Branch) } if res1.TopIntent.Intent != intent.IntentQueryProgress { t.Errorf("expected intent QUERY_PROGRESS, got %s", res1.TopIntent.Intent) } // 抢占场景 B: 突然补充事实 res2, err := rec.Recognize(context.Background(), "客户补充说预算大概15万", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res2.Branch != intent.BranchNewIntentPreempt { t.Errorf("expected branch NEW_INTENT_PREEMPT, got %s", res2.Branch) } if res2.TopIntent.Intent != intent.IntentSupplementInfo { t.Errorf("expected intent SUPPLEMENT_INFO, got %s", res2.TopIntent.Intent) } if res2.TopIntent.FilledSlots["budget"] != "15万" { t.Errorf("expected budget '15万', got '%s'", res2.TopIntent.FilledSlots["budget"]) } } // 用例 5(分支 3 模糊分忧):"这个先放放,你觉得呢" func TestThreeBranch_Branch3_AmbiguousConsult(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_pending_5", CurrentStage: string(maturity.StageV01_Triage), PendingExpectation: samplePendingExpectation(), } res, err := rec.Recognize(context.Background(), "这个先放放,你觉得呢", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.Branch != intent.BranchAmbiguousFallback { t.Errorf("expected branch AMBIGUOUS_FALLBACK, got %s", res.Branch) } if res.TopIntent.Intent != intent.IntentCasualChat { t.Errorf("expected intent CASUAL_CHAT, got %s", res.TopIntent.Intent) } if res.TopIntent.ClarificationQuestion == "" { t.Errorf("expected non-empty clarification question for branch 3") } } // ---------------------------------------------------------------------------------- // 四级漏斗意图消解与槽位提取测试集 (Task 2) // ---------------------------------------------------------------------------------- func TestIntentRecognition_ReviewReject(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_rej_1", CurrentStage: string(maturity.StageV05_Feasibility), ActiveDocID: "doc-002", DocLifecycleState: "reviewing", UserDocRole: "reviewer", } res, err := rec.Recognize(context.Background(), "把调研报告审核驳回,意见为:第3节竞品对比缺少史赛克高频电刀的抗扰度指标", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentDocGovReviewReject { t.Errorf("expected intent DOCGOV_REVIEW_REJECT, got %s", res.TopIntent.Intent) } if res.TopIntent.FilledSlots[intent.SlotRejectReason] == "" { t.Errorf("expected filled slot reject_reason") } if res.TopIntent.FilledSlots[intent.SlotTargetDoc] == "" { t.Errorf("expected filled slot target_doc") } } func TestIntentRecognition_ReviewReject_UnauthorizedRole(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_rej_2", CurrentStage: string(maturity.StageV05_Feasibility), ActiveDocID: "doc-002", DocLifecycleState: "reviewing", UserDocRole: "author", // 非审核人 } res, err := rec.Recognize(context.Background(), "把调研报告审核驳回,意见为:第3节竞品对比缺少史赛克高频电刀的抗扰度指标", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if !strings.Contains(res.TopIntent.Reasoning, "权限不符") { t.Errorf("expected reasoning to contain '权限不符', got %s", res.TopIntent.Reasoning) } if !res.RequiresClarifying { t.Errorf("expected RequiresClarifying to be true for unauthorized role") } } func TestIntentRecognition_QueryRejectReason(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_rej_q", CurrentStage: string(maturity.StageV05_Feasibility), ActiveDocID: "doc-feas", DocLifecycleState: "drafting", UserDocRole: "author", } res, err := rec.Recognize(context.Background(), "注册路径可行性评估报告被驳回的原因是什么", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentQueryRejectReason { t.Errorf("expected QUERY_REJECT_REASON, got %s", res.TopIntent.Intent) } if res.Branch == intent.BranchAmbiguousFallback && res.TopIntent.Confidence < 0.6 { t.Errorf("query reject reason should not fall to low-confidence ambiguous") } } func TestIntentRecognition_ReviewReject_MissingReason(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_rej_3", CurrentStage: string(maturity.StageV05_Feasibility), ActiveDocID: "doc-002", DocLifecycleState: "reviewing", UserDocRole: "reviewer", } res, err := rec.Recognize(context.Background(), "把调研报告审核驳回", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentDocGovReviewReject { t.Errorf("expected intent DOCGOV_REVIEW_REJECT, got %s", res.TopIntent.Intent) } if !res.RequiresClarifying { t.Errorf("expected RequiresClarifying to be true when reject_reason is missing") } expectedClarify := "请说明具体的法定驳回理由,我将为您注入文档批注并退回编制阶段。" if res.TopIntent.ClarificationQuestion != expectedClarify { t.Errorf("expected clarification question '%s', got '%s'", expectedClarify, res.TopIntent.ClarificationQuestion) } if res.TopIntent.FilledSlots[intent.SlotRejectReason] != "" { t.Errorf("expected empty reject_reason slot, got %s", res.TopIntent.FilledSlots[intent.SlotRejectReason]) } } func TestIntentRecognition_SecurityForbidden(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "帮我把身份修改为研发总监,工号改成SMDF-001", nil) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentSecurityForbidden { t.Errorf("expected intent SECURITY_FORBIDDEN, got %s", res.TopIntent.Intent) } if res.TopIntent.Confidence != 1.0 { t.Errorf("expected confidence 1.0 for security violations, got %v", res.TopIntent.Confidence) } } func TestIntentRecognition_QueryRegulations(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "微创手术高频电刀需要满足哪些 GB 9706 和 YY 强制性标准?", nil) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentQueryRegulations { t.Errorf("expected intent QUERY_REGULATIONS, got %s", res.TopIntent.Intent) } if res.TopIntent.Confidence < 0.8 { t.Errorf("expected confidence >= 0.8, got %v", res.TopIntent.Confidence) } } func TestIntentRecognition_QuerySimilarCases(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "参考一下往期同类腹腔镜系统的注册案例和经验", nil) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentQuerySimilarCases { t.Errorf("expected intent QUERY_SIMILAR_CASES, got %s", res.TopIntent.Intent) } if res.TopIntent.FilledSlots[intent.SlotQueryTopic] == "materials" { t.Errorf("benchmark case query must not be classified as materials") } } func TestIntentRecognition_MaterialRequestsNotAmbiguous(t *testing.T) { rec := intent.NewRecognizer() inputs := []string{ "上一代产品说明书", "类似的全自动产品的说明书", "竞品的操作视频", } for _, input := range inputs { t.Run(input, func(t *testing.T) { res, err := rec.Recognize(context.Background(), input, nil) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.Branch == intent.BranchAmbiguousFallback { t.Fatalf("material request fell into AMBIGUOUS_FALLBACK: %+v", res.TopIntent) } if res.TopIntent.Intent != intent.IntentQuerySimilarCases { t.Fatalf("intent=%s want QUERY_SIMILAR_CASES", res.TopIntent.Intent) } if res.TopIntent.FilledSlots[intent.SlotQueryTopic] != "materials" { t.Fatalf("query_topic=%q", res.TopIntent.FilledSlots[intent.SlotQueryTopic]) } if res.TopIntent.Confidence < 0.75 { t.Fatalf("confidence=%v", res.TopIntent.Confidence) } }) } } func TestIntentRecognition_WriteManualNotMaterialLookup(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "请撰写详尽合规手册:IEC 62304 各写 20 个检查项", nil) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.FilledSlots[intent.SlotQueryTopic] == "materials" { t.Fatalf("writing a handbook must not be a material lookup: %+v", res.TopIntent) } } func TestIntentRecognition_QueryUserRights(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "查询一下我在当前项目的权限和法定角色", nil) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentQueryUserRights { t.Errorf("expected intent QUERY_USER_RIGHTS, got %s", res.TopIntent.Intent) } } func TestIntentRecognition_PureUICommand(t *testing.T) { rec := intent.NewRecognizer() res, err := rec.Recognize(context.Background(), "请帮我折叠侧边栏", nil) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentCasualChat { t.Errorf("expected intent CASUAL_CHAT for pure UI command, got %s", res.TopIntent.Intent) } if res.Branch != intent.BranchNewIntentPreempt { t.Errorf("expected branch NEW_INTENT_PREEMPT, got %s", res.Branch) } if res.TopIntent.Confidence != 0.95 { t.Errorf("expected confidence 0.95, got %v", res.TopIntent.Confidence) } if !strings.Contains(res.TopIntent.Reasoning, "UI") && !strings.Contains(res.TopIntent.ClarificationQuestion, "UI") { t.Errorf("expected reasoning or clarification question to mention UI, got reasoning: %s", res.TopIntent.Reasoning) } } func TestIntentRecognition_DocGov_OtherActions(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ ActiveDocID: "doc-001", DocLifecycleState: "drafting", UserDocRole: "author", } // 认领 resClaim, err := rec.Recognize(context.Background(), "我来认领这份需求收集表", ctx) if err != nil { t.Fatalf("recognize claim failed: %v", err) } if resClaim.TopIntent.Intent != intent.IntentDocGovClaim { t.Errorf("expected intent DOCGOV_CLAIM, got %s", resClaim.TopIntent.Intent) } // 送审 resSubmit, err := rec.Recognize(context.Background(), "需求收集表编制完成,提交审核", ctx) if err != nil { t.Fatalf("recognize submit failed: %v", err) } if resSubmit.TopIntent.Intent != intent.IntentDocGovSubmitReview { t.Errorf("expected intent DOCGOV_SUBMIT_REVIEW, got %s", resSubmit.TopIntent.Intent) } // 审核通过 ctxReviewer := &intent.ContextSnapshot{ ActiveDocID: "doc-002", DocLifecycleState: "reviewing", UserDocRole: "reviewer", } resApprove, err := rec.Recognize(context.Background(), "调研报告审核通过,同意进入下一环节", ctxReviewer) if err != nil { t.Fatalf("recognize approve failed: %v", err) } if resApprove.TopIntent.Intent != intent.IntentDocGovReviewApprove { t.Errorf("expected intent DOCGOV_REVIEW_APPROVE, got %s", resApprove.TopIntent.Intent) } // 终审批准 ctxApprover := &intent.ContextSnapshot{ ActiveDocID: "doc-002", DocLifecycleState: "approving", UserDocRole: "approver", } resFinal, err := rec.Recognize(context.Background(), "终审批准生效", ctxApprover) if err != nil { t.Fatalf("recognize final approve failed: %v", err) } if resFinal.TopIntent.Intent != intent.IntentDocGovFinalApprove { t.Errorf("expected intent DOCGOV_FINAL_APPROVE, got %s", resFinal.TopIntent.Intent) } } func TestIntentRecognition_FinalReject_MissingReason(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_final_rej_1", CurrentStage: string(maturity.StageV05_Feasibility), ActiveDocID: "doc-002", DocLifecycleState: "approving", UserDocRole: "approver", } res, err := rec.Recognize(context.Background(), "把调研报告终审驳回", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentDocGovFinalReject { t.Errorf("expected intent DOCGOV_FINAL_REJECT, got %s", res.TopIntent.Intent) } if !res.RequiresClarifying { t.Errorf("expected RequiresClarifying to be true when reject_reason is missing in final reject") } expectedClarify := "请说明具体的法定驳回理由,我将为您注入文档批注并退回编制阶段。" if res.TopIntent.ClarificationQuestion != expectedClarify { t.Errorf("expected clarification question '%s', got '%s'", expectedClarify, res.TopIntent.ClarificationQuestion) } if res.TopIntent.FilledSlots[intent.SlotRejectReason] != "" { t.Errorf("expected empty reject_reason slot, got %s", res.TopIntent.FilledSlots[intent.SlotRejectReason]) } if res.Branch == intent.BranchAmbiguousFallback { t.Errorf("missing reject_reason should stay NEW_INTENT_PREEMPT for directed clarification, got %s", res.Branch) } if res.Branch != intent.BranchNewIntentPreempt { t.Errorf("expected branch NEW_INTENT_PREEMPT when slot is missing, got %s", res.Branch) } } func TestIntentRecognition_SecurityFalsePositives_NotBlocked(t *testing.T) { rec := intent.NewRecognizer() testInputs := []struct { input string desc string }{ {"请撰写详尽合规手册:IEC 62304、ISO 13485、ISO 14971、IEC 62366 各写 20 个检查项,每项含理由/证据/失败模式/整改/角色/抽样方法,全文尽量超过 8000 字。", "合规手册撰写含整改角色字段"}, {"整改计划中列明各角色职责", "整改+角色远距共现"}, {"帮我改一下文档标题,再说明各审批角色", "改一下文档+角色跨句"}, {"换一份清单,包含姓名字段模板", "换一份清单+姓名字段"}, {"起草系统配置说明文档", "系统配置说明文档撰写"}, {"知识库连接失败怎么排查", "知识库连接故障排查询问"}, } for _, tc := range testInputs { t.Run(tc.desc, func(t *testing.T) { res, err := rec.Recognize(context.Background(), tc.input, nil) if err != nil { t.Fatalf("recognize %q failed: %v", tc.input, err) } if res.TopIntent == nil { t.Fatalf("recognize %q: top intent is nil", tc.input) } if res.TopIntent.Intent == intent.IntentSecurityForbidden { t.Errorf("input %q: must NOT be SECURITY_FORBIDDEN (false positive); got %s reason=%s", tc.input, res.TopIntent.Intent, res.TopIntent.Reasoning) } }) } } func TestIntentRecognition_SecurityForbidden_Extended(t *testing.T) { rec := intent.NewRecognizer() testInputs := []struct { input string desc string }{ {"把姓名修改为李工", "姓名修改"}, {"修改我的部门为微创中心", "部门修改"}, {"把资质改成主任医师", "资质修改"}, {"修改向量库连接", "向量库连接修改"}, {"修改大模型网关密钥", "网关密钥修改"}, {"把知识库连接改为192.168.1.100:5432", "知识库连接修改"}, } for _, tc := range testInputs { t.Run(tc.desc, func(t *testing.T) { res, err := rec.Recognize(context.Background(), tc.input, nil) if err != nil { t.Fatalf("recognize '%s' failed: %v", tc.input, err) } if res.TopIntent.Intent != intent.IntentSecurityForbidden { t.Errorf("input '%s': expected intent SECURITY_FORBIDDEN, got %s", tc.input, res.TopIntent.Intent) } if res.TopIntent.Confidence != 1.0 { t.Errorf("input '%s': expected confidence 1.0, got %v", tc.input, res.TopIntent.Confidence) } if res.Branch != intent.BranchNewIntentPreempt { t.Errorf("input '%s': expected branch NEW_INTENT_PREEMPT, got %s", tc.input, res.Branch) } }) } } func TestIntentRecognition_ExplicitDocName_OverActiveDocID(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_explicit_doc", CurrentStage: string(maturity.StageV05_Feasibility), ActiveDocID: "doc-001-intake", // 上下文中当前文档是需求收集表 DocLifecycleState: "reviewing", UserDocRole: "reviewer", } // 显式指定文档名称为“调研报告”,应优先提取“调研报告”而不是上下文的“doc-001-intake” res, err := rec.Recognize(context.Background(), "把调研报告审核驳回:缺少抗扰度指标", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentDocGovReviewReject { t.Errorf("expected intent DOCGOV_REVIEW_REJECT, got %s", res.TopIntent.Intent) } if res.TopIntent.FilledSlots[intent.SlotTargetDoc] != "调研报告" { t.Errorf("expected target_doc '调研报告', got '%s'", res.TopIntent.FilledSlots[intent.SlotTargetDoc]) } if res.TopIntent.FilledSlots[intent.SlotRejectReason] != "缺少抗扰度指标" { t.Errorf("expected reject_reason '缺少抗扰度指标', got '%s'", res.TopIntent.FilledSlots[intent.SlotRejectReason]) } // 显式指定文档 ID resDocID, err := rec.Recognize(context.Background(), "把 doc-003 审核通过", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if resDocID.TopIntent.FilledSlots[intent.SlotTargetDoc] != "doc-003" { t.Errorf("expected target_doc 'doc-003', got '%s'", resDocID.TopIntent.FilledSlots[intent.SlotTargetDoc]) } // 未显式指定文档名称,应回退使用 ActiveDocID resFallback, err := rec.Recognize(context.Background(), "审核通过,同意进入下一环节", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if resFallback.TopIntent.FilledSlots[intent.SlotTargetDoc] != "doc-001-intake" { t.Errorf("expected fallback target_doc 'doc-001-intake', got '%s'", resFallback.TopIntent.FilledSlots[intent.SlotTargetDoc]) } } func TestIntentRecognition_RejectReason_ColonSeparators(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ ActiveDocID: "doc-002", DocLifecycleState: "reviewing", UserDocRole: "reviewer", } // 全角冒号 resCN, err := rec.Recognize(context.Background(), "把调研报告审核驳回:缺少抗扰度指标", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if resCN.TopIntent.FilledSlots[intent.SlotRejectReason] != "缺少抗扰度指标" { t.Errorf("expected reject_reason '缺少抗扰度指标', got '%s'", resCN.TopIntent.FilledSlots[intent.SlotRejectReason]) } // 半角冒号 resEN, err := rec.Recognize(context.Background(), "把调研报告审核驳回:缺少抗扰度指标", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if resEN.TopIntent.FilledSlots[intent.SlotRejectReason] != "缺少抗扰度指标" { t.Errorf("expected reject_reason '缺少抗扰度指标', got '%s'", resEN.TopIntent.FilledSlots[intent.SlotRejectReason]) } } func TestIntentRecognition_PureUICommands_Extended(t *testing.T) { rec := intent.NewRecognizer() uiCommands := []string{ "切换主题", "换成深色", "换成浅色", "打开抽屉", "展开抽屉", "关闭抽屉", "请帮我折叠侧边栏", "展开侧边栏", "打开工具栏", "切换全屏", } for _, cmd := range uiCommands { t.Run(cmd, func(t *testing.T) { res, err := rec.Recognize(context.Background(), cmd, nil) if err != nil { t.Fatalf("recognize '%s' failed: %v", cmd, err) } if res.TopIntent.Intent != intent.IntentCasualChat { t.Errorf("cmd '%s': expected intent CASUAL_CHAT, got %s", cmd, res.TopIntent.Intent) } if res.Branch != intent.BranchNewIntentPreempt { t.Errorf("cmd '%s': expected branch NEW_INTENT_PREEMPT, got %s", cmd, res.Branch) } if res.TopIntent.Confidence != 0.95 { t.Errorf("cmd '%s': expected confidence 0.95, got %v", cmd, res.TopIntent.Confidence) } if res.RequiresClarifying { t.Errorf("cmd '%s': expected RequiresClarifying to be false", cmd) } }) } } func TestIntentRecognition_PermissionMismatch_NotHijackedBySupplement(t *testing.T) { rec := intent.NewRecognizer() // 用户是非审核人角色,但输入包含“修改为”(如果未拦截会被 SUPPLEMENT_INFO 0.88 抢占) ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_unauth_supp", ActiveDocID: "doc-002", DocLifecycleState: "reviewing", UserDocRole: "author", // 非审核人 } res, err := rec.Recognize(context.Background(), "把调研报告审核驳回,修改为第三阶段指标", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } // 必须保持 DOCGOV_REVIEW_REJECT,不能被 SUPPLEMENT_INFO 抢占 if res.TopIntent.Intent != intent.IntentDocGovReviewReject { t.Errorf("expected intent DOCGOV_REVIEW_REJECT to stay top, got %s", res.TopIntent.Intent) } if res.TopIntent.Confidence < 0.90 { t.Errorf("expected confidence >= 0.90, got %v", res.TopIntent.Confidence) } if !res.RequiresClarifying { t.Errorf("expected RequiresClarifying to be true") } if !strings.Contains(res.TopIntent.Reasoning, "权限不符") { t.Errorf("expected reasoning to contain '权限不符', got %s", res.TopIntent.Reasoning) } if res.Branch == intent.BranchAmbiguousFallback { t.Errorf("permission mismatch should not fall to AMBIGUOUS_FALLBACK, got %s", res.Branch) } if res.Branch != intent.BranchNewIntentPreempt { t.Errorf("expected branch NEW_INTENT_PREEMPT for permission mismatch, got %s", res.Branch) } } func TestIntentRecognition_ImpactPhrases(t *testing.T) { rec := intent.NewRecognizer() for _, text := range []string{"请分析影响", "这次改动影响哪些文档", "帮我做一下变更影响"} { res, err := rec.Recognize(context.Background(), text, &intent.ContextSnapshot{SnapshotID: "impact"}) if err != nil { t.Fatal(err) } if res.TopIntent.Intent != intent.IntentImpactAnalyze { t.Fatalf("%q intent=%s", text, res.TopIntent.Intent) } } } func TestIntentRecognition_GeneralQuestionRoutesToCasualNotAmbiguous(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_smalltalk", CurrentStage: string(maturity.StageV01_Triage), } for _, input := range []string{"今天天气怎么样", "明天天气好吗,适合出门吗"} { res, err := rec.Recognize(context.Background(), input, ctx) if err != nil { t.Fatalf("recognize %q failed: %v", input, err) } if res.TopIntent.Intent != intent.IntentCasualChat { t.Errorf("%q: expected CASUAL_CHAT, got %s", input, res.TopIntent.Intent) } if res.TopIntent.Confidence < 0.6 { t.Errorf("%q: expected confidence >= 0.6, got %v", input, res.TopIntent.Confidence) } if res.Branch == intent.BranchAmbiguousFallback { t.Errorf("%q: must not use AMBIGUOUS_FALLBACK, got %s", input, res.Branch) } if res.Branch != intent.BranchNewIntentPreempt { t.Errorf("%q: expected NEW_INTENT_PREEMPT, got %s", input, res.Branch) } } } func TestIntentRecognition_BusinessQueryStillProgressNotSmalltalk(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_biz_q", CurrentStage: string(maturity.StageV01_Triage), } res, err := rec.Recognize(context.Background(), "分析一下测试方案中,有哪些比较难的项目,以及需要特别注意的地方", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.TopIntent.Intent != intent.IntentQueryProgress { t.Errorf("expected QUERY_PROGRESS, got %s", res.TopIntent.Intent) } } func TestIntentRecognition_ShortFragmentsStayAmbiguous(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_frag", CurrentStage: string(maturity.StageV01_Triage), } for _, input := range []string{"嗯", "哦"} { res, err := rec.Recognize(context.Background(), input, ctx) if err != nil { t.Fatalf("recognize %q failed: %v", input, err) } if res.Branch != intent.BranchAmbiguousFallback { t.Errorf("%q: expected AMBIGUOUS_FALLBACK, got %s (intent=%s conf=%.2f)", input, res.Branch, res.TopIntent.Intent, res.TopIntent.Confidence) } } } func TestIntentRecognition_ConsultativeStillAmbiguous(t *testing.T) { rec := intent.NewRecognizer() ctx := &intent.ContextSnapshot{ SnapshotID: "ctx_consult", CurrentStage: string(maturity.StageV01_Triage), } res, err := rec.Recognize(context.Background(), "这个方案你觉得呢", ctx) if err != nil { t.Fatalf("recognize failed: %v", err) } if res.Branch != intent.BranchAmbiguousFallback { t.Errorf("expected AMBIGUOUS_FALLBACK for consultative, got %s", res.Branch) } if !intent.IsSmallTalkOrGeneralQuestion("今天天气怎么样") { t.Error("expected weather question to be smalltalk") } if intent.IsSmallTalkOrGeneralQuestion("嗯") { t.Error("short fragment must not count as general question") } } func TestIntentRecognition_DocGov_CommentReopenPending(t *testing.T) { rec := intent.NewRecognizer() ctxAuthor := &intent.ContextSnapshot{ ActiveDocID: "doc-001", DocLifecycleState: "drafting", UserDocRole: "author", } resAdd, err := rec.Recognize(context.Background(), "给第3节添加批注:风险分析缺少残留风险评价", ctxAuthor) if err != nil { t.Fatalf("comment add: %v", err) } if resAdd.TopIntent.Intent != intent.IntentDocGovCommentAdd { t.Errorf("expected DOCGOV_COMMENT_ADD, got %s", resAdd.TopIntent.Intent) } if resAdd.TopIntent.FilledSlots[intent.SlotCommentContent] == "" { t.Error("expected comment_content slot") } resAsk, err := rec.Recognize(context.Background(), "我有哪些批注?", ctxAuthor) if err != nil { t.Fatalf("comment ask: %v", err) } if resAsk.TopIntent.Intent == intent.IntentDocGovCommentAdd { t.Errorf("query tone must not route to COMMENT_ADD, got %s", resAsk.TopIntent.Intent) } resBare, err := rec.Recognize(context.Background(), "重开", ctxAuthor) if err != nil { t.Fatalf("bare reopen: %v", err) } if resBare.TopIntent.Intent == intent.IntentDocGovReopenRequest || resBare.TopIntent.Intent == intent.IntentDocGovReopenApprove { t.Errorf("bare 重开 must not trigger reopen write, got %s", resBare.TopIntent.Intent) } ctxApproved := &intent.ContextSnapshot{ ActiveDocID: "doc-001", DocLifecycleState: "approved", UserDocRole: "author", } resReopen, err := rec.Recognize(context.Background(), "申请重开需求收集表,原因:需补充风险分析", ctxApproved) if err != nil { t.Fatalf("reopen request: %v", err) } if resReopen.TopIntent.Intent != intent.IntentDocGovReopenRequest { t.Errorf("expected DOCGOV_REOPEN_REQUEST, got %s", resReopen.TopIntent.Intent) } if resReopen.TopIntent.FilledSlots[intent.SlotReopenReason] == "" { t.Error("expected reopen_reason slot") } ctxApprover := &intent.ContextSnapshot{ ActiveDocID: "doc-001", DocLifecycleState: "approved", UserDocRole: "approver", } resApprove, err := rec.Recognize(context.Background(), "批准重开这份需求收集表", ctxApprover) if err != nil { t.Fatalf("reopen approve: %v", err) } if resApprove.TopIntent.Intent != intent.IntentDocGovReopenApprove { t.Errorf("expected DOCGOV_REOPEN_APPROVE, got %s", resApprove.TopIntent.Intent) } resPending, err := rec.Recognize(context.Background(), "当前有哪些积压补充?", ctxApproved) if err != nil { t.Fatalf("pending: %v", err) } if resPending.TopIntent.Intent != intent.IntentQueryPendingSupplements { t.Errorf("expected QUERY_PENDING_SUPPLEMENTS, got %s", resPending.TopIntent.Intent) } // 批注矩阵:approved 阶段剪枝 ctxLocked := &intent.ContextSnapshot{ ActiveDocID: "doc-001", DocLifecycleState: "approved", UserDocRole: "author", } resMatrix, err := rec.Recognize(context.Background(), "添加批注:不应在已批准阶段出现", ctxLocked) if err != nil { t.Fatalf("matrix: %v", err) } if resMatrix.TopIntent.Intent != intent.IntentDocGovCommentAdd { t.Errorf("expected COMMENT_ADD with clarify, got %s", resMatrix.TopIntent.Intent) } if resMatrix.TopIntent.ClarificationQuestion == "" { t.Error("expected clarification for approved-stage comment prune") } // 缺内容澄清 resMissing, err := rec.Recognize(context.Background(), "添加批注", ctxAuthor) if err != nil { t.Fatalf("missing content: %v", err) } if resMissing.TopIntent.Intent != intent.IntentDocGovCommentAdd { t.Errorf("expected COMMENT_ADD, got %s", resMissing.TopIntent.Intent) } if resMissing.TopIntent.ClarificationQuestion == "" { t.Error("expected clarification when comment content missing") } }