用户 2026-09-15 的订正:「agent 平台的 session 是和 path 绑定的,path+session 才能
指定到准确的 agent,而授权也是对 session 授权,而不是整个 agent」;随后报了一个
严重错投:「工作区在 TrueAgent 的 pi 客户端,发送邮件给 dsh 被投递给了工作区在 home
的客户端」。
根因是四处叠加(都有实测证据):
1. 解析只按别名:`WHERE session_alias = $1`,path 被当可选提示 ⇒ 两条不相干的
线索能落进同一会话;
2. 别名全局唯一 ⇒ path 在解析时冗余,进一步被忽略;
3. 桥按来信的 to_workspace 起 worker(实测日志 `新建 pi 会话 …(cwd=/home)`);
4. 投递(SSE)只按 name 广播。
本提交改 1/2 + 堵源头:
- 解析按 (path, 别名):给了 path 必须两者同时命中,对不上就是 ErrSessionNotFound
(**绝不**退回按别名找);没给 path 则要求该别名唯一,多条时 ErrSessionAmbiguous
(不许掷骰子);
- 唯一索引从 `(session_alias)` 改成 `(COALESCE(workspace,''), session_alias)`
(两方言 + 显式 DROP 旧索引;EnsureSessionAlias 靠唯一索引判撞名,自动变成按 path);
- 新 IsPlausibleWorkspace(与目录建议**同源**一条规则):地址受理处拒收不像工作目录的
path(`/home`、`/root`、`~/.pi/mail-sessions/…`、顶层挂载点),并给出可执行文案;
- 建议接口的候选过滤改用同一条规则(只留具体目录;祖先容器让位给更具体的候选)。
判据:新增/改写 4 组,全部做变异验证 —— 三条核心变异(解析退回按别名找 / 歧义时
掷骰子 / 去掉「深度≥2」)全部变红。过程中判据还抓到自己一个漏洞:
IsPlausibleWorkspace("/home") 在旧规则下是 true(/home 不是 root 的家目录)。
★ 语义变化(会让旧测试红):不同 path 下的同名别名现在是两条独立会话,不再"撞名"。
TestAdoptHandlesAliasCollision 已按新模型重写为两侧(同 path 才加后缀)。
未做:桥侧仍按来信 to_workspace 起 cwd(下一步);线上的库要等下一次启动才跑迁移。
248 lines
8.7 KiB
Go
248 lines
8.7 KiB
Go
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 自动命名时的撞名判据。
|
||
|
||
★ 语义在 2026-09-15 被用户订正过:「agent 平台的 session 是和 path 绑定的,
|
||
path+session 才能指定到准确的 agent」。所以别名唯一性是**按 path** 的:
|
||
|
||
- 不同工作目录下的同名别名 → 两条独立会话,**都保留原名**(不是撞名);
|
||
- 同一工作目录下的同名别名 → 才算撞名,追加 -2/-3… 后缀。
|
||
|
||
判据两侧都写:只验一侧的话,"永远加后缀"或"从不加后缀"都能骗过。
|
||
(后者的后果是同一目录里两条会话同号 —— 寻址会指到哪条就成了掷骰子。)
|
||
*/
|
||
func TestAdoptAliasCollisionIsScopedToPath(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)
|
||
}
|
||
|
||
// ① 不同 path:同名允许,两个字面相同
|
||
idB, err := AdoptPlatformSession(ctx, "pi", "pi-sess-b", taken, "/tmp/b", "标题")
|
||
if err != nil {
|
||
t.Fatalf("接管(不同 path): %v", err)
|
||
}
|
||
if got := SessionAliasOf(ctx, idB); got != taken {
|
||
t.Errorf("不同 path 下同名别名应原样保留(path+别名才是身份),实际 %q", got)
|
||
}
|
||
if pid := PlatformIDOf(ctx, idB); pid != "pi-sess-b" {
|
||
t.Errorf("platform_id = %q", pid)
|
||
}
|
||
|
||
// ② 同一 path:这才是真撞名,必须加后缀
|
||
idC, err := AdoptPlatformSession(ctx, "pi", "pi-sess-c", taken, "/tmp/a", "标题")
|
||
if err != nil {
|
||
t.Fatalf("接管(同 path): %v", err)
|
||
}
|
||
got := SessionAliasOf(ctx, idC)
|
||
if got == "" {
|
||
t.Fatal("接管后没有别名 —— 这条会话将无法寻址")
|
||
}
|
||
if got == taken {
|
||
t.Errorf("同一 path 下别名重复了:%q(同目录两条会话同号 = 寻址掷骰子)", got)
|
||
}
|
||
}
|