Files
weichenandCursor 3f42149457 feat(bizdev): snapshot collaborators onto Runtime and Server ports
Composition roots copy Parts once so HTTP/CLI handlers stop calling locator getters on every request.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 13:00:33 +00:00

630 lines
22 KiB
Go
Raw Permalink 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 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 := bizdev.Snapshot(svc).Ctx.IndexApprovedDocument(ctx, doc, body)
if err != nil {
t.Fatalf("IndexApprovedDocument failed: %v", err)
}
// 2. 发起新会话咨询
rec := intent.NewRecognizer()
engine := coordinator.NewCoordinatorEngine(svc)
engine.SetContextServices(bizdev.Snapshot(svc).Ctx, bizdev.Snapshot(svc).KB)
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 = bizdev.Snapshot(svc).Ctx
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 落盘(轮询至 deadline,避免固定 sleep 竞态)
deadline := time.Now().Add(2 * time.Second)
var results []ctxmgr.RecalledMemoryItem
foundCurrentTurn := false
for time.Now().Before(deadline) {
var searchErr error
results, searchErr = ctxMgr.SearchChatMemory(ctx, ctxmgr.ChatMemorySearchOptions{
Query: "电磁兼容样机数量",
UserID: 99,
ProjectID: 200,
TopK: 5,
MinScore: 0.1,
})
if searchErr != nil {
t.Fatalf("SearchChatMemory failed: %v", searchErr)
}
for _, r := range results {
if r.SessionID == "sess_current_recall" {
foundCurrentTurn = true
break
}
}
if foundCurrentTurn {
break
}
time.Sleep(20 * time.Millisecond)
}
// 5. 验证本轮当场生成的问答对是否已被向量记忆库索引
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)
}
}