Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_memory_compact_test.go

220 lines
8.5 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 app
import (
"testing"
"time"
"yingce/backend/internal/model"
)
func TestAgentMemoryCompactMergesAndRewritesOwnedLessons(t *testing.T) {
s, db, _, _ := creationTestService(t)
if sqlDB, err := db.DB(); err == nil {
t.Cleanup(func() { _ = sqlDB.Close() })
}
first, err := s.CreateUserAgentMemory("user-a", AgentMemoryRequest{
Topic: "video.duration-a", Category: "video", Situation: "竖屏视频时长参数",
Lesson: "用模型目录返回的时长,不要猜秒数",
})
if err != nil {
t.Fatal(err)
}
second, err := s.CreateUserAgentMemory("user-a", AgentMemoryRequest{
Topic: "video.duration-b", Category: "video", Situation: "同样是竖屏视频时长",
Lesson: "时长只能用来自目录的合法值",
})
if err != nil {
t.Fatal(err)
}
rewrite, err := s.CreateUserAgentMemory("user-a", AgentMemoryRequest{
Topic: "canvas.snapshot", Category: "canvas", Situation: "连续写画布时报快照过期",
Lesson: "写成功后要用返回的新 snapshotHash 再写下一次",
})
if err != nil {
t.Fatal(err)
}
other, err := s.CreateUserAgentMemory("user-b", AgentMemoryRequest{
Topic: "video.duration-other", Category: "video", Situation: "别人的竖屏视频时长",
Lesson: "别人的记忆不能被合并进来",
})
if err != nil {
t.Fatal(err)
}
summary, err := s.applyAgentMemoryCompactPlan("user-a", agentMemoryCompactPlan{
Merges: []agentMemoryCompactMerge{{
IDs: []string{first.ID, second.ID, other.ID}, Topic: "video.duration", Category: "video",
Situation: "竖屏视频生成", Lesson: "只用模型目录返回的时长", Source: "compact",
}},
Rewrites: []agentMemoryCompactRewrite{{
ID: rewrite.ID, Topic: "canvas.snapshot-hash", Category: "canvas",
Situation: "连续写画布", Lesson: "每次写成功后用返回的新快照哈希", Source: "compact",
}},
})
if err != nil {
t.Fatal(err)
}
if summary.Merged != 0 || summary.Rewritten != 1 || summary.Removed != 0 || summary.Skipped != 1 {
t.Fatalf("含他人 id 的 merge 必须整组跳过,rewrite 仍可应用:%+v", summary)
}
summary, err = s.applyAgentMemoryCompactPlan("user-a", agentMemoryCompactPlan{
Merges: []agentMemoryCompactMerge{{
IDs: []string{first.ID, second.ID}, Topic: "video.duration", Category: "video",
Situation: "竖屏视频生成", Lesson: "只用模型目录返回的时长", Source: "compact",
}},
Rewrites: []agentMemoryCompactRewrite{{
ID: rewrite.ID, Topic: "canvas.snapshot-hash", Category: "canvas",
Situation: "连续写画布", Lesson: "每次写成功后用返回的新快照哈希", Source: "compact",
}},
})
if err != nil {
t.Fatal(err)
}
if summary.Merged != 1 || summary.Rewritten != 1 || summary.Removed != 1 {
t.Fatalf("应合并一条、改写一条、删除一条:%+v", summary)
}
owned, err := s.repo.ApprovedAgentLessons("user-a", 10)
if err != nil || len(owned) != 2 {
t.Fatalf("压缩后本人应剩 2 条:%+v(%v)", owned, err)
}
byID := map[string]model.AgentLesson{}
for _, lesson := range owned {
byID[lesson.ID] = lesson
}
if _, ok := byID[second.ID]; ok {
t.Fatalf("被合并的源记忆应删除:%+v", owned)
}
keep := byID[first.ID]
if keep.Topic != "video.duration" || keep.Lesson != "只用模型目录返回的时长" {
t.Fatalf("合并应写回保留的第一条:%+v", keep)
}
rewritten := byID[rewrite.ID]
if rewritten.Topic != "canvas.snapshot-hash" || rewritten.Lesson != "每次写成功后用返回的新快照哈希" {
t.Fatalf("改写应更新原文:%+v", rewritten)
}
others, err := s.repo.ApprovedAgentLessons("user-b", 10)
if err != nil || len(others) != 1 || others[0].ID != other.ID {
t.Fatalf("别人的记忆必须原样保留:%+v(%v)", others, err)
}
}
func TestNoteAgentMemoryCompactTaskRequiresMatchingTask(t *testing.T) {
s, db, _, _ := creationTestService(t)
if sqlDB, err := db.DB(); err == nil {
t.Cleanup(func() { _ = sqlDB.Close() })
}
first, err := s.CreateUserAgentMemory("user-a", AgentMemoryRequest{
Topic: "canvas.write-a", Category: "canvas", Situation: "写画布前先读状态",
Lesson: "先读后写,避免快照过期",
})
if err != nil {
t.Fatal(err)
}
second, err := s.CreateUserAgentMemory("user-a", AgentMemoryRequest{
Topic: "canvas.write-b", Category: "canvas", Situation: "写画布前先核对快照",
Lesson: "每次写之前都再读一次",
})
if err != nil {
t.Fatal(err)
}
setting, err := s.ensureAgentMemorySetting("user-a")
if err != nil {
t.Fatal(err)
}
setting.CompactTaskID = "task-real"
setting.LastStatus = model.AgentMemoryCompactStatusQueued
if err := s.repo.SaveAgentMemorySetting(setting); err != nil {
t.Fatal(err)
}
planText := `{"merges":[{"ids":["` + first.ID + `","` + second.ID + `"],"topic":"canvas.write","category":"canvas","situation":"写画布","lesson":"先读后写","source":"compact"}],"rewrites":[]}`
s.noteAgentMemoryCompactTask(model.Task{
ID: "task-other", UserID: "user-a", Operation: cloudAgentMemoryCompactOp,
}, map[string]any{"text": planText}, nil)
owned, err := s.repo.ApprovedAgentLessons("user-a", 10)
if err != nil || len(owned) != 2 {
t.Fatalf("任务 ID 对不上时不得应用压缩:%+v(%v)", owned, err)
}
s.noteAgentMemoryCompactTask(model.Task{
ID: "task-real", UserID: "user-a", Operation: cloudAgentMemoryCompactOp,
}, map[string]any{"text": "```json\n" + planText + "\n```"}, nil)
owned, err = s.repo.ApprovedAgentLessons("user-a", 10)
if err != nil || len(owned) != 1 || owned[0].ID != first.ID {
t.Fatalf("匹配任务成功后应合并:%+v(%v)", owned, err)
}
updated, err := s.repo.AgentMemorySetting("user-a")
if err != nil || updated.LastStatus != model.AgentMemoryCompactStatusSucceeded || updated.LastSummaryJSON == "" {
t.Fatalf("成功后应记下状态:%+v(%v)", updated, err)
}
}
func TestUpdateUserAgentMemorySettingNormalizesInterval(t *testing.T) {
s, db, _, _ := creationTestService(t)
if sqlDB, err := db.DB(); err == nil {
t.Cleanup(func() { _ = sqlDB.Close() })
}
if _, err := s.UpdateUserAgentMemorySetting("user-a", AgentMemorySettingRequest{CompactInterval: "yearly"}); err == nil {
t.Fatal("非法周期必须拒绝")
}
view, err := s.UpdateUserAgentMemorySetting("user-a", AgentMemorySettingRequest{
CompactInterval: "weekly", Model: "chan::gpt-text", ChannelID: "chan", ChannelModelKey: "gpt-text",
})
if err != nil {
t.Fatal(err)
}
if view.CompactInterval != model.AgentMemoryCompactIntervalWeekly || view.ChannelID != "chan" {
t.Fatalf("应保存周期和模型:%+v", view)
}
again, err := s.UpdateUserAgentMemorySetting("user-a", AgentMemorySettingRequest{CompactInterval: "daily"})
if err != nil {
t.Fatal(err)
}
if again.CompactInterval != model.AgentMemoryCompactIntervalDaily || again.ChannelID != "chan" || again.Model != "chan::gpt-text" {
t.Fatalf("只改周期时必须保留已选模型:%+v", again)
}
}
func TestAgentMemoryCompactDueAndBusy(t *testing.T) {
now := time.Date(2026, 9, 17, 12, 0, 0, 0, time.UTC)
stale := now.Add(-cloudAgentMemoryCompactStaleAfter - time.Minute)
recent := now.Add(-time.Minute)
if agentMemoryCompactDue(model.AgentMemorySetting{CompactInterval: model.AgentMemoryCompactIntervalOff}, now) {
t.Fatal("关闭定时后不应到期")
}
if !agentMemoryCompactDue(model.AgentMemorySetting{CompactInterval: model.AgentMemoryCompactIntervalDaily}, now) {
t.Fatal("从未压缩过的 daily 应到期")
}
last := now.Add(-2 * time.Hour)
if agentMemoryCompactDue(model.AgentMemorySetting{CompactInterval: model.AgentMemoryCompactIntervalDaily, LastCompactAt: &last}, now) {
t.Fatal("未满一天不应到期")
}
last = now.Add(-25 * time.Hour)
if !agentMemoryCompactDue(model.AgentMemorySetting{CompactInterval: model.AgentMemoryCompactIntervalDaily, LastCompactAt: &last}, now) {
t.Fatal("超过一天应到期")
}
if !agentMemoryCompactBusy(model.AgentMemorySetting{LastStatus: model.AgentMemoryCompactStatusQueued, CompactStartedAt: &recent}, now) {
t.Fatal("排队未超时应视为忙碌")
}
if agentMemoryCompactBusy(model.AgentMemorySetting{LastStatus: model.AgentMemoryCompactStatusQueued, CompactStartedAt: &stale}, now) {
t.Fatal("超过 30 分钟的排队应允许重试")
}
}
func TestParseAgentMemoryCompactPlanStripsFence(t *testing.T) {
plan, err := parseAgentMemoryCompactPlan("好的\n```json\n{\"rewrites\":[],\"merges\":[]}\n```\n")
if err != nil {
t.Fatal(err)
}
if len(plan.Rewrites) != 0 || len(plan.Merges) != 0 {
t.Fatalf("空计划应可解析:%+v", plan)
}
if _, err := parseAgentMemoryCompactPlan("不是 JSON"); err == nil {
t.Fatal("非 JSON 必须失败")
}
}