206 lines
6.9 KiB
Go
206 lines
6.9 KiB
Go
package ctxmgr
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
func TestContextManager_PreferenceReflection(t *testing.T) {
|
|
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
|
|
ctx := context.Background()
|
|
|
|
userID := int64(99)
|
|
sessionID := "sess_pref_test"
|
|
|
|
// 模拟用户发送了多次体现其工作习惯的消息
|
|
_ = mgr.RecordUserMessage(ctx, sessionID, userID, "user", "说重点,太长了不要废话,直接给结论。", "QUERY_PROGRESS", "PREEMPT", nil)
|
|
_ = mgr.RecordUserMessage(ctx, sessionID, userID, "user", "本次申报重点关注国内NMPA与欧洲CE合规,另外严格把控工期排期和成本预算。", "SUPPLEMENT_INFO", "EXPECTATION_HIT", nil)
|
|
_ = mgr.RecordUserMessage(ctx, sessionID, userID, "user", "文末请务必附带适合向高管汇报的 PPT 要点清单。", "SUPPLEMENT_INFO", "EXPECTATION_HIT", nil)
|
|
|
|
pref, err := mgr.GetUserPreference(ctx, userID)
|
|
if err != nil {
|
|
t.Fatalf("get user preference failed: %v", err)
|
|
}
|
|
|
|
// 1. 验证沟通风格是否被提炼为精炼结构化
|
|
if pref.CommunicationStyle != StyleConciseStructured {
|
|
t.Errorf("expected StyleConciseStructured, got %s", pref.CommunicationStyle)
|
|
}
|
|
|
|
// 2. 验证关注领域是否包含合规、预算和周期
|
|
hasReg := false
|
|
hasCost := false
|
|
hasTimeline := false
|
|
for _, f := range pref.FocusAreas {
|
|
if f == "reg_compliance" {
|
|
hasReg = true
|
|
}
|
|
if f == "cost_budget" {
|
|
hasCost = true
|
|
}
|
|
if f == "timeline" {
|
|
hasTimeline = true
|
|
}
|
|
}
|
|
if !hasReg || !hasCost || !hasTimeline {
|
|
t.Errorf("missing focus areas in preference: %v", pref.FocusAreas)
|
|
}
|
|
|
|
// 3. 验证输出格式偏好
|
|
if pref.OutputFormatPref != "ppt_bullets" {
|
|
t.Errorf("expected ppt_bullets, got %s", pref.OutputFormatPref)
|
|
}
|
|
|
|
// 4. 验证生成的个性化 Prompt 指导语
|
|
guidance := mgr.reflector.GeneratePromptGuidance(pref)
|
|
if !strings.Contains(guidance, "结构精炼、结论先行") {
|
|
t.Errorf("guidance missing concise style instruction: %s", guidance)
|
|
}
|
|
if !strings.Contains(guidance, "法规标准合规与审评审批风险") {
|
|
t.Errorf("guidance missing reg compliance focus: %s", guidance)
|
|
}
|
|
if !strings.Contains(guidance, "PPT") || !strings.Contains(guidance, "条目清单") {
|
|
t.Errorf("guidance missing ppt format instruction: %s", guidance)
|
|
}
|
|
}
|
|
|
|
func TestContextManager_TimeDecayActivities(t *testing.T) {
|
|
mgr := NewContextManager(nil, nil, TimeDecayConfig{
|
|
Enabled: true,
|
|
HalfLifeDays: 7.0,
|
|
MinWeightFloor: 0.10,
|
|
MilestoneExempt: true,
|
|
})
|
|
ctx := context.Background()
|
|
projectID := int64(888)
|
|
|
|
// 记录一项里程碑决策 (不衰减)
|
|
milestone, _ := mgr.RecordActivity(ctx, projectID, 1, "张工", "decision", "通过V0.5阶段可行性初审", nil, 1.0, true)
|
|
|
|
// 记录一项 14 天前的日常协同讨论 (经历 2 个半衰期)
|
|
oldOp, _ := mgr.RecordActivity(ctx, projectID, 2, "李工", "operation", "日常测试参数备选记录", nil, 1.0, false)
|
|
oldOp.CreatedAt = time.Now().Add(-14 * 24 * time.Hour)
|
|
|
|
// 更新内存中的活动列表时间以供测试排序
|
|
mgr.mu.Lock()
|
|
for i := range mgr.activities[projectID] {
|
|
if mgr.activities[projectID][i].ID == oldOp.ID {
|
|
mgr.activities[projectID][i].CreatedAt = oldOp.CreatedAt
|
|
}
|
|
}
|
|
mgr.mu.Unlock()
|
|
|
|
pCtx, err := mgr.GetProjectContext(ctx, projectID)
|
|
if err != nil {
|
|
t.Fatalf("get project context failed: %v", err)
|
|
}
|
|
|
|
if len(pCtx.AllActivities) < 2 {
|
|
t.Fatalf("expected 2 activities, got %d", len(pCtx.AllActivities))
|
|
}
|
|
|
|
// 里程碑事件排在最前且权重维持 1.0
|
|
if pCtx.AllActivities[0].ID != milestone.ID {
|
|
t.Errorf("expected milestone to rank highest, got ID %d", pCtx.AllActivities[0].ID)
|
|
}
|
|
if pCtx.AllActivities[0].DecayedWeight != 1.0 {
|
|
t.Errorf("expected milestone weight 1.0, got %.2f", pCtx.AllActivities[0].DecayedWeight)
|
|
}
|
|
|
|
// 14 天前的旧操作权重应衰减至 0.25 左右
|
|
if pCtx.AllActivities[1].DecayedWeight > 0.30 {
|
|
t.Errorf("expected old op decayed below 0.30, got %.3f", pCtx.AllActivities[1].DecayedWeight)
|
|
}
|
|
}
|
|
|
|
func TestContextManager_DatabaseIntegration(t *testing.T) {
|
|
dsn := os.Getenv("CONTEXT_DB_DSN")
|
|
if dsn == "" {
|
|
dsn = "postgres://app:dev_secret_db_pass@127.0.0.1:5433/agent_system_dev?sslmode=disable"
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
pool, err := pgxpool.New(ctx, dsn)
|
|
if err != nil {
|
|
t.Skipf("cannot connect to context db (%v), skipping DB test", err)
|
|
return
|
|
}
|
|
defer pool.Close()
|
|
|
|
if err := pool.Ping(ctx); err != nil {
|
|
t.Skipf("context db ping failed (%v), skipping DB test", err)
|
|
return
|
|
}
|
|
|
|
mgr := NewContextManager(pool, nil, DefaultTimeDecayConfig())
|
|
|
|
// 准备真实项目与用户
|
|
var projID int64
|
|
projCode := "PROJ_MGR_" + time.Now().Format("20060102150405")
|
|
err = pool.QueryRow(ctx, `
|
|
INSERT INTO projects (code, name, customer_name, device_type, current_stage)
|
|
VALUES ($1, '智能微创内窥镜', '水木创新中心', '内窥镜', 'V0.5_feasibility')
|
|
RETURNING id
|
|
`, projCode).Scan(&projID)
|
|
if err != nil {
|
|
t.Fatalf("insert project failed: %v", err)
|
|
}
|
|
|
|
var userID int64
|
|
accountID := "acc_mgr_" + time.Now().Format("150405")
|
|
err = pool.QueryRow(ctx, `
|
|
INSERT INTO users (account_id, name, role)
|
|
VALUES ($1, '首席法规官', 'regulatory')
|
|
RETURNING id
|
|
`, accountID).Scan(&userID)
|
|
if err != nil {
|
|
t.Fatalf("insert user failed: %v", err)
|
|
}
|
|
|
|
sessID := "sess_db_" + time.Now().Format("150405")
|
|
_ = mgr.RecordUserMessage(ctx, sessID, userID, "user", "请简短汇报,结论先行,关注NMPA注册进度和排期。", "QUERY_PROGRESS", "PREEMPT", nil)
|
|
_ = mgr.RecordUserMessage(ctx, sessID, userID, "assistant", "已为您梳理完成,当前已完成法规初筛。", "", "", nil)
|
|
|
|
_, err = mgr.RecordActivity(ctx, projID, userID, "首席法规官", "decision", "选定注册检验所并锁定型检方案", nil, 1.0, true)
|
|
if err != nil {
|
|
t.Fatalf("record activity in db failed: %v", err)
|
|
}
|
|
|
|
// 验证单用户上下文读取
|
|
uCtx, err := mgr.GetUserContext(ctx, projID, userID, sessID)
|
|
if err != nil {
|
|
t.Fatalf("get user context from db failed: %v", err)
|
|
}
|
|
if len(uCtx.RecentChats) < 2 {
|
|
t.Errorf("expected at least 2 chat messages in db, got %d", len(uCtx.RecentChats))
|
|
}
|
|
if uCtx.Preference.CommunicationStyle != StyleConciseStructured {
|
|
t.Errorf("expected preference StyleConciseStructured in db, got %s", uCtx.Preference.CommunicationStyle)
|
|
}
|
|
|
|
// 验证项目级上下文读取
|
|
pCtx, err := mgr.GetProjectContext(ctx, projID)
|
|
if err != nil {
|
|
t.Fatalf("get project context from db failed: %v", err)
|
|
}
|
|
if len(pCtx.MilestoneDecisions) == 0 {
|
|
t.Errorf("expected milestone decisions in db, got 0")
|
|
}
|
|
|
|
// 验证混合上下文装配
|
|
blended, err := mgr.BuildBlendedSquadContext(ctx, projID, userID, "feasibility_reg", "regulatory")
|
|
if err != nil {
|
|
t.Fatalf("build blended context failed: %v", err)
|
|
}
|
|
if !strings.Contains(blended.PersonalizedGuidance, "结构精炼、结论先行") {
|
|
t.Errorf("blended context missing user personalized guidance: %s", blended.PersonalizedGuidance)
|
|
}
|
|
}
|