Files
Agent-System/internal/intent/recognizer_test.go
T
weichenandCursor 525047b841 feat(intent): depend on maturity vocabulary instead of bizdev
Intent only needed the three official stage tokens. Moving them to a
leaf package lets the recognizer compile without the orchestration root.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 10:29:38 +00:00

739 lines
27 KiB
Go

package intent_test
import (
"context"
"strings"
"testing"
"time"
"chat-agent-system/internal/maturity"
"chat-agent-system/internal/intent"
)
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_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_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)
}
}
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_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)
}
}