Files
Agent-System/internal/ctxmgr/manager_test.go
T
weichenandCursor 18a323c4dc test: cover asset raw round-trip and harden cross-user session upsert
Deny foreign user_id upsert without manage rights (403); unique project
names in ctxmgr PG fixture for global name index; add test-env data repair SQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 09:18:36 +00:00

216 lines
7.3 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)
}
// 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 = "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")
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)
}
}