package coordinator_test import ( "context" "testing" "chat-agent-system/internal/coordinator" "chat-agent-system/internal/intent" "chat-agent-system/internal/maturity" "chat-agent-system/internal/squad" "strings" ) func TestCoordinatorEngine_QueryProgress(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ Project: coordinator.ProjectInfo{ Name: "便携式掌上超声项目", }, LatestAdvisorBrief: &squad.AdvisorBriefing{ MaturityScore: 70, MaturityLevel: "V0.1", MaturityHeadline: "已具备基本产品定义,亟待补充探头频率与目标市场", PreemptiveRisks: []string{"欧盟CE MDR认证周期较长"}, NextBestActions: []string{"推进进入V0.5可行性论证"}, CommunicationKit: "【给客户的建议沟通要点】...", }, } recRes := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentQueryProgress, FilledSlots: map[string]string{ intent.SlotQueryTopic: "risk", }, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("process intent failed: %v", err) } if res.Status != coordinator.StatusAccepted { t.Errorf("expected status ACCEPTED, got %s", res.Status) } if res.AdvisorBrief == nil { t.Errorf("expected advisor brief in result") } } // TestCoordinatorEngine_QueryProgress_NoBrief_SubstantiveReply 复现线上问题: // 新会话无参谋简报(LatestAdvisorBrief == nil)时,用户询问风险/进展/话术 // 必须得到基于基准简报的实质研判,而不是 V0.1 推销模板。 func TestCoordinatorEngine_QueryProgress_NoBrief_SubstantiveReply(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) cases := []struct { name string topic string mustContain []string }{ {"risk", "risk", []string{"潜在风险", "⚠️", "摸底"}}, {"progress", "progress", []string{"进展汇报", "诊断结论"}}, {"talk_script", "talk_script", []string{"沟通话术", "微信沟通草稿"}}, {"all", "all", []string{"核心研判", "关键风险预警"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { userCtx := &coordinator.UserProjectContext{ User: coordinator.UserInfo{Name: "张工", ProjectRole: "composer"}, Project: coordinator.ProjectInfo{ Name: "医疗器械研发项目", CustomerName: "北京水木东方", CurrentWorkflow: "intake", CurrentStage: maturity.StageV01_Triage, }, BlackboardFacts: map[string]string{}, } recRes := &intent.IntentRecognitionResult{ RawInput: "简单分析一下这个项目有什么风险", Branch: intent.BranchNewIntentPreempt, TopIntent: &intent.CandidateIntent{ Intent: intent.IntentQueryProgress, Branch: intent.BranchNewIntentPreempt, Confidence: 0.9, FilledSlots: map[string]string{intent.SlotQueryTopic: tc.topic}, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("[%s] process intent failed: %v", tc.name, err) } if res.Status != coordinator.StatusAccepted { t.Fatalf("[%s] expected status ACCEPTED, got %s", tc.name, res.Status) } for _, want := range tc.mustContain { if !strings.Contains(res.NaturalReply, want) { t.Errorf("[%s] expected reply to contain %q, got: %s", tc.name, want, res.NaturalReply) } } if strings.Contains(res.NaturalReply, "当前处于初始准备期") { t.Errorf("[%s] reply regressed to canned V0.1 sales pitch: %s", tc.name, res.NaturalReply) } if res.AdvisorBrief == nil { t.Errorf("[%s] expected synthesized baseline advisor brief in result", tc.name) } if res.ReplySource != coordinator.ReplySourceTemplate { t.Errorf("[%s] expected reply source template, got %q", tc.name, res.ReplySource) } if res.NextExpectation == nil { t.Errorf("[%s] expected next expectation preserved", tc.name) } }) } } func TestCoordinatorEngine_SupplementInfo(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ Project: coordinator.ProjectInfo{ Name: "便携式掌上超声项目", }, BlackboardFacts: make(map[string]string), } recRes := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentSupplementInfo, FilledSlots: map[string]string{ intent.SlotFactKey: "target_market", intent.SlotFactValue: "国内及欧盟CE", }, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("process intent failed: %v", err) } if res.Status != coordinator.StatusAccepted { t.Errorf("expected status ACCEPTED, got %s", res.Status) } if userCtx.BlackboardFacts["target_market"] != "国内及欧盟CE" { t.Errorf("expected blackboard target_market updated, got %s", userCtx.BlackboardFacts["target_market"]) } } func TestCoordinatorEngine_AdvanceStage(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ Project: coordinator.ProjectInfo{ Name: "超声测试项目", CurrentStage: maturity.StageV01_Triage, }, User: coordinator.UserInfo{ ProjectRole: "composer", }, } recRes := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentAdvanceStage, FilledSlots: map[string]string{ intent.SlotTargetStage: string(maturity.StageV05_Feasibility), }, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("process intent failed: %v", err) } if res.Status != coordinator.StatusAccepted { t.Errorf("expected status ACCEPTED, got %s", res.Status) } if userCtx.Project.CurrentStage != maturity.StageV05_Feasibility { t.Errorf("expected stage V0.5_feasibility, got %s", userCtx.Project.CurrentStage) } } func TestCoordinatorEngine_DeclineExpectation(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ Project: coordinator.ProjectInfo{ Name: "超声测试项目", CurrentStage: maturity.StageV01_Triage, }, PendingExpectation: coordinator.BuildNextExpectation(maturity.StageV01_Triage, "超声测试项目"), } recRes := &intent.IntentRecognitionResult{ Branch: intent.BranchExpectationHit, TopIntent: &intent.CandidateIntent{ Intent: intent.IntentDeclineExpectation, Branch: intent.BranchExpectationHit, IsDecline: true, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("process intent failed: %v", err) } if res.Status != coordinator.StatusAccepted { t.Errorf("expected status ACCEPTED, got %s", res.Status) } if !res.ClearExpectation { t.Errorf("expected ClearExpectation to be true") } if userCtx.PendingExpectation != nil { t.Errorf("expected PendingExpectation to be cleared in userCtx") } } func TestCoordinatorEngine_AmbiguousConsult(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ Project: coordinator.ProjectInfo{ Name: "超声测试项目", CurrentStage: maturity.StageV01_Triage, }, } recRes := &intent.IntentRecognitionResult{ Branch: intent.BranchAmbiguousFallback, TopIntent: &intent.CandidateIntent{ Intent: intent.IntentCasualChat, Branch: intent.BranchAmbiguousFallback, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("process intent failed: %v", err) } if res.Branch != intent.BranchAmbiguousFallback { t.Errorf("expected branch AMBIGUOUS_FALLBACK, got %s", res.Branch) } if res.NextExpectation == nil { t.Errorf("expected NextExpectation to be populated") } } func TestCoordinatorEngine_AdvanceStageWithPatch(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ Project: coordinator.ProjectInfo{ Name: "超声测试项目", CurrentStage: maturity.StageV01_Triage, }, BlackboardFacts: make(map[string]string), } recRes := &intent.IntentRecognitionResult{ Branch: intent.BranchExpectationHit, TopIntent: &intent.CandidateIntent{ Intent: intent.IntentAdvanceStage, Branch: intent.BranchExpectationHit, FilledSlots: map[string]string{ intent.SlotTargetStage: string(maturity.StageV05_Feasibility), "target_market": "欧洲CE", }, PatchSlots: map[string]string{ "target_market": "欧洲CE", }, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("process intent failed: %v", err) } if res.Status != coordinator.StatusAccepted { t.Errorf("expected status ACCEPTED, got %s", res.Status) } if userCtx.BlackboardFacts["target_market"] != "欧洲CE" { t.Errorf("expected target_market '欧洲CE' in blackboard facts, got '%s'", userCtx.BlackboardFacts["target_market"]) } if userCtx.Project.TargetMarket != "欧洲CE" { t.Errorf("expected project TargetMarket '欧洲CE', got '%s'", userCtx.Project.TargetMarket) } } func TestCoordinatorEngine_ReviewRejectWithCard(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ User: coordinator.UserInfo{ ID: "user_102", Name: "李主管", Role: "reviewer", }, Project: coordinator.ProjectInfo{ Name: "微创内窥镜项目", }, } recRes := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentDocGovReviewReject, FilledSlots: map[string]string{ intent.SlotTargetDoc: "doc-002", intent.SlotRejectReason: "第3节缺少史赛克高频电刀的抗扰度指标", }, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("process intent failed: %v", err) } if res.InteractiveCard == nil { t.Fatalf("expected interactive card payload in coordination result") } if res.InteractiveCard.CardType != "CONFIRMATION" { t.Errorf("expected card type CONFIRMATION, got %s", res.InteractiveCard.CardType) } if len(res.InteractiveCard.Actions) < 2 { t.Errorf("expected at least 2 actions (confirm and cancel)") } if !strings.Contains(res.NaturalReply, "驳回") { t.Errorf("expected natural reply to mention 驳回, got %s", res.NaturalReply) } if res.NextExpectation == nil { t.Fatalf("expected NextExpectation in reject result") } if res.NextExpectation.TargetAction != "confirm_reject" { t.Errorf("expected target action confirm_reject, got %s", res.NextExpectation.TargetAction) } if res.NextExpectation.ExpectedIntent != intent.IntentDocGovProposalDecide { t.Errorf("expected expected intent DOCGOV_PROPOSAL_DECIDE, got %s", res.NextExpectation.ExpectedIntent) } } func TestCoordinatorEngine_SecurityForbidden(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ User: coordinator.UserInfo{ ID: "user_101", Name: "张工", Role: "engineer", ProjectRole: "composer", }, Project: coordinator.ProjectInfo{ Name: "医用超声设备项目", }, } recRes := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentSecurityForbidden, FilledSlots: map[string]string{ intent.SlotForbiddenTarget: "identity_or_settings", }, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("process intent failed: %v", err) } if res.Status != coordinator.StatusRejected { t.Errorf("expected status REJECTED, got %s", res.Status) } if !strings.Contains(res.NaturalReply, "合规权限拦截") || !strings.Contains(res.NaturalReply, "ISO 13485") { t.Errorf("expected ISO 13485 compliance warning, got %s", res.NaturalReply) } // Verify user context was not modified if userCtx.User.Name != "张工" || userCtx.User.Role != "engineer" { t.Errorf("expected user context to remain unmodified") } } func TestCoordinatorEngine_QueryUserRights(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ User: coordinator.UserInfo{ ID: "user_102", Name: "李主管", Role: "reviewer", ProjectRole: "reviewer", }, Project: coordinator.ProjectInfo{ Name: "微创内窥镜项目", }, UserDocs: coordinator.UserDocsInfo{ Reviewed: []string{"技术与市场调研报告", "法规与注册论证报告"}, Claimed: []string{"技术与市场调研报告"}, }, } recRes := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentQueryUserRights, FilledSlots: map[string]string{ intent.SlotQueryTopic: "user_rights", }, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("process intent failed: %v", err) } if res.Status != coordinator.StatusAccepted { t.Errorf("expected status ACCEPTED, got %s", res.Status) } if !strings.Contains(res.NaturalReply, "李主管") || !strings.Contains(res.NaturalReply, "审核") { t.Errorf("expected reply to mention user name and review rights, got %s", res.NaturalReply) } } func TestCoordinatorEngine_DocGovActions(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ User: coordinator.UserInfo{ ID: "user_101", Name: "张工", Role: "engineer", ProjectRole: "composer", }, Project: coordinator.ProjectInfo{ Name: "智能内窥镜项目", }, UserDocs: coordinator.UserDocsInfo{ Composed: []string{"doc-001"}, }, } // 1. DocGovClaim recClaim := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentDocGovClaim, FilledSlots: map[string]string{ intent.SlotTargetDoc: "doc-001", }, }, } resClaim, err := engine.ProcessIntent(context.Background(), recClaim, userCtx) if err != nil { t.Fatalf("process claim failed: %v", err) } if resClaim.Status != coordinator.StatusAccepted { t.Errorf("expected claim accepted, got %s", resClaim.Status) } // 2. DocGovSubmitReview recSubmit := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentDocGovSubmitReview, FilledSlots: map[string]string{ intent.SlotTargetDoc: "doc-001", }, }, } resSubmit, err := engine.ProcessIntent(context.Background(), recSubmit, userCtx) if err != nil { t.Fatalf("process submit failed: %v", err) } if resSubmit.Status != coordinator.StatusAccepted { t.Errorf("expected submit accepted, got %s", resSubmit.Status) } // 3. DocGovApprove reviewerCtx := &coordinator.UserProjectContext{ User: coordinator.UserInfo{ ID: "user_102", Name: "李主管", Role: "reviewer", ProjectRole: "reviewer", }, Project: coordinator.ProjectInfo{ Name: "智能内窥镜项目", }, } recApprove := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentDocGovReviewApprove, FilledSlots: map[string]string{ intent.SlotTargetDoc: "doc-001", }, }, } resApprove, err := engine.ProcessIntent(context.Background(), recApprove, reviewerCtx) if err != nil { t.Fatalf("process approve failed: %v", err) } if resApprove.Status != coordinator.StatusAccepted { t.Errorf("expected approve accepted, got %s", resApprove.Status) } // 4. DocGovProposalDecide recProposal := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentDocGovProposalDecide, FilledSlots: map[string]string{ intent.SlotProposalAction: "accept", }, }, } resProposal, err := engine.ProcessIntent(context.Background(), recProposal, userCtx) if err != nil { t.Fatalf("process proposal decide failed: %v", err) } if resProposal.Status != coordinator.StatusAccepted { t.Errorf("expected proposal decide accepted, got %s", resProposal.Status) } // 5. QueryRegulations recReg := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentQueryRegulations, FilledSlots: map[string]string{ intent.SlotQueryTopic: "regulations", }, }, } resReg, err := engine.ProcessIntent(context.Background(), recReg, userCtx) if err != nil { t.Fatalf("process query regulations failed: %v", err) } if resReg.Status != coordinator.StatusAccepted || !strings.Contains(resReg.NaturalReply, "GB 9706") { t.Errorf("expected GB 9706 in regulations reply, got %s", resReg.NaturalReply) } // 6. QuerySimilarCases recCase := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentQuerySimilarCases, FilledSlots: map[string]string{ intent.SlotQueryTopic: "similar_cases", }, }, } resCase, err := engine.ProcessIntent(context.Background(), recCase, userCtx) if err != nil { t.Fatalf("process query similar cases failed: %v", err) } if resCase.Status != coordinator.StatusAccepted { t.Errorf("expected similar cases accepted, got %s", resCase.Status) } } // TestCoordinatorEngine_RejectExpectationAndAffirmativeChain 测试驳回二次确认链条 (挂载 NextExpectation 与多词确认闭环) func TestCoordinatorEngine_RejectExpectationAndAffirmativeChain(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) rec := intent.NewRecognizer() userCtx := &coordinator.UserProjectContext{ User: coordinator.UserInfo{ ID: "user_102", Name: "李审核", Role: "reviewer", ProjectRole: "reviewer", }, Project: coordinator.ProjectInfo{ Name: "心血管介入导管项目", }, } // 1. 发起驳回,产生交互卡片与 NextExpectation recReject := &intent.IntentRecognitionResult{ Branch: intent.BranchNewIntentPreempt, TopIntent: &intent.CandidateIntent{ Intent: intent.IntentDocGovReviewReject, FilledSlots: map[string]string{ intent.SlotTargetDoc: "doc_cardio_01", intent.SlotRejectReason: "第3节缺少血液相容性溶血试验评价指标", intent.SlotTargetSection: "第3节", }, }, } resReject, err := engine.ProcessIntent(context.Background(), recReject, userCtx) if err != nil { t.Fatalf("process reject failed: %v", err) } if resReject.Status != coordinator.StatusAccepted { t.Fatalf("expected reject status ACCEPTED, got %s", resReject.Status) } if resReject.InteractiveCard == nil { t.Fatalf("expected InteractiveCard to be generated") } if resReject.InteractiveCard.CardType != "CONFIRMATION" { t.Errorf("expected card type CONFIRMATION, got %s", resReject.InteractiveCard.CardType) } if resReject.InteractiveCard.TTLTurns != 3 { t.Errorf("expected TTLTurns 3, got %d", resReject.InteractiveCard.TTLTurns) } // 验证 NextExpectation 挂载正确 if resReject.NextExpectation == nil { t.Fatalf("expected NextExpectation to be populated") } if resReject.NextExpectation.TargetAction != "confirm_reject" { t.Errorf("expected TargetAction confirm_reject, got %s", resReject.NextExpectation.TargetAction) } if resReject.NextExpectation.ExpectedIntent != intent.IntentDocGovProposalDecide { t.Errorf("expected ExpectedIntent DOCGOV_PROPOSAL_DECIDE, got %s", resReject.NextExpectation.ExpectedIntent) } if resReject.NextExpectation.AffirmativeWord != "确认" { t.Errorf("expected AffirmativeWord 确认, got %s", resReject.NextExpectation.AffirmativeWord) } if resReject.NextExpectation.DeclineWord != "取消" { t.Errorf("expected DeclineWord 取消, got %s", resReject.NextExpectation.DeclineWord) } // 2. 验证用户说 "确认", "好", "同意" 均能命中 Branch 1 并正确驱动协调层执行驳回 affirmativeWords := []string{"确认", "好", "同意"} for _, word := range affirmativeWords { // 每次使用相同初始化的上下文进行分支 1 验证 testCtx := &coordinator.UserProjectContext{ User: userCtx.User, Project: userCtx.Project, InteractiveCard: resReject.InteractiveCard, PendingExpectation: resReject.NextExpectation, } snap := testCtx.ToSnapshot() recRes, err := rec.Recognize(context.Background(), word, snap) if err != nil { t.Fatalf("recognize '%s' failed: %v", word, err) } if recRes.Branch != intent.BranchExpectationHit { t.Errorf("word '%s': expected branch EXPECTATION_HIT, got %s", word, recRes.Branch) } if recRes.TopIntent.Intent != intent.IntentDocGovProposalDecide { t.Errorf("word '%s': expected intent DOCGOV_PROPOSAL_DECIDE, got %s", word, recRes.TopIntent.Intent) } // 协调层处理确认 coordRes, err := engine.ProcessIntent(context.Background(), recRes, testCtx) if err != nil { t.Fatalf("coord process confirmation failed for word '%s': %v", word, err) } if coordRes.Status != coordinator.StatusAccepted { t.Errorf("word '%s': expected status ACCEPTED, got %s", word, coordRes.Status) } if !coordRes.ClearExpectation { t.Errorf("word '%s': expected ClearExpectation true", word) } if !strings.Contains(coordRes.NaturalReply, "驳回") { t.Errorf("word '%s': expected natural reply to confirm reject execution, got: %s", word, coordRes.NaturalReply) } if testCtx.InteractiveCard != nil { t.Errorf("word '%s': expected InteractiveCard to be cleared after resolution", word) } } } // TestCoordinatorEngine_TTLTurnsDecayAndCardCleanup 测试卡片生命周期消退与过期清理 func TestCoordinatorEngine_TTLTurnsDecayAndCardCleanup(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ User: coordinator.UserInfo{ ID: "user_101", Name: "张工", Role: "engineer", ProjectRole: "composer", }, Project: coordinator.ProjectInfo{ Name: "测试项目", }, InteractiveCard: &intent.InteractiveCardPayload{ CardID: "card_test_ttl", CardType: "CONFIRMATION", TargetDocID: "doc_test_001", CurrentStatus: "active", TTLTurns: 2, }, } // 验证当前活动状态下 ToSnapshot() 正确输出 ActiveDocID snap0 := userCtx.ToSnapshot() if snap0.ActiveDocID != "doc_test_001" { t.Fatalf("expected ActiveDocID doc_test_001, got %s", snap0.ActiveDocID) } // 轮次 1:执行一个无关对话意图,TTLTurns 衰减为 1 rec1 := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentCasualChat, }, } _, err := engine.ProcessIntent(context.Background(), rec1, userCtx) if err != nil { t.Fatalf("turn 1 failed: %v", err) } if userCtx.InteractiveCard == nil { t.Fatalf("turn 1: expected card to still exist with TTL 1") } if userCtx.InteractiveCard.TTLTurns != 1 { t.Errorf("turn 1: expected TTLTurns 1, got %d", userCtx.InteractiveCard.TTLTurns) } snap1 := userCtx.ToSnapshot() if snap1.ActiveDocID != "doc_test_001" { t.Errorf("turn 1: expected ActiveDocID doc_test_001, got %s", snap1.ActiveDocID) } // 轮次 2:再执行一个无关意图,TTLTurns 衰减为 0 <= 0,卡片过期并置空清理 rec2 := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentCasualChat, }, } _, err = engine.ProcessIntent(context.Background(), rec2, userCtx) if err != nil { t.Fatalf("turn 2 failed: %v", err) } if userCtx.InteractiveCard != nil { t.Errorf("turn 2: expected card to be cleared after TTL expiration, got %+v", userCtx.InteractiveCard) } // 快照不再包含失效的 ActiveDocID snap2 := userCtx.ToSnapshot() if snap2.ActiveDocID != "" { t.Errorf("turn 2: expected ActiveDocID to be empty after expiry, got %s", snap2.ActiveDocID) } // 验证非 active 状态卡片立即被清理 userCtx.InteractiveCard = &intent.InteractiveCardPayload{ CardID: "card_cancelled", CardType: "CONFIRMATION", TargetDocID: "doc_test_002", CurrentStatus: "cancelled", TTLTurns: 5, } // ToSnapshot() 对非 active 状态忽略 ActiveDocID if userCtx.ToSnapshot().ActiveDocID != "" { t.Errorf("expected non-active card to not set ActiveDocID in snapshot") } // 经过 ProcessIntent 自动清理置空 _, _ = engine.ProcessIntent(context.Background(), rec1, userCtx) if userCtx.InteractiveCard != nil { t.Errorf("expected non-active card to be cleared by ProcessIntent") } } // TestCoordinatorEngine_ApproveRoleValidationAndFinalConfirmationCard 测试批准动作角色权责核验与终审批准二次确认 func TestCoordinatorEngine_ApproveRoleValidationAndFinalConfirmationCard(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) rec := intent.NewRecognizer() // 1. 审核通过 (ReviewApprove):未授权角色 (composer / engineer) 必须被拒绝 composerCtx := &coordinator.UserProjectContext{ User: coordinator.UserInfo{ ID: "user_composer", Name: "张编制", Role: "engineer", ProjectRole: "composer", }, Project: coordinator.ProjectInfo{ Name: "血液透析机项目", }, } recReviewApprove := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentDocGovReviewApprove, FilledSlots: map[string]string{ intent.SlotTargetDoc: "doc_dialysis_01", }, }, } resUnauthorizedReview, err := engine.ProcessIntent(context.Background(), recReviewApprove, composerCtx) if err != nil { t.Fatalf("process review approve failed: %v", err) } if resUnauthorizedReview.Status != coordinator.StatusRejected { t.Errorf("expected review approve by composer to be REJECTED, got %s", resUnauthorizedReview.Status) } if resUnauthorizedReview.RejectReason != "UNAUTHORIZED_REVIEW_ROLE" { t.Errorf("expected reject reason UNAUTHORIZED_REVIEW_ROLE, got %s", resUnauthorizedReview.RejectReason) } if !strings.Contains(resUnauthorizedReview.NaturalReply, "权限不足") { t.Errorf("expected natural reply to explain permission lack, got %s", resUnauthorizedReview.NaturalReply) } // 2. 审核通过 (ReviewApprove):合法角色 (lead_engineer 或 reviewer) 允许通过 reviewerCtx := &coordinator.UserProjectContext{ User: coordinator.UserInfo{ ID: "user_lead", Name: "赵主管", Role: "lead_engineer", ProjectRole: "reviewer", }, Project: coordinator.ProjectInfo{ Name: "血液透析机项目", }, } resAuthorizedReview, err := engine.ProcessIntent(context.Background(), recReviewApprove, reviewerCtx) if err != nil { t.Fatalf("process review approve with lead_engineer failed: %v", err) } if resAuthorizedReview.Status != coordinator.StatusAccepted { t.Errorf("expected review approve by lead_engineer to be ACCEPTED, got %s", resAuthorizedReview.Status) } // 3. 终审批准 (FinalApprove):未授权角色 (reviewer) 必须被拒绝 recFinalApprove := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentDocGovFinalApprove, FilledSlots: map[string]string{ intent.SlotTargetDoc: "doc_dialysis_01", }, }, } resUnauthorizedFinal, err := engine.ProcessIntent(context.Background(), recFinalApprove, reviewerCtx) if err != nil { t.Fatalf("process final approve with reviewer failed: %v", err) } if resUnauthorizedFinal.Status != coordinator.StatusRejected { t.Errorf("expected final approve by reviewer to be REJECTED, got %s", resUnauthorizedFinal.Status) } if resUnauthorizedFinal.RejectReason != "UNAUTHORIZED_APPROVAL_ROLE" { t.Errorf("expected reject reason UNAUTHORIZED_APPROVAL_ROLE, got %s", resUnauthorizedFinal.RejectReason) } // 4. 终审批准 (FinalApprove):合法角色 (approver 或 project_manager) 生成 CONFIRMATION 卡片与 NextExpectation approverCtx := &coordinator.UserProjectContext{ User: coordinator.UserInfo{ ID: "user_pm", Name: "王总监", Role: "project_manager", ProjectRole: "approver", }, Project: coordinator.ProjectInfo{ Name: "血液透析机项目", }, } resAuthorizedFinal, err := engine.ProcessIntent(context.Background(), recFinalApprove, approverCtx) if err != nil { t.Fatalf("process final approve with approver failed: %v", err) } if resAuthorizedFinal.Status != coordinator.StatusAccepted { t.Fatalf("expected final approve by approver to be ACCEPTED, got %s", resAuthorizedFinal.Status) } if resAuthorizedFinal.InteractiveCard == nil { t.Fatalf("expected CONFIRMATION card for final approve") } if resAuthorizedFinal.InteractiveCard.CardType != "CONFIRMATION" { t.Errorf("expected card type CONFIRMATION, got %s", resAuthorizedFinal.InteractiveCard.CardType) } if resAuthorizedFinal.InteractiveCard.TTLTurns != 3 { t.Errorf("expected card TTLTurns 3, got %d", resAuthorizedFinal.InteractiveCard.TTLTurns) } if resAuthorizedFinal.NextExpectation == nil { t.Fatalf("expected NextExpectation for final approve confirmation") } if resAuthorizedFinal.NextExpectation.TargetAction != "confirm_final_approve" { t.Errorf("expected target action confirm_final_approve, got %s", resAuthorizedFinal.NextExpectation.TargetAction) } // 5. 模拟总监在下一轮回复“确认”,分支 1 命中并正式受控锁定归档 snap := approverCtx.ToSnapshot() recConfirm, err := rec.Recognize(context.Background(), "确认", snap) if err != nil { t.Fatalf("recognize confirm failed: %v", err) } if recConfirm.Branch != intent.BranchExpectationHit { t.Errorf("expected branch EXPECTATION_HIT, got %s", recConfirm.Branch) } resConfirm, err := engine.ProcessIntent(context.Background(), recConfirm, approverCtx) if err != nil { t.Fatalf("process confirm final approve failed: %v", err) } if resConfirm.Status != coordinator.StatusAccepted { t.Errorf("expected confirm status ACCEPTED, got %s", resConfirm.Status) } if !(strings.Contains(resConfirm.NaturalReply, "终审") || strings.Contains(resConfirm.NaturalReply, "批准")) { t.Errorf("expected natural reply to confirm final approval, got: %s", resConfirm.NaturalReply) } if approverCtx.InteractiveCard != nil { t.Errorf("expected interactive card to be cleared after final approve confirmation") } if approverCtx.PendingExpectation != nil { t.Errorf("expected PendingExpectation to be cleared after final approve confirmation") } } func TestCoordinatorEngine_QueryRegulations_BaselineFallback(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ Project: coordinator.ProjectInfo{ Name: "4K超高清荧光内窥镜系统", DeviceType: "内窥镜", }, RewrittenQuery: "4K超高清荧光内窥镜系统适用的国家标准和法规有哪些?", } recRes := &intent.IntentRecognitionResult{ TopIntent: &intent.CandidateIntent{ Intent: intent.IntentQueryRegulations, Branch: intent.BranchNewIntentPreempt, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("process intent failed: %v", err) } if res.Status != coordinator.StatusAccepted { t.Errorf("expected status ACCEPTED, got %s", res.Status) } if !strings.Contains(res.NaturalReply, "GB 9706.1-2020") { t.Errorf("expected reply to contain GB 9706.1-2020, got: %s", res.NaturalReply) } if !strings.Contains(res.NaturalReply, "YY 9706.102-2021") { t.Errorf("expected reply to contain YY 9706.102-2021, got: %s", res.NaturalReply) } } func TestCoordinatorEngine_TestingAnalysis(t *testing.T) { engine := coordinator.NewCoordinatorEngine(nil) userCtx := &coordinator.UserProjectContext{ Project: coordinator.ProjectInfo{ Name: "肌骨超声AI辅助诊断系统", DeviceType: "肌骨超声", }, User: coordinator.UserInfo{ ID: "108", Name: "张工", Role: "test_manager", ProjectRole: "test_manager", }, } recRes := &intent.IntentRecognitionResult{ RawInput: "分析一下测试方案中,有哪些比较难的项目,以及需要特别注意的地方", TopIntent: &intent.CandidateIntent{ Intent: intent.IntentQueryProgress, Branch: intent.BranchNewIntentPreempt, FilledSlots: map[string]string{ intent.SlotQueryTopic: "testing_analysis", }, }, } res, err := engine.ProcessIntent(context.Background(), recRes, userCtx) if err != nil { t.Fatalf("process intent failed: %v", err) } if res.Status != coordinator.StatusAccepted { t.Errorf("expected status ACCEPTED, got %s", res.Status) } // 验证包含声输出安全与温升要求 GB 9706.237 if !strings.Contains(res.NaturalReply, "GB 9706.237-2020") { t.Errorf("expected reply to contain GB 9706.237-2020, got: %s", res.NaturalReply) } // 验证包含体模标定与测距公差 0.3mm if !strings.Contains(res.NaturalReply, "0.3mm") { t.Errorf("expected reply to contain 0.3mm tolerance, got: %s", res.NaturalReply) } // 验证包含 AI 算法滑膜分割与 Dice 系数 if !strings.Contains(res.NaturalReply, "Dice") { t.Errorf("expected reply to contain Dice score, got: %s", res.NaturalReply) } // 严格杜绝商业摸底机械套话 if strings.Contains(res.NaturalReply, "完全理解您想再掂量一下") { t.Errorf("reply must not contain commercial triage boilerplate, got: %s", res.NaturalReply) } if !strings.Contains(res.NaturalReply, "张工,您好") { t.Errorf("greeting must use the user name, got: %s", res.NaturalReply) } if strings.Contains(res.NaturalReply, "张工您好") { t.Errorf("hardcoded greeting must be gone, got: %s", res.NaturalReply) } }