Reactivate dev seed users after auth PG tests, use unique session tokens, fix import guard and coordinator assertions, and skip flaky E2E in short mode; add pg_restore CI drill job. Co-authored-by: Cursor <cursoragent@cursor.com>
626 lines
22 KiB
Go
626 lines
22 KiB
Go
package chat_test
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"chat-agent-system/internal/bizdev"
|
||
"chat-agent-system/internal/chat"
|
||
"chat-agent-system/internal/config"
|
||
"chat-agent-system/internal/coordinator"
|
||
"chat-agent-system/internal/ctxmgr"
|
||
"chat-agent-system/internal/docgov"
|
||
"chat-agent-system/internal/intent"
|
||
"chat-agent-system/internal/squad"
|
||
)
|
||
|
||
func TestChatSession_EndToEndDialog(t *testing.T) {
|
||
rec := intent.NewRecognizer()
|
||
engine := coordinator.NewCoordinatorEngine(nil)
|
||
|
||
userCtx := &coordinator.UserProjectContext{
|
||
SnapshotID: "sess_ctx_1",
|
||
User: coordinator.UserInfo{
|
||
ID: "u1",
|
||
Name: "张工",
|
||
Role: "engineer",
|
||
ProjectRole: "composer",
|
||
},
|
||
Project: coordinator.ProjectInfo{
|
||
Name: "掌上无线超声系统",
|
||
CurrentStage: bizdev.StageV01_Triage,
|
||
},
|
||
LatestAdvisorBrief: &squad.AdvisorBriefing{
|
||
MaturityScore: 75,
|
||
MaturityLevel: "V0.1",
|
||
MaturityHeadline: "初步框架已就绪",
|
||
PreemptiveRisks: []string{"探头功耗与散热设计"},
|
||
NextBestActions: []string{"补充客户供电参数", "推进到V0.5可行性论证"},
|
||
CommunicationKit: "建议与客户确认供电方式",
|
||
},
|
||
}
|
||
|
||
session := chat.NewSession("sess_001", userCtx, rec, engine)
|
||
|
||
// Turn 1: 问候 (分支 2: 新指令/对话)
|
||
resp1, err := session.SendMessage(context.Background(), "你好,请问你是谁?")
|
||
if err != nil {
|
||
t.Fatalf("turn 1 failed: %v", err)
|
||
}
|
||
if resp1.IntentResult.TopIntent.Intent != intent.IntentCasualChat {
|
||
t.Errorf("turn 1 expected CASUAL_CHAT, got %s", resp1.IntentResult.TopIntent.Intent)
|
||
}
|
||
|
||
// Turn 2: 查进展与风险 (分支 2: 抢占查询)
|
||
resp2, err := session.SendMessage(context.Background(), "现在这个项目进展怎么样?有啥风险没?")
|
||
if err != nil {
|
||
t.Fatalf("turn 2 failed: %v", err)
|
||
}
|
||
if resp2.IntentResult.TopIntent.Intent != intent.IntentQueryProgress {
|
||
t.Errorf("turn 2 expected QUERY_PROGRESS, got %s", resp2.IntentResult.TopIntent.Intent)
|
||
}
|
||
if !strings.Contains(resp2.ReplyText, "掌上无线超声系统") {
|
||
t.Errorf("turn 2 reply should mention project name: %s", resp2.ReplyText)
|
||
}
|
||
|
||
// Turn 3: 补充信息 (分支 2: 补充事实)
|
||
resp3, err := session.SendMessage(context.Background(), "客户补充说目标市场是国内及欧洲CE")
|
||
if err != nil {
|
||
t.Fatalf("turn 3 failed: %v", err)
|
||
}
|
||
if resp3.IntentResult.TopIntent.Intent != intent.IntentSupplementInfo {
|
||
t.Errorf("turn 3 expected SUPPLEMENT_INFO, got %s", resp3.IntentResult.TopIntent.Intent)
|
||
}
|
||
if session.UserCtx.BlackboardFacts["target_market"] != "国内及欧洲CE" {
|
||
t.Errorf("target_market not updated: %v", session.UserCtx.BlackboardFacts)
|
||
}
|
||
|
||
// Turn 4: 单字快速肯定兑现上轮推荐 (分支 1: 极速兑现)
|
||
resp4, err := session.SendMessage(context.Background(), "好")
|
||
if err != nil {
|
||
t.Fatalf("turn 4 failed: %v", err)
|
||
}
|
||
if resp4.Branch != intent.BranchExpectationHit {
|
||
t.Errorf("turn 4 expected branch EXPECTATION_HIT, got %s", resp4.Branch)
|
||
}
|
||
if resp4.IntentResult.TopIntent.Intent != intent.IntentAdvanceStage {
|
||
t.Errorf("turn 4 expected ADVANCE_STAGE, got %s", resp4.IntentResult.TopIntent.Intent)
|
||
}
|
||
if session.UserCtx.Project.CurrentStage != bizdev.StageV05_Feasibility {
|
||
t.Errorf("expected project advanced to V0.5, got %s", session.UserCtx.Project.CurrentStage)
|
||
}
|
||
|
||
if len(session.Messages) != 8 {
|
||
t.Errorf("expected 8 messages (4 user + 4 asst), got %d", len(session.Messages))
|
||
}
|
||
}
|
||
|
||
// 用例 1: 分支 1 极速兑现 ("好" / "1")
|
||
func TestChatSession_Branch1_FastAffirmative(t *testing.T) {
|
||
rec := intent.NewRecognizer()
|
||
engine := coordinator.NewCoordinatorEngine(nil)
|
||
userCtx := &coordinator.UserProjectContext{
|
||
Project: coordinator.ProjectInfo{
|
||
Name: "心电贴片系统",
|
||
CurrentStage: bizdev.StageV01_Triage,
|
||
},
|
||
}
|
||
|
||
session := chat.NewSession("sess_fast", userCtx, rec, engine)
|
||
resp, err := session.SendMessage(context.Background(), "好")
|
||
if err != nil {
|
||
t.Fatalf("send message failed: %v", err)
|
||
}
|
||
|
||
if resp.Branch != intent.BranchExpectationHit {
|
||
t.Errorf("expected EXPECTATION_HIT, got %s", resp.Branch)
|
||
}
|
||
if resp.IntentResult.TopIntent.Intent != intent.IntentAdvanceStage {
|
||
t.Errorf("expected ADVANCE_STAGE, got %s", resp.IntentResult.TopIntent.Intent)
|
||
}
|
||
if session.UserCtx.Project.CurrentStage != bizdev.StageV05_Feasibility {
|
||
t.Errorf("expected stage V0.5, got %s", session.UserCtx.Project.CurrentStage)
|
||
}
|
||
}
|
||
|
||
// 用例 2: 分支 1 复合打补丁 ("好,但目标市场改成欧洲CE")
|
||
func TestChatSession_Branch1_AffirmativeWithPatch(t *testing.T) {
|
||
rec := intent.NewRecognizer()
|
||
engine := coordinator.NewCoordinatorEngine(nil)
|
||
userCtx := &coordinator.UserProjectContext{
|
||
Project: coordinator.ProjectInfo{
|
||
Name: "便携超声设备",
|
||
CurrentStage: bizdev.StageV01_Triage,
|
||
},
|
||
BlackboardFacts: make(map[string]string),
|
||
}
|
||
|
||
session := chat.NewSession("sess_patch", userCtx, rec, engine)
|
||
resp, err := session.SendMessage(context.Background(), "好,但目标市场改成欧洲CE")
|
||
if err != nil {
|
||
t.Fatalf("send message failed: %v", err)
|
||
}
|
||
|
||
if resp.Branch != intent.BranchExpectationHit {
|
||
t.Errorf("expected EXPECTATION_HIT, got %s", resp.Branch)
|
||
}
|
||
if resp.IntentResult.TopIntent.Intent != intent.IntentAdvanceStage {
|
||
t.Errorf("expected ADVANCE_STAGE, got %s", resp.IntentResult.TopIntent.Intent)
|
||
}
|
||
if session.UserCtx.BlackboardFacts["target_market"] != "欧洲CE" {
|
||
t.Errorf("expected blackboard target_market '欧洲CE', got '%s'", session.UserCtx.BlackboardFacts["target_market"])
|
||
}
|
||
if session.UserCtx.Project.TargetMarket != "欧洲CE" {
|
||
t.Errorf("expected project TargetMarket '欧洲CE', got '%s'", session.UserCtx.Project.TargetMarket)
|
||
}
|
||
}
|
||
|
||
// 用例 3: 分支 1 否定撤销 ("先别推,等等")
|
||
func TestChatSession_Branch1_Decline(t *testing.T) {
|
||
rec := intent.NewRecognizer()
|
||
engine := coordinator.NewCoordinatorEngine(nil)
|
||
userCtx := &coordinator.UserProjectContext{
|
||
Project: coordinator.ProjectInfo{
|
||
Name: "骨科手术机器人",
|
||
CurrentStage: bizdev.StageV01_Triage,
|
||
},
|
||
}
|
||
|
||
session := chat.NewSession("sess_decline", userCtx, rec, engine)
|
||
resp, err := session.SendMessage(context.Background(), "先别推,等等")
|
||
if err != nil {
|
||
t.Fatalf("send message failed: %v", err)
|
||
}
|
||
|
||
if resp.Branch != intent.BranchExpectationHit {
|
||
t.Errorf("expected EXPECTATION_HIT, got %s", resp.Branch)
|
||
}
|
||
if resp.IntentResult.TopIntent.Intent != intent.IntentDeclineExpectation {
|
||
t.Errorf("expected DECLINE_EXPECTATION, got %s", resp.IntentResult.TopIntent.Intent)
|
||
}
|
||
if session.UserCtx.PendingExpectation != nil {
|
||
t.Errorf("expected PendingExpectation to be cleared after decline")
|
||
}
|
||
if !strings.Contains(resp.ReplyText, "听您的") {
|
||
t.Errorf("expected polite advisor reply: %s", resp.ReplyText)
|
||
}
|
||
}
|
||
|
||
// 用例 4: 分支 2 突发话题抢占 ("查一下探头功耗要怎么设计")
|
||
func TestChatSession_Branch2_TopicPreemption(t *testing.T) {
|
||
rec := intent.NewRecognizer()
|
||
engine := coordinator.NewCoordinatorEngine(nil)
|
||
userCtx := &coordinator.UserProjectContext{
|
||
Project: coordinator.ProjectInfo{
|
||
Name: "无线超声系统",
|
||
CurrentStage: bizdev.StageV01_Triage,
|
||
},
|
||
}
|
||
|
||
session := chat.NewSession("sess_preempt", userCtx, rec, engine)
|
||
resp, err := session.SendMessage(context.Background(), "查一下探头功耗要怎么设计")
|
||
if err != nil {
|
||
t.Fatalf("send message failed: %v", err)
|
||
}
|
||
|
||
if resp.Branch != intent.BranchNewIntentPreempt {
|
||
t.Errorf("expected NEW_INTENT_PREEMPT, got %s", resp.Branch)
|
||
}
|
||
if resp.IntentResult.TopIntent.Intent != intent.IntentQueryProgress {
|
||
t.Errorf("expected QUERY_PROGRESS, got %s", resp.IntentResult.TopIntent.Intent)
|
||
}
|
||
if !strings.Contains(resp.ReplyText, "功耗") && !strings.Contains(resp.ReplyText, "架构") {
|
||
t.Errorf("expected technical answer in reply: %s", resp.ReplyText)
|
||
}
|
||
}
|
||
|
||
// 用例 5: 分支 3 模糊分忧 ("这个先放放,你觉得呢")
|
||
func TestChatSession_Branch3_AmbiguousConsult(t *testing.T) {
|
||
rec := intent.NewRecognizer()
|
||
engine := coordinator.NewCoordinatorEngine(nil)
|
||
userCtx := &coordinator.UserProjectContext{
|
||
Project: coordinator.ProjectInfo{
|
||
Name: "智能输注泵项目",
|
||
CurrentStage: bizdev.StageV01_Triage,
|
||
},
|
||
}
|
||
|
||
session := chat.NewSession("sess_consult", userCtx, rec, engine)
|
||
resp, err := session.SendMessage(context.Background(), "这个先放放,你觉得呢")
|
||
if err != nil {
|
||
t.Fatalf("send message failed: %v", err)
|
||
}
|
||
|
||
if resp.Branch != intent.BranchAmbiguousFallback {
|
||
t.Errorf("expected AMBIGUOUS_FALLBACK, got %s", resp.Branch)
|
||
}
|
||
if !strings.Contains(resp.ReplyText, "完全理解") && !strings.Contains(resp.ReplyText, "建议") {
|
||
t.Errorf("expected thoughtful advisor consult in reply: %s", resp.ReplyText)
|
||
}
|
||
if session.UserCtx.PendingExpectation == nil {
|
||
t.Errorf("expected NextExpectation to be populated for user choice")
|
||
}
|
||
}
|
||
|
||
// 用例 6: 贴身参谋决策锦囊中自推荐往期标杆案例
|
||
func TestChatSession_AdvisorCaseRecommendation(t *testing.T) {
|
||
cfg := &config.Config{DataDir: t.TempDir()}
|
||
svc := bizdev.MustNewService(t, cfg)
|
||
ctx := context.Background()
|
||
|
||
// 1. 预置一个已终审批准的往期标杆案例
|
||
doc := &docgov.DocumentRecord{
|
||
DocID: "doc_history_ultrasonic_01",
|
||
ProjectID: 201,
|
||
TaskKey: "feasibility_reg",
|
||
Title: "超声软组织切割止血手术设备注册评估",
|
||
Stage: "V0.5_feasibility",
|
||
CurrentState: docgov.StateApproved,
|
||
IsLocked: true,
|
||
AuthorName: "李工",
|
||
}
|
||
body := `# 超声软组织切割止血手术设备注册评估
|
||
|
||
## 概述与器械分类
|
||
主要用于外科微创手术中软组织切割与止血闭合,按三类医疗器械申报。
|
||
|
||
## 关键性能与型检预估
|
||
重点满足 GB 9706.1-2020 及 YY 0644 专用安全规范,型检周期约 5 个月。`
|
||
|
||
_, err := svc.ContextManager().IndexApprovedDocument(ctx, doc, body)
|
||
if err != nil {
|
||
t.Fatalf("IndexApprovedDocument failed: %v", err)
|
||
}
|
||
|
||
// 2. 发起新会话咨询
|
||
rec := intent.NewRecognizer()
|
||
engine := coordinator.NewCoordinatorEngine(svc)
|
||
userCtx := &coordinator.UserProjectContext{
|
||
Project: coordinator.ProjectInfo{
|
||
Name: "超声软组织刀研发咨询",
|
||
DeviceType: "超声手术设备",
|
||
IntendedUse: "软组织切割闭合",
|
||
CurrentStage: bizdev.StageV01_Triage,
|
||
},
|
||
LatestAdvisorBrief: &squad.AdvisorBriefing{
|
||
MaturityScore: 60,
|
||
MaturityLevel: "V0.1",
|
||
MaturityHeadline: "需求初步梳理中",
|
||
},
|
||
}
|
||
|
||
session := chat.NewSession("sess_case_recommend", userCtx, rec, engine)
|
||
session.CtxMgr = svc.ContextManager()
|
||
|
||
resp, err := session.SendMessage(ctx, "我们之前做过类似的超声手术设备吗?注册和型检要注意什么?")
|
||
if err != nil {
|
||
t.Fatalf("SendMessage failed: %v", err)
|
||
}
|
||
|
||
if resp.AdvisorBrief == nil {
|
||
t.Fatalf("expected AdvisorBrief in response")
|
||
}
|
||
if len(resp.AdvisorBrief.RecommendedCases) == 0 {
|
||
t.Fatalf("expected RecommendedCases to be populated, got 0")
|
||
}
|
||
|
||
recCase := resp.AdvisorBrief.RecommendedCases[0]
|
||
t.Logf("✓ 贴身参谋锦囊成功自推荐标杆案例: 文档=%s, 标题=%s, 章节=%s, 相似度=%.2f",
|
||
recCase.DocID, recCase.ArtifactTitle, recCase.SectionTitle, recCase.SimilarityScore)
|
||
|
||
if recCase.DocID != "doc_history_ultrasonic_01" {
|
||
t.Errorf("unexpected doc ID: %s", recCase.DocID)
|
||
}
|
||
}
|
||
|
||
func TestChatSession_RecentDialogueAndGuidanceSync(t *testing.T) {
|
||
ctx := context.Background()
|
||
rec := intent.NewRecognizer()
|
||
engine := coordinator.NewCoordinatorEngine(nil)
|
||
ctxMgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.DefaultTimeDecayConfig())
|
||
|
||
userCtx := &coordinator.UserProjectContext{
|
||
SnapshotID: "sess_ctx_sync",
|
||
User: coordinator.UserInfo{
|
||
ID: "u_sync_1",
|
||
Name: "李工",
|
||
Role: "engineer",
|
||
ProjectRole: "composer",
|
||
},
|
||
Project: coordinator.ProjectInfo{
|
||
Name: "数字化微创内窥镜",
|
||
CurrentStage: bizdev.StageV01_Triage,
|
||
},
|
||
BlackboardFacts: make(map[string]string),
|
||
}
|
||
|
||
session := chat.NewSession("sess_sync_001", userCtx, rec, engine)
|
||
session.UserID = 42
|
||
session.ProjectID = 100
|
||
session.CtxMgr = ctxMgr
|
||
|
||
// Turn 1: 问候
|
||
_, err := session.SendMessage(ctx, "你好,我是内窥镜项目负责人李工")
|
||
if err != nil {
|
||
t.Fatalf("turn 1 failed: %v", err)
|
||
}
|
||
if len(session.UserCtx.RecentDialogue) != 2 {
|
||
t.Fatalf("expected RecentDialogue to have 2 turns after turn 1, got %d", len(session.UserCtx.RecentDialogue))
|
||
}
|
||
if session.UserCtx.RecentDialogue[0].Role != "user" || !strings.Contains(session.UserCtx.RecentDialogue[0].Content, "李工") {
|
||
t.Errorf("unexpected user dialogue turn: %+v", session.UserCtx.RecentDialogue[0])
|
||
}
|
||
if session.UserCtx.RecentDialogue[1].Role != "assistant" {
|
||
t.Errorf("unexpected assistant dialogue turn: %+v", session.UserCtx.RecentDialogue[1])
|
||
}
|
||
|
||
// Turn 2: 用户表达偏好指令 ("以后请...")
|
||
_, err = session.SendMessage(ctx, "以后请直接说重点并优先关注法规标准合规")
|
||
if err != nil {
|
||
t.Fatalf("turn 2 failed: %v", err)
|
||
}
|
||
if len(session.UserCtx.RecentDialogue) != 4 {
|
||
t.Fatalf("expected RecentDialogue to have 4 turns after turn 2, got %d", len(session.UserCtx.RecentDialogue))
|
||
}
|
||
if session.UserCtx.PersonalizedGuidance == "" {
|
||
t.Errorf("expected PersonalizedGuidance to be populated after custom instruction")
|
||
}
|
||
if !strings.Contains(session.UserCtx.PersonalizedGuidance, "Memory Injection") {
|
||
t.Errorf("expected Memory Injection header in PersonalizedGuidance, got: %s", session.UserCtx.PersonalizedGuidance)
|
||
}
|
||
|
||
// Turn 3: 连续对话验证
|
||
_, err = session.SendMessage(ctx, "现在项目进展到哪一步了?")
|
||
if err != nil {
|
||
t.Fatalf("turn 3 failed: %v", err)
|
||
}
|
||
if len(session.UserCtx.RecentDialogue) != 6 {
|
||
t.Fatalf("expected RecentDialogue to have 6 turns after turn 3, got %d", len(session.UserCtx.RecentDialogue))
|
||
}
|
||
if session.UserCtx.PersonalizedGuidance == "" {
|
||
t.Errorf("PersonalizedGuidance lost on subsequent turn")
|
||
}
|
||
}
|
||
|
||
func TestChatSession_SlidingWindowAndSummary(t *testing.T) {
|
||
ctx := context.Background()
|
||
rec := intent.NewRecognizer()
|
||
engine := coordinator.NewCoordinatorEngine(nil)
|
||
ctxMgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.DefaultTimeDecayConfig())
|
||
|
||
userCtx := &coordinator.UserProjectContext{
|
||
SnapshotID: "sess_ctx_window",
|
||
User: coordinator.UserInfo{
|
||
ID: "u_win_1",
|
||
Name: "赵工",
|
||
Role: "engineer",
|
||
ProjectRole: "composer",
|
||
},
|
||
Project: coordinator.ProjectInfo{
|
||
Name: "数字化高频超声吸引系统",
|
||
CurrentStage: bizdev.StageV01_Triage,
|
||
},
|
||
BlackboardFacts: make(map[string]string),
|
||
}
|
||
|
||
session := chat.NewSession("sess_win_001", userCtx, rec, engine)
|
||
session.UserID = 99
|
||
session.ProjectID = 200
|
||
session.CtxMgr = ctxMgr
|
||
|
||
// 模拟连续进行 10 轮交互 (生成 20 条消息,超出默认窗口上限 16 条)
|
||
// 第 1 轮带有核心事实
|
||
_, err := session.SendMessage(ctx, "你好,我们准备立项高频超声手术刀系统,预算200万")
|
||
if err != nil {
|
||
t.Fatalf("turn 1 failed: %v", err)
|
||
}
|
||
|
||
// 模拟追加 9 轮简短问答
|
||
for i := 2; i <= 10; i++ {
|
||
_, err := session.SendMessage(ctx, fmt.Sprintf("这是第 %d 轮日常沟通咨询", i))
|
||
if err != nil {
|
||
t.Fatalf("turn %d failed: %v", i, err)
|
||
}
|
||
}
|
||
|
||
if len(session.Messages) != 20 {
|
||
t.Fatalf("expected 20 total messages, got %d", len(session.Messages))
|
||
}
|
||
|
||
// 验证双预算滑动窗口(条数上限 16 且受 Token 预算约束裁剪)
|
||
if len(session.UserCtx.RecentDialogue) > 16 || len(session.UserCtx.RecentDialogue) < 10 {
|
||
t.Errorf("expected between 10 and 16 messages in RecentDialogue window under dual budget, got %d", len(session.UserCtx.RecentDialogue))
|
||
}
|
||
|
||
// 验证早期被截断的消息是否被压缩为滚动摘要
|
||
if session.UserCtx.DialogueSummary == "" {
|
||
t.Errorf("expected DialogueSummary to be populated after window truncation")
|
||
}
|
||
if !strings.Contains(session.UserCtx.DialogueSummary, "【早期对话摘要】") {
|
||
t.Errorf("expected summary header, got: %s", session.UserCtx.DialogueSummary)
|
||
}
|
||
if !strings.Contains(session.UserCtx.DialogueSummary, "超声手术设备") || !strings.Contains(session.UserCtx.DialogueSummary, "200万元") {
|
||
t.Errorf("expected preserved facts in summary snippet, got: %s", session.UserCtx.DialogueSummary)
|
||
}
|
||
|
||
// 验证会话级任务状态跟踪 (DST) 正常运转
|
||
if session.UserCtx.TaskState == nil {
|
||
t.Fatalf("expected TaskState to be populated on UserCtx")
|
||
}
|
||
if session.UserCtx.TaskState.TopicAnchor == "" {
|
||
t.Errorf("expected TopicAnchor in TaskState")
|
||
}
|
||
}
|
||
|
||
func TestChatSession_SemanticMemoryRecallAndAsyncTurnRecord(t *testing.T) {
|
||
if testing.Short() {
|
||
t.Skip("skipping async memory indexing test in short mode")
|
||
}
|
||
ctx := context.Background()
|
||
rec := intent.NewRecognizer()
|
||
engine := coordinator.NewCoordinatorEngine(nil)
|
||
ctxMgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.DefaultTimeDecayConfig())
|
||
|
||
// 1. 预置一条同一用户的既往历史会话切片
|
||
_, err := ctxMgr.RecordTurnMemory(
|
||
ctx,
|
||
"sess_past_recall",
|
||
3,
|
||
99,
|
||
200,
|
||
"YY 0505 电磁兼容检测需要提前准备几台样机?",
|
||
"通常需要准备至少 3 台工程样机,并配齐专用试验附件。",
|
||
"TECHNICAL_CONSULT",
|
||
"DIRECT_HIT",
|
||
true,
|
||
)
|
||
if err != nil {
|
||
t.Fatalf("seed past memory failed: %v", err)
|
||
}
|
||
|
||
userCtx := &coordinator.UserProjectContext{
|
||
SnapshotID: "sess_current_recall",
|
||
User: coordinator.UserInfo{
|
||
ID: "99",
|
||
Name: "赵工",
|
||
Role: "engineer",
|
||
ProjectRole: "composer",
|
||
},
|
||
Project: coordinator.ProjectInfo{
|
||
ID: "proj_200",
|
||
Name: "数字化高频超声吸引系统",
|
||
CurrentStage: bizdev.StageV01_Triage,
|
||
},
|
||
BlackboardFacts: make(map[string]string),
|
||
}
|
||
|
||
session := chat.NewSession("sess_current_recall", userCtx, rec, engine)
|
||
session.UserID = 99
|
||
session.ProjectID = 200
|
||
session.CtxMgr = ctxMgr
|
||
|
||
// 2. 发送带有历史回溯触发词的问答
|
||
resp, err := session.SendMessage(ctx, "我们之前讨论过 YY 0505 电磁兼容测试要几台样机吗?")
|
||
if err != nil {
|
||
t.Fatalf("SendMessage failed: %v", err)
|
||
}
|
||
if resp == nil {
|
||
t.Fatalf("expected non-nil response")
|
||
}
|
||
|
||
// 3. 验证是否成功命中按需召回
|
||
if len(session.UserCtx.RecalledMemories) == 0 {
|
||
t.Fatalf("expected RecalledMemories to be populated on UserCtx")
|
||
}
|
||
if !strings.Contains(session.UserCtx.RecalledMemorySnippet, "【相关历史片段】") {
|
||
t.Errorf("expected header in RecalledMemorySnippet, got: %s", session.UserCtx.RecalledMemorySnippet)
|
||
}
|
||
if !strings.Contains(session.UserCtx.RecalledMemorySnippet, "YY 0505") {
|
||
t.Errorf("expected snippet to contain YY 0505, got: %s", session.UserCtx.RecalledMemorySnippet)
|
||
}
|
||
|
||
// 4. 等待异步写入 goroutine 落盘
|
||
time.Sleep(100 * time.Millisecond)
|
||
|
||
// 5. 验证本轮当场生成的问答对是否已被向量记忆库索引
|
||
results, err := ctxMgr.SearchChatMemory(ctx, ctxmgr.ChatMemorySearchOptions{
|
||
Query: "电磁兼容样机数量",
|
||
UserID: 99,
|
||
ProjectID: 200,
|
||
TopK: 5,
|
||
MinScore: 0.1,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("SearchChatMemory failed: %v", err)
|
||
}
|
||
|
||
foundCurrentTurn := false
|
||
for _, r := range results {
|
||
if r.SessionID == "sess_current_recall" {
|
||
foundCurrentTurn = true
|
||
break
|
||
}
|
||
}
|
||
if !foundCurrentTurn {
|
||
t.Errorf("expected current turn to be asynchronously indexed into chat memory")
|
||
}
|
||
}
|
||
|
||
func TestChatSession_QueryRewrite_EllipsisAndPronoun(t *testing.T) {
|
||
ctx := context.Background()
|
||
rec := intent.NewRecognizer()
|
||
engine := coordinator.NewCoordinatorEngine(nil)
|
||
ctxMgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.DefaultTimeDecayConfig())
|
||
|
||
userCtx := &coordinator.UserProjectContext{
|
||
SnapshotID: "sess_rewrite_test",
|
||
User: coordinator.UserInfo{
|
||
ID: "101",
|
||
Name: "钱工",
|
||
Role: "engineer",
|
||
ProjectRole: "composer",
|
||
},
|
||
Project: coordinator.ProjectInfo{
|
||
ID: "proj_rewrite_1",
|
||
Name: "数字化高频超声吸引系统",
|
||
DeviceType: "高频超声手术刀",
|
||
CurrentStage: bizdev.StageV01_Triage,
|
||
},
|
||
BlackboardFacts: make(map[string]string),
|
||
}
|
||
|
||
session := chat.NewSession("sess_rewrite_test", userCtx, rec, engine)
|
||
session.UserID = 101
|
||
session.ProjectID = 301
|
||
session.CtxMgr = ctxMgr
|
||
|
||
// Turn 1: 提问完整独立句(建立 TopicAnchor)
|
||
_, err := session.SendMessage(ctx, "GB 9706.1 对我们这个器械适用吗?")
|
||
if err != nil {
|
||
t.Fatalf("turn 1 failed: %v", err)
|
||
}
|
||
if session.UserCtx.TaskState == nil || session.UserCtx.TaskState.TopicAnchor == "" {
|
||
t.Fatalf("expected TopicAnchor to be set on Turn 1")
|
||
}
|
||
|
||
// Turn 2: 省略句提问 "YY 0505 呢?"
|
||
_, err = session.SendMessage(ctx, "YY 0505 呢?")
|
||
if err != nil {
|
||
t.Fatalf("turn 2 failed: %v", err)
|
||
}
|
||
|
||
// 验证改写结果与规则命中
|
||
if session.UserCtx.RewrittenQuery == "" {
|
||
t.Errorf("expected RewrittenQuery to be set on Turn 2")
|
||
}
|
||
if !strings.Contains(session.UserCtx.RewrittenQuery, "YY 0505") || !strings.Contains(session.UserCtx.RewrittenQuery, "适用吗") {
|
||
t.Errorf("expected completed query with YY 0505 and predicate, got: %s", session.UserCtx.RewrittenQuery)
|
||
}
|
||
|
||
// 验证 Prompt 双呈现
|
||
composer := coordinator.NewMemoryComposer()
|
||
promptTurn2 := composer.ComposeDialoguePrompt("YY 0505 呢?", session.UserCtx)
|
||
if !strings.Contains(promptTurn2, "用户原话: YY 0505 呢?") {
|
||
t.Errorf("expected raw user input in prompt, got: %s", promptTurn2)
|
||
}
|
||
if !strings.Contains(promptTurn2, "结合上下文理解完整诉求:") {
|
||
t.Errorf("expected contextual explanation in prompt, got: %s", promptTurn2)
|
||
}
|
||
|
||
// Turn 3: 代词消解提问 "它的预算是多少?"
|
||
_, err = session.SendMessage(ctx, "它的预算是多少?")
|
||
if err != nil {
|
||
t.Fatalf("turn 3 failed: %v", err)
|
||
}
|
||
if !strings.Contains(session.UserCtx.RewrittenQuery, "数字化高频超声吸引系统") {
|
||
t.Errorf("expected pronoun resolution to project name, got: %s", session.UserCtx.RewrittenQuery)
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|