package ctxmgr import ( "chat-agent-system/internal/db" "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) } // 5. 验证 CompilePromptGuidance 公共接口 compiledGuidance, err := mgr.CompilePromptGuidance(ctx, userID) if err != nil { t.Fatalf("CompilePromptGuidance failed: %v", err) } if compiledGuidance != guidance { t.Errorf("expected compiled guidance to match reflector output, got %s", compiledGuidance) } } 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 = db.SkipIfNoTestDatabaseURL(t) } 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 } if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil { t.Fatalf("migration failed: %v", err) } mgr := NewContextManager(pool, nil, DefaultTimeDecayConfig()) // 准备真实项目与用户 var projID int64 projCode := "PROJ_MGR_" + time.Now().Format("20060102150405") projName := "智能微创内窥镜-" + time.Now().Format("150405.000") err = pool.QueryRow(ctx, ` INSERT INTO projects (code, name, customer_name, device_type, current_stage) VALUES ($1, $2, '水木创新中心', '内窥镜', 'V0.5_feasibility') RETURNING id `, projCode, projName).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) } }