Files
MailUI4Agents/server/internal/repo/suggest_paths_test.go
JianFeeeee ad1f3f14c3 fix(session): 会话身份 = (path, 别名) —— 修用户报的错投,并堵住 /home 这类毒 path
用户 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(下一步);线上的库要等下一次启动才跑迁移。
2026-09-15 12:20:28 +08:00

88 lines
3.4 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 (
"reflect"
"testing"
)
/*
SuggestPaths 的候选过滤(2026-09-15 用户报的「莫名其妙的 pi@/home 会话」)。
背景是一整条**自增强环**:建议列表是从历史数据学的(mails.to_workspace + 平台心跳 +
注册自报),而历史里混进了「进程恰好所在的目录」——`/root`、`/home`、`/home/program`、
`/root/.pi/mail-sessions/<uuid>`。用户点了第一条建议 → 发信给 `zcode@/home` →
那条会话的 workspace 成了 `/home` → 之后这条线索里所有参与方都显示 `xxx@/home`,
pi 桥还会**真把 worker 起在 /home**(实测日志 `新建 pi 会话 …(cwd=/home)`),
而沙箱 rw 只有 /home/program/agentmail ⇒ 那个 worker 连文件都写不了。
判据两侧都写:
- 坏值必须被剔掉(否则环继续跑);
- 好值必须保留,包括**还没创建的目录**(注册时自报的 /tmp/remotebot-ws 那种)
—— 如果实现顺手加上存在性检查,这一条会红。
*/
func TestFilterWorkspaces(t *testing.T) {
cases := []struct {
name string
in []string
want []string
}{
{
name: "剔掉进程家目录、家目录本身、父目录、隐藏段(实测到的污染形状)",
in: []string{
"/root", // 进程家目录
"/home", // 一切的家目录
"/home/program", // 只是容器
"/root/.pi/mail-sessions/8f056b73", // pi 的会话存储(隐藏段)
"/home/program/agentmail", // ← 唯一像工作目录的
},
want: []string{"/home/program/agentmail"},
},
{
name: "两个互不为祖先的项目目录都要保留",
in: []string{"/home/program/agentmail", "/home/program/llmsproxy"},
want: []string{"/home/program/agentmail", "/home/program/llmsproxy"},
},
{
name: "还没创建的目录不因为'不存在'被剔(存在性检查是过度过滤)",
in: []string{"/tmp/remotebot-ws", "/home/program/agentmail"},
want: []string{"/tmp/remotebot-ws", "/home/program/agentmail"},
},
{
name: "相对路径不是工作目录地址",
in: []string{"rel/path", "/home/program/agentmail"},
want: []string{"/home/program/agentmail"},
},
{
name: "尾斜杠 / 中间 . 段都做 Clean(不是当成新候选)",
in: []string{"/home/program/agentmail/"},
want: []string{"/home/program/agentmail"},
},
{
// 取舍:rule 4 让**更具体**的目录胜出。父子都是工作目录时父目录会被剔掉 ——
// 这是为治本(`/home`、`/home/program` 那种容器)付出的代价:宁可少给
// 一条候选(用户仍可手写),也不能把容器当工作目录(那会让 worker 起在
// 一个连文件都写不了的目录里)。这条判据把代价钉住,避免以后有人
// "顺手放宽"却没人知道为什么。
name: "父子都是候选时只留更具体的那个(已知取舍)",
in: []string{"/home/program/agentmail/./", "/home/program/agentmail/x"},
want: []string{"/home/program/agentmail/x"},
},
{
name: "空输入原样返回(不放大 nil/空切片差异)",
in: nil,
want: nil,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := filterWorkspaces(c.in)
if len(got) == 0 && len(c.want) == 0 {
return
}
if !reflect.DeepEqual(got, c.want) {
t.Errorf("filterWorkspaces(%v)\n got %v\nwant %v", c.in, got, c.want)
}
})
}
}