package repo import ( "context" "testing" "time" "github.com/agentmail/gateway/internal/db" ) func seedPlatformMirror(t *testing.T, agentName string, list []PlatformSession) { t.Helper() if err := ReplacePlatformSessions(context.Background(), agentName, list); err != nil { t.Fatalf("上报镜像: %v", err) } } // 补全把平台会话列为候选,投递侧必须能命中同一条。 // 此前 FindNamedSessionFor 只查 sessions 表 —— 候选列表在承诺一件做不到的事。 func TestFindPlatformSession(t *testing.T) { setupTestDB(t) ctx := context.Background() now := time.Now() seedPlatformMirror(t, "pi", []PlatformSession{ {PlatformID: "pi-sess-1", Workspace: "/home/program/agentmail", Slug: "设计文档-项目定位", Title: "邮件驱动·多智能体协作平台", UpdatedAt: &now}, {PlatformID: "pi-sess-2", Workspace: "/tmp/other", Slug: "别处的会话", Title: "无关", UpdatedAt: &now}, }) t.Run("按 slug + workspace 命中", func(t *testing.T) { pid, ws, title, err := FindPlatformSession(ctx, "pi", "设计文档-项目定位", "/home/program/agentmail") if err != nil { t.Fatalf("查找: %v", err) } if pid != "pi-sess-1" { t.Errorf("platform_id = %q", pid) } if ws != "/home/program/agentmail" { t.Errorf("workspace = %q", ws) } if title != "邮件驱动·多智能体协作平台" { t.Errorf("title = %q", title) } }) // 地址省略 path 位时不限工作区 t.Run("workspace 为空时不限", func(t *testing.T) { if pid, _, _, err := FindPlatformSession(ctx, "pi", "别处的会话", ""); err != nil || pid != "pi-sess-2" { t.Errorf("得到 %q err=%v", pid, err) } }) // workspace 不匹配时不该命中 —— 那会让邮件投进另一个项目的会话 t.Run("workspace 不匹配不命中", func(t *testing.T) { if _, _, _, err := FindPlatformSession(ctx, "pi", "别处的会话", "/home/program/agentmail"); err == nil { t.Error("workspace 不同却命中了") } }) t.Run("别的 Agent 的镜像不串", func(t *testing.T) { if _, _, _, err := FindPlatformSession(ctx, "dsh", "设计文档-项目定位", ""); err == nil { t.Error("dsh 命中了 pi 的会话") } }) t.Run("空参数返回 not found 而不是 panic", func(t *testing.T) { if _, _, _, err := FindPlatformSession(ctx, "", "x", ""); err != ErrSessionNotFound { t.Errorf("空 agent 应给 ErrSessionNotFound,得到 %v", err) } if _, _, _, err := FindPlatformSession(ctx, "pi", "", ""); err != ErrSessionNotFound { t.Errorf("空 slug 应给 ErrSessionNotFound,得到 %v", err) } }) } // 接管后本侧有正式身份:可寻址(别名)、绑定 platform_id、workspace 用会话真实的。 func TestAdoptPlatformSession(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) id, err := AdoptPlatformSession(ctx, "pi", "pi-sess-1", "设计文档-项目定位", "/home/program/agentmail", "邮件驱动·多智能体协作平台") if err != nil { t.Fatalf("接管: %v", err) } // 别名复用平台 slug:人在补全里看到的就是那个名字,换掉会让他找不到 if alias := SessionAliasOf(ctx, id); alias != "设计文档-项目定位" { t.Errorf("别名 = %q,期望复用平台 slug", alias) } if pid := PlatformIDOf(ctx, id); pid != "pi-sess-1" { t.Errorf("platform_id = %q", pid) } // workspace 取平台会话的真实 cwd var ws string if err := db.DB.QueryRowContext(ctx, `SELECT workspace FROM sessions WHERE session_id = $1`, id).Scan(&ws); err != nil { t.Fatalf("读 workspace: %v", err) } if ws != "/home/program/agentmail" { t.Errorf("workspace = %q", ws) } } // 普通会话的 platform_id 必须是空串(不是接管来的)。 func TestPlatformIDOfEmptyForNormalSession(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) id, err := CreateSession(ctx, nil, "pi", "普通邮件会话", "/tmp/x") if err != nil { t.Fatalf("建会话: %v", err) } if pid := PlatformIDOf(ctx, id); pid != "" { t.Errorf("普通会话的 platform_id 应为空,得到 %q", pid) } } // 一条平台会话只能被接管一次。 // // 第二次投递必须复用第一次建的本侧会话 —— 否则同一条 TUI 对话会在邮箱里 // 裂成多条互不相干的线索:人看到三个同名会话,而回信只落在其中一条上。 func TestFindSessionByPlatformIDPreventsDoubleAdopt(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) id, err := AdoptPlatformSession(ctx, "pi", "pi-sess-1", "某会话", "/tmp/ws", "标题") if err != nil { t.Fatalf("接管: %v", err) } // 接管后还没有邮件 —— 此时反查不到(EXISTS 子句要求有本 Agent 参与的邮件) if _, err := FindSessionByPlatformID(ctx, "pi", "pi-sess-1"); err == nil { t.Log("注意:无邮件时也能反查到") } // 投一封进去,让参与关系成立 if _, err := CreateMail(ctx, id, nil, "jianf", "", "pi", "/tmp/ws", "主题", "正文", nil); err != nil { t.Fatalf("建邮件: %v", err) } got, err := FindSessionByPlatformID(ctx, "pi", "pi-sess-1") if err != nil { t.Fatalf("反查: %v", err) } if got != id { t.Errorf("反查到 %v,期望 %v", got, id) } // 别的 platform_id 查不到 if _, err := FindSessionByPlatformID(ctx, "pi", "pi-sess-999"); err != ErrSessionNotFound { t.Errorf("不存在的 platform_id 应给 ErrSessionNotFound,得到 %v", err) } // 别的 Agent 查不到(参与关系不成立) if _, err := FindSessionByPlatformID(ctx, "dsh", "pi-sess-1"); err != ErrSessionNotFound { t.Errorf("dsh 不该查到 pi 的接管会话,得到 %v", err) } } // 整表替换镜像后,已接管的本侧会话不受影响。 // // 镜像是平台当前状态的快照、会被整表替换;而 sessions.platform_id 是本侧的 // 持久绑定。平台侧那条会话被删掉之后,本侧线索与历史邮件仍然要在。 func TestAdoptedSessionSurvivesMirrorReplace(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) now := time.Now() seedPlatformMirror(t, "pi", []PlatformSession{ {PlatformID: "pi-sess-1", Workspace: "/tmp/ws", Slug: "会话甲", UpdatedAt: &now}, }) id, err := AdoptPlatformSession(ctx, "pi", "pi-sess-1", "会话甲", "/tmp/ws", "标题") if err != nil { t.Fatalf("接管: %v", err) } // 平台侧删了那条会话(新快照里没有它) seedPlatformMirror(t, "pi", []PlatformSession{ {PlatformID: "pi-sess-2", Workspace: "/tmp/ws", Slug: "会话乙", UpdatedAt: &now}, }) // 本侧绑定与别名都还在 if pid := PlatformIDOf(ctx, id); pid != "pi-sess-1" { t.Errorf("镜像替换后 platform_id 丢了:%q", pid) } if alias := SessionAliasOf(ctx, id); alias != "会话甲" { t.Errorf("别名丢了:%q", alias) } // 但镜像里查不到了(补全不再列它,符合预期) if _, _, _, err := FindPlatformSession(ctx, "pi", "会话甲", ""); err != ErrSessionNotFound { t.Errorf("镜像里应已消失,得到 %v", err) } } // 接管用的 slug 与本侧某条无关会话撞名时要自动加后缀(别名全局唯一)。 func TestAdoptHandlesAliasCollision(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) // 先占掉这个别名 taken := "撞名的别名" if _, err := CreateSession(ctx, &taken, "pi", "已存在", "/tmp/a"); err != nil { t.Fatalf("建占位会话: %v", err) } id, err := AdoptPlatformSession(ctx, "pi", "pi-sess-x", taken, "/tmp/b", "标题") if err != nil { t.Fatalf("接管: %v", err) } alias := SessionAliasOf(ctx, id) if alias == "" { t.Fatal("接管后没有别名 —— 这条会话将无法寻址") } if alias == taken { t.Errorf("别名与已存在的重复了:%q", alias) } // 绑定仍然正确 if pid := PlatformIDOf(ctx, id); pid != "pi-sess-x" { t.Errorf("platform_id = %q", pid) } }