Files
Agent-System/internal/coordinator/engine_test.go
T
weichen b5a782bb38 feat(chat): dialogue triggers for comments/reopen/pending-supplements with mistouch guards
Wire intent→coordinator DocResolver confirm cards→action_executor kinds for comment, reopen, pending-supplements query, and real submit-review execution.
2026-09-25 20:14:10 +00:00

1069 lines
36 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package coordinator_test
import (
"context"
"strings"
"testing"
"chat-agent-system/internal/app/stageflow"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/maturity"
"chat-agent-system/internal/squad"
)
// nilBriefStageRunner 模拟阶段任务全部 halt / 无已执行产物:AdvisorBrief 为 nil。
type nilBriefStageRunner struct{}
func (nilBriefStageRunner) ExecuteTask(ctx context.Context, req stageflow.TaskRunRequest) (*stageflow.TaskRunResult, error) {
return &stageflow.TaskRunResult{}, nil
}
func (nilBriefStageRunner) ExecuteStage(ctx context.Context, req stageflow.StageRunRequest) (*stageflow.StageRunResult, error) {
return &stageflow.StageRunResult{
Stage: req.Stage,
StageTitle: "可行性论证",
Success: true,
// PrimaryArtifact / AdvisorBrief 均为 nil —— 复现 2026-09-25 nil-deref panic
}, nil
}
// 回归:空 executedArtifacts 时 stageBrief=nil,旧代码在 NaturalReply 组装处
// 裸解引用 stageRes.AdvisorBrief 导致 panic(TestBizDev_CaseRecommendation_FullLink_E2E 亦触发)。
func TestCoordinatorEngine_AdvanceStageNilBriefNoPanic(t *testing.T) {
engine := coordinator.NewCoordinatorEngine(nil)
engine.SetStageTaskRunner(nilBriefStageRunner{})
userCtx := &coordinator.UserProjectContext{
Project: coordinator.ProjectInfo{
Name: "超声测试项目",
CurrentStage: maturity.StageV01_Triage,
},
User: coordinator.UserInfo{ProjectRole: "composer"},
BlackboardFacts: make(map[string]string),
}
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.Fatalf("expected status ACCEPTED, got %s", res.Status)
}
if !strings.Contains(res.NaturalReply, "未生成阶段级参谋简报") {
t.Errorf("expected graceful nil-brief fallback in reply, got: %s", res.NaturalReply)
}
if userCtx.LatestAdvisorBrief != nil {
t.Errorf("expected LatestAdvisorBrief to stay nil, got %+v", userCtx.LatestAdvisorBrief)
}
}
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)
}
if resClaim.InteractiveCard == nil && resClaim.Status == coordinator.StatusAccepted {
// Without DocResolver, ambiguous/empty ref clarifies; with doc id soft-fallback may still card.
}
// Soft fallback treats explicit doc id as resolved → confirmation card (no fake success PendingAction).
if resClaim.PendingAction != nil {
t.Errorf("claim must not emit PendingAction before confirm")
}
if resClaim.InteractiveCard == nil {
t.Errorf("expected claim confirmation card, reply=%q", resClaim.NaturalReply)
}
// 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 (confirm card), got %s reply=%q", resSubmit.Status, resSubmit.NaturalReply)
}
if resSubmit.PendingAction != nil {
t.Errorf("submit must not emit PendingAction before confirm")
}
if resSubmit.InteractiveCard == nil {
t.Errorf("expected submit confirmation card")
}
if resSubmit.InteractiveCard == nil || resSubmit.InteractiveCard.CardType != "CONFIRMATION" {
t.Errorf("expected submit CONFIRMATION card, got %+v", resSubmit.InteractiveCard)
}
if resSubmit.PendingAction != nil {
t.Errorf("submit must not PendingAction before confirm")
}
// 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.StatusClarificationRequired {
t.Errorf("expected proposal default branch to clarify (no fake success), 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)
}
}