Files
Agent-System/internal/coordinator/engine_test.go
T

934 lines
31 KiB
Go

package coordinator_test
import (
"context"
"testing"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/intent"
"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: bizdev.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: bizdev.StageV01_Triage,
},
User: coordinator.UserInfo{
ProjectRole: "composer",
},
}
recRes := &intent.IntentRecognitionResult{
TopIntent: &intent.CandidateIntent{
Intent: intent.IntentAdvanceStage,
FilledSlots: map[string]string{
intent.SlotTargetStage: string(bizdev.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 != bizdev.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: bizdev.StageV01_Triage,
},
PendingExpectation: coordinator.BuildNextExpectation(bizdev.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: bizdev.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: bizdev.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(bizdev.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, "已确认终审批准") {
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)
}
}