package ctxmgr_test import ( "context" "strings" "testing" "time" "chat-agent-system/internal/ctxmgr" ) func TestRecordTurnMemory_IdempotentAndWeight(t *testing.T) { mgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.DefaultTimeDecayConfig()) ctx := context.Background() // 1. 记录普通闲聊轮次 id1, err := mgr.RecordTurnMemory( ctx, "sess_test_1", 1, 1001, 2001, "你好,帮我看一下项目情况", "好的,当前项目处于三角验证阶段...", "CASUAL_CHAT", "DIRECT_HIT", false, ) if err != nil { t.Fatalf("RecordTurnMemory failed: %v", err) } if id1 <= 0 { t.Errorf("expected valid chunk ID, got %d", id1) } // 2. 幂等更新同一轮次(例如流式补充更新或重试) id2, err := mgr.RecordTurnMemory( ctx, "sess_test_1", 1, 1001, 2001, "你好,帮我看一下项目情况", "好的,当前项目处于三角验证阶段,已为您整理待办事项。", "CASUAL_CHAT", "DIRECT_HIT", false, ) if err != nil { t.Fatalf("RecordTurnMemory retry failed: %v", err) } if id2 != id1 { t.Errorf("expected idempotent chunk id %d, got %d", id1, id2) } // 3. 记录里程碑/决策轮次 idMilestone, err := mgr.RecordTurnMemory( ctx, "sess_test_1", 2, 1001, 2001, "确认器械类型为超声手术设备,预算定为150万", "已记录器械类型和预算约束,并同步到项目黑板。", "CONFIRM_ACTION", "DIRECT_HIT", true, ) if err != nil { t.Fatalf("RecordTurnMemory milestone failed: %v", err) } if idMilestone <= id1 { t.Errorf("expected new chunk id > %d, got %d", id1, idMilestone) } } func TestSearchChatMemory_PrivacyGate(t *testing.T) { mgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.DefaultTimeDecayConfig()) ctx := context.Background() // 用户 A (ID: 101) 的私有会话 _, err := mgr.RecordTurnMemory( ctx, "sess_userA_priv", 1, 101, 2001, "用户A私密提问:超声换能器设计工艺有缺陷怎么处理", "建议重新核对压电陶瓷匹配层厚度与阻抗...", "TECHNICAL_CONSULT", "DIRECT_HIT", false, ) if err != nil { t.Fatalf("record user A private failed: %v", err) } // 用户 A (ID: 101) 的项目公开里程碑会话 _, err = mgr.RecordTurnMemoryWithVisibility( ctx, "sess_userA_pub", 2, 101, 2001, "GB 9706.1 适用性确认完成,正式进入V0.5阶段", "项目里程碑:标准适用性评估完成,全员可见。", "EXECUTE_STAGE_ADVANCE", "DIRECT_HIT", true, "project_public", ) if err != nil { t.Fatalf("record user A public failed: %v", err) } // 用户 B (ID: 102) 的私有会话 _, err = mgr.RecordTurnMemory( ctx, "sess_userB_priv", 1, 102, 2001, "用户B私有提问:GB 9706.1 绝缘耐压指标是多少", "按照 GB 9706.1 标准要求,应用部分需耐受 4000V 试验电压...", "TECHNICAL_CONSULT", "DIRECT_HIT", false, ) if err != nil { t.Fatalf("record user B private failed: %v", err) } // Case 1: 用户 B 检索 "超声换能器" (用户 A 的私密话题) resultsB, err := mgr.SearchChatMemory(ctx, ctxmgr.ChatMemorySearchOptions{ Query: "超声换能器设计工艺", UserID: 102, ProjectID: 2001, TopK: 3, MinScore: 0.1, }) if err != nil { t.Fatalf("SearchChatMemory for user B failed: %v", err) } // 绝对不能查出用户 A 的私有对话 for _, r := range resultsB { if r.SessionID == "sess_userA_priv" || strings.Contains(r.Content, "用户A私密提问") { t.Errorf("PRIVACY VIOLATION: User B recalled User A's private memory: %s", r.Content) } } // Case 2: 用户 B 检索 "GB 9706.1" // 用户 B 应当能查出:1) 自己的私有提问; 2) 用户 A 的公开里程碑 resultsBStandards, err := mgr.SearchChatMemory(ctx, ctxmgr.ChatMemorySearchOptions{ Query: "GB 9706.1 标准与适用性", UserID: 102, ProjectID: 2001, TopK: 3, MinScore: 0.1, }) if err != nil { t.Fatalf("SearchChatMemory failed: %v", err) } foundOwn := false foundPublic := false for _, r := range resultsBStandards { if r.SessionID == "sess_userB_priv" { foundOwn = true if r.IsOtherUser { t.Errorf("own turn should not have IsOtherUser=true") } } if r.SessionID == "sess_userA_pub" { foundPublic = true if !r.IsOtherUser { t.Errorf("other user's public turn should have IsOtherUser=true") } } } if !foundOwn { t.Errorf("expected User B to recall their own chat memory") } if !foundPublic { t.Errorf("expected User B to recall project_public memory from User A") } // Case 3: 用户 A 检索 "超声换能器" // 用户 A 可以正常查出自己的私有提问 resultsA, err := mgr.SearchChatMemory(ctx, ctxmgr.ChatMemorySearchOptions{ Query: "超声换能器设计工艺", UserID: 101, ProjectID: 2001, TopK: 3, MinScore: 0.1, }) if err != nil { t.Fatalf("SearchChatMemory for user A failed: %v", err) } foundAOwn := false for _, r := range resultsA { if r.SessionID == "sess_userA_priv" { foundAOwn = true } } if !foundAOwn { t.Errorf("expected User A to recall their own private memory") } } func TestSearchChatMemory_ExcludeSessionID(t *testing.T) { mgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.DefaultTimeDecayConfig()) ctx := context.Background() _, _ = mgr.RecordTurnMemory(ctx, "current_session", 1, 101, 2001, "测试同会话第一轮", "回复1", "CASUAL_CHAT", "", false) _, _ = mgr.RecordTurnMemory(ctx, "past_session", 1, 101, 2001, "测试历史会话第一轮", "回复2", "CASUAL_CHAT", "", false) // 排除当前会话 results, err := mgr.SearchChatMemory(ctx, ctxmgr.ChatMemorySearchOptions{ Query: "测试会话第一轮", UserID: 101, ProjectID: 2001, ExcludeSessionID: "current_session", TopK: 5, MinScore: 0.1, }) if err != nil { t.Fatalf("SearchChatMemory failed: %v", err) } for _, r := range results { if r.SessionID == "current_session" { t.Errorf("expected current_session to be excluded, but found in results") } } } func TestFormatRecalledMemorySnippet(t *testing.T) { now := time.Date(2026, 9, 10, 10, 0, 0, 0, time.UTC) items := []ctxmgr.RecalledMemoryItem{ { SessionID: "sess_cur", TurnNo: 9, Content: "用户: GB 9706.1 对超声设备适用吗?\n助手: 适用,另需满足 YY 9706.102 EMC 规范。", CreatedAt: now, IsOtherUser: false, SimilarityScore: 0.95, }, { SessionID: "sess_past", TurnNo: 3, Content: "用户: 客户预算通常是多少\n助手: 通常按千万级申报,但一期需严格控制在150万内。", CreatedAt: now.Add(-2 * 24 * time.Hour), IsOtherUser: false, SimilarityScore: 0.88, }, { SessionID: "sess_other", TurnNo: 5, AuthorName: "李工", Content: "用户: V0.1需求收集表终审结论\n助手: 需求收集表终审通过,进入框架阶段。", CreatedAt: now.Add(-4 * 24 * time.Hour), IsOtherUser: true, SimilarityScore: 0.82, }, } snippet := ctxmgr.FormatRecalledMemorySnippet(items, "sess_cur") if !strings.Contains(snippet, "【相关历史片段】") { t.Fatalf("expected header not found") } if !strings.Contains(snippet, "[本会话·第9轮]") { t.Errorf("expected [本会话·第9轮] tag") } if !strings.Contains(snippet, "[历史会话·2026-09-08]") { t.Errorf("expected [历史会话·2026-09-08] tag") } if !strings.Contains(snippet, "[项目公开·2026-09-06] (作者: 李工)") { t.Errorf("expected project public tag with author") } if !strings.Contains(snippet, "不得评价个人") { t.Errorf("expected privacy disclaimer for other user's snippet") } }