Files
MailUI4Agents/server/internal/repo/session_identity_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

114 lines
4.8 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"
"errors"
"testing"
)
/*
会话身份 = **path + 别名**(用户 2026-09-15 订正)。
原话:「agent 平台的 session 是和 path 绑定的,path+session 才能指定到准确的 agent,
而授权也是对 session 授权,而不是整个 agent」。
这条判据直接对着当天那个**严重错投**:工作区在 TrueAgent 的 pi 给 dsh 发信,信却进了
workspace=/home 的会话/客户端。根因是解析只按别名(`WHERE session_alias = $1`),
path 被当成了可选提示 —— 于是两条不相干的线索能落进同一条会话。
判据两侧都写(只验一侧会挡住"退回按别名找"这种假修复):
- 给了 **正确** path → 命中;
- 给了 **另一个** path(而别名确实存在于别处)→ 必须 ErrSessionNotFound,
**绝不能**命中共名的那一条(这一条是本次 bug 的回归判据);
- 不给 path 且别名唯一 → 命中(兼容 `name@.别名` 的旧地址);
- 不给 path 且同名两条 → ErrSessionAmbiguous(不许掷骰子)。
*/
func TestFindNamedSessionForRequiresPath(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "dsh", 20)
alias := "鸿蒙客户端与-WebUI-界面对齐"
a := alias
idA, err := CreateSession(ctx, &a, "pi", "在 repo 里干活", "/home/program/agentmail")
if err != nil {
t.Fatalf("建会话 A: %v", err)
}
seedMailForSession(t, idA, "pi", "dsh", "/home/program/agentmail")
b := alias
idB, err := CreateSession(ctx, &b, "pi", "在 /home 里那一条(污染值)", "/home")
if err != nil {
t.Fatalf("建会话 B(同名不同 path): %v", err)
}
seedMailForSession(t, idB, "pi", "dsh", "/home")
// ① 正确 path → 命中 A
got, err := FindNamedSessionFor(ctx, "dsh", "/home/program/agentmail", alias)
if err != nil || got != idA {
t.Fatalf("正确 path 应命中 A:got=%v err=%v", got, err)
}
// ② ★ 错 path(/home)但别名相同 → 必须命中 B,**不是** A;
// 而如果那个 path 根本没有会话 → NotFound,绝不退回命中 A。
got, err = FindNamedSessionFor(ctx, "dsh", "/tmp/别的地方", alias)
if !errors.Is(err, ErrSessionNotFound) {
t.Fatalf("错 path 必须 ErrSessionNotFound(不许退回按别名找),实际 got=%v err=%v", got, err)
}
got, err = FindNamedSessionFor(ctx, "dsh", "/home", alias)
if err != nil || got != idB {
t.Fatalf("path=/home 应命中 B(同名但不同身份):got=%v err=%v", got, err)
}
// ③ 不给 path:别名不唯一 → 歧义,不许猜
if _, err := FindNamedSessionFor(ctx, "dsh", "", alias); !errors.Is(err, ErrSessionAmbiguous) {
t.Fatalf("同名两条 + 无 path 必须 ErrSessionAmbiguous,实际 %v", err)
}
// ④ 不给 path 且唯一 → 命中(兼容旧地址 `name@.别名`)
uniq := "只有一条的别名"
u := uniq
idU, err := CreateSession(ctx, &u, "pi", "独一份", "/home/program/agentmail")
if err != nil {
t.Fatal(err)
}
seedMailForSession(t, idU, "pi", "dsh", "/home/program/agentmail")
if got, err := FindNamedSessionFor(ctx, "dsh", "", uniq); err != nil || got != idU {
t.Fatalf("唯一别名 + 无 path 应命中:got=%v err=%v", got, err)
}
// ⑤ 没参与过的名字不该命中(地址解析的另一半判据)
if _, err := FindNamedSessionFor(ctx, "zcode", "/home/program/agentmail", alias); !errors.Is(err, ErrSessionNotFound) {
t.Fatalf("没参与过这条线索的名字不该命中,实际 %v", err)
}
}
// IsPlausibleWorkspace:地址受理与"目录建议"共用的一条规则。
//
// 它存在的意义是**堵住源头**:一个 `/home` 这样的值进了地址,会话的 workspace 就错了,
// 桥会把 worker 起在那里(实测 cwd=/home),那个 worker 发的信继续带 /home,
// 建议接口再把它学成第一条候选 —— 自我强化。所以这里两侧都钉:
// 实测污染过的形状必须是 false,真实工作目录必须是 true。
func TestIsPlausibleWorkspace(t *testing.T) {
cases := []struct {
in string
want bool
why string
}{
{"/home", false, "一切的家目录(实测污染值)"},
{"/root", false, "进程家目录(实测污染值)"},
{"/root/.pi/mail-sessions/8f056b73", false, "会话存储目录(实测污染值)"},
{"/home/program/agentmail/../llmsproxy", true, "含 .. 但归一化后是具体目录"},
{"rel/path", false, "相对路径不是地址里的 path"},
{"", false, "空"},
{"/home/program/agentmail", true, "真实工作目录"},
{"/home/newqqagent", true, "homeagent 的真实工作目录"},
{"/tmp/remotebot-ws", true, "尚未创建的注册目录也要放行(不做存在性检查)"},
}
for _, c := range cases {
if got := IsPlausibleWorkspace(c.in); got != c.want {
t.Errorf("IsPlausibleWorkspace(%q) = %v, want %v(%s)", c.in, got, c.want, c.why)
}
}
}