Files
MailUI4Agents/server/internal/repo/adopt_alias_test.go

275 lines
10 KiB
Go
Raw 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 repo
import (
"context"
"testing"
"time"
"github.com/agentmail/gateway/internal/db"
)
// ─── 接管会话的别名不可被平台命名同步覆盖 ───
//
// 锁的是一次生产事故的**第二环**(第一环是调度器漏 platform_session_id
//
// 12:12 人选中补全里的「项目定位」→ 接管平台会话 01a05a5e本侧建 26e26477
// 12:20 日历提醒省略 session 位 → 落进 26e26477第三环见 TestDefaultSession...
// 12:20 插件收不到 platform_session_id → 另开一条 pi 会话
// 12:20 那条新会话的名字经 /sessions/{id}/sync 回写
// → SyncSessionAlias 把 26e26477 的别名冲成「日程提醒:…」
//
// 结果:人在补全里选的名字凭空消失,同一条会话在候选列表里出现两次
// (一次用被冲掉的别名、一次用镜像里的原始 slug而另一条真实会话被
// 按别名字符串去重吃掉了。
//
// 接管会话的别名是**人从补全里选中的平台 slug**,任何平台命名同步都不该动它。
func TestSyncSessionAliasNeverOverwritesAdoptedAlias(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id, err := AdoptPlatformSession(ctx, "pi", "01a05a5e", "项目定位", "/home/program/agentmail", "标题")
if err != nil {
t.Fatalf("接管: %v", err)
}
if got := SessionAliasOf(ctx, id); got != "项目定位" {
t.Fatalf("接管后别名 = %q期望 项目定位", got)
}
// 平台侧同步一个完全不同的名字(生产上就是日历提醒的主题)
final, err := SyncSessionAlias(ctx, id, "日程提醒:小宅自测")
if err != nil {
t.Fatalf("SyncSessionAlias: %v", err)
}
if final != "项目定位" {
t.Errorf("同步返回 %q —— 接管会话的别名不该被改", final)
}
if got := SessionAliasOf(ctx, id); got != "项目定位" {
t.Errorf("库里别名变成了 %q —— 人在补全里选的名字被冲掉了", got)
}
}
// 普通(非接管)会话仍然接受平台命名同步 —— 别名复用平台命名是既定决策,
// 上面那道门不能把它一起关掉。
func TestSyncSessionAliasStillWorksForNormalSession(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id, err := CreateSession(ctx, nil, "pi", "邮件驱动的会话", "/tmp/ws")
if err != nil {
t.Fatalf("建会话: %v", err)
}
if _, err := EnsureSessionAlias(ctx, id, "pi-初始别名"); err != nil {
t.Fatalf("EnsureSessionAlias: %v", err)
}
final, err := SyncSessionAlias(ctx, id, "平台生成的名字")
if err != nil {
t.Fatalf("SyncSessionAlias: %v", err)
}
if final != "平台生成的名字" {
t.Errorf("普通会话应当接受同步,得到 %q", final)
}
}
// 接管会话**没有**别名时理论上不会发生AdoptPlatformSession 一定给一个)
// 仍然允许写入 —— 否则那条会话永远无法寻址。
func TestSyncSessionAliasFillsEmptyAdoptedAlias(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id, err := CreateSession(ctx, nil, "pi", "标题", "/tmp/ws")
if err != nil {
t.Fatalf("建会话: %v", err)
}
// 手工造出「有 platform_id 但无别名」的状态
if _, err := db.DB.ExecContext(ctx,
`UPDATE sessions SET platform_id = 'pid-x' WHERE session_id = $1`, id); err != nil {
t.Fatalf("置 platform_id: %v", err)
}
final, err := SyncSessionAlias(ctx, id, "补上一个名字")
if err != nil {
t.Fatalf("SyncSessionAlias: %v", err)
}
if final != "补上一个名字" {
t.Errorf("无别名的接管会话应当允许写入,得到 %q", final)
}
}
// ─── 接管会话不是「默认会话」 ───
//
// 事故的**第三环**:日历提醒的收件地址省略 session 位(`homeagent` 而不是
// `homeagent@/x.某会话`),走 FindOrCreateDefaultSession。它原来只按
// 「参与过 + workspace 匹配 + 未归档」挑最近活跃的一条 —— 于是挑中了人
// 刚刚显式指定的那条接管会话。
//
// 接管会话是人**点名**要谈的一条线索,不该被省略 session 位的邮件当默认会话。
func TestFindOrCreateDefaultSessionSkipsAdoptedSessions(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
// 一条接管会话,且有邮件(满足 EXISTS 条件)
adopted, err := AdoptPlatformSession(ctx, "pi", "pid-adopted", "人选的线索", "/home/program/agentmail", "标题")
if err != nil {
t.Fatalf("接管: %v", err)
}
if _, err := CreateMail(ctx, adopted, nil, "jianf", "", "pi", "/home/program/agentmail",
"人发的第一封", "正文", nil); err != nil {
t.Fatalf("建邮件: %v", err)
}
// 省略 session 位投递 → 不该落进那条接管会话
got, err := FindOrCreateDefaultSession(ctx, "pi", "/home/program/agentmail", "calendar", "日程提醒")
if err != nil {
t.Fatalf("FindOrCreateDefaultSession: %v", err)
}
if got == adopted {
t.Error("省略 session 位的邮件落进了接管会话 —— 那是人显式指定的线索")
}
// 该新建一条,且它不带 platform_id
if pid := PlatformIDOf(ctx, got); pid != "" {
t.Errorf("新建的默认会话不该有 platform_id得到 %q", pid)
}
}
// 普通会话仍然可以作为默认会话被复用 —— 上面那道门不能把它一起关掉,
// 否则每封省略 session 位的邮件都会新开一条会话。
func TestFindOrCreateDefaultSessionStillReusesNormalSession(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
first, err := FindOrCreateDefaultSession(ctx, "pi", "/tmp/ws", "jianf", "第一封")
if err != nil {
t.Fatalf("第一次: %v", err)
}
if _, err := CreateMail(ctx, first, nil, "jianf", "", "pi", "/tmp/ws", "第一封", "正文", nil); err != nil {
t.Fatalf("建邮件: %v", err)
}
second, err := FindOrCreateDefaultSession(ctx, "pi", "/tmp/ws", "jianf", "第二封")
if err != nil {
t.Fatalf("第二次: %v", err)
}
if second != first {
t.Error("普通默认会话应当被复用,否则每封省略 session 位的邮件都开新会话")
}
}
// ─── 候选列表按 platform_id 去重 ───
//
// 事故的**第四环**`SuggestSessionCandidates` 的去重只比别名字符串。
// 别名一被冲掉,同一条会话就在列表里出现两次:
//
// 候选 1 日程提醒:…(被冲掉的别名) source=mail ← 26e26477
// 候选 2 项目定位(镜像里的原始 slug source=platform ← 也是 26e26477
//
// 更糟的是**另一条真实会话被吃掉了**:它的 slug 恰好等于候选 1 那个
// 被冲掉的别名,于是 `seen[slug]` 命中、被 continue 跳过。
// 人在界面上看到两条,实际只有一条能选,而第三条不存在于列表里。
func TestSuggestSessionCandidatesDedupesByPlatformID(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedPlatformAgent(t, "pi")
// 接管一条平台会话
adopted, err := AdoptPlatformSession(ctx, "pi", "01a05a5e", "项目定位", "/home/program/agentmail", "标题")
if err != nil {
t.Fatalf("接管: %v", err)
}
if _, err := CreateMail(ctx, adopted, nil, "jianf", "", "pi", "/home/program/agentmail",
"主题", "正文", nil); err != nil {
t.Fatalf("建邮件: %v", err)
}
// 镜像里同时有它与另一条真实会话
now := time.Now()
if err := ReplacePlatformSessions(ctx, "pi", []PlatformSession{
{PlatformID: "01a05a5e", Workspace: "/home/program/agentmail", Slug: "项目定位",
Title: "项目定位", MailDriven: true, UpdatedAt: &now},
{PlatformID: "01a06aa5", Workspace: "/home/program/agentmail", Slug: "另一条真实会话",
Title: "另一条", MailDriven: true, UpdatedAt: &now},
}); err != nil {
t.Fatalf("上报镜像: %v", err)
}
got, err := SuggestSessionCandidates(ctx, "jianf", "pi", "/home/program/agentmail")
if err != nil {
t.Fatalf("SuggestSessionCandidates: %v", err)
}
// 应当恰好两条接管那条mail 来源)+ 另一条真实会话platform 来源)
if len(got) != 2 {
t.Fatalf("应有 2 个候选,实际 %d%+v", len(got), got)
}
byAlias := map[string]SessionCandidate{}
for _, c := range got {
byAlias[c.Alias] = c
}
if c, ok := byAlias["项目定位"]; !ok {
t.Error("接管会话应当在候选里")
} else if c.Source != "mail" {
t.Errorf("接管会话的来源应是 mail保证送得到得到 %q", c.Source)
}
if c, ok := byAlias["另一条真实会话"]; !ok {
t.Error("另一条真实会话被吃掉了 —— 那正是 bug 的表现")
} else if c.Source != "platform" {
t.Errorf("未接管的平台会话来源应是 platform得到 %q", c.Source)
}
}
// 别名被冲掉之后也不该出现重复项。
//
// 这是事故现场的**精确复现**:本侧别名与镜像 slug 不一致(别名被另一条会话的
// 命名同步冲掉了),此时按别名字符串去重必然漏,只有按 platform_id 才对。
func TestSuggestSessionCandidatesNoDupWhenAliasDiverged(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedPlatformAgent(t, "pi")
adopted, err := AdoptPlatformSession(ctx, "pi", "01a05a5e", "项目定位", "/home/program/agentmail", "标题")
if err != nil {
t.Fatalf("接管: %v", err)
}
if _, err := CreateMail(ctx, adopted, nil, "jianf", "", "pi", "/home/program/agentmail",
"主题", "正文", nil); err != nil {
t.Fatalf("建邮件: %v", err)
}
// 模拟别名被冲掉(绕过 SyncSessionAlias 的守卫直接改库 ——
// 存量数据里可能已经有这种状态)
if _, err := db.DB.ExecContext(ctx,
`UPDATE sessions SET session_alias = $1 WHERE session_id = $2`,
"日程提醒:小宅自测", adopted); err != nil {
t.Fatalf("改别名: %v", err)
}
now := time.Now()
if err := ReplacePlatformSessions(ctx, "pi", []PlatformSession{
{PlatformID: "01a05a5e", Workspace: "/home/program/agentmail", Slug: "项目定位",
Title: "项目定位", MailDriven: true, UpdatedAt: &now},
}); err != nil {
t.Fatalf("上报镜像: %v", err)
}
got, err := SuggestSessionCandidates(ctx, "jianf", "pi", "/home/program/agentmail")
if err != nil {
t.Fatalf("SuggestSessionCandidates: %v", err)
}
// 只有一条会话,就该只有一个候选 —— 别名分叉不该让它变成两个
if len(got) != 1 {
t.Fatalf("同一条会话应只有 1 个候选,实际 %d%+v", len(got), got)
}
if got[0].Source != "mail" {
t.Errorf("应保留 mail 来源(它保证送得到),得到 %q", got[0].Source)
}
}
// 存量数据里可能已经有这种状态(守卫是后加的)。