Files
MailUI4Agents/plugins/homeagent-mail-bridge/plugin_session_scope_test.go
JianFeeeee be0693821b fix(agents): 四家桥的 read_inbox 一律按会话收窄(dsh/opencode/zcode/homeagent)
用户:「你还是没修好不同 session agent 收件箱隔离的问题」。上一轮我只修了 **pi**,
另外四家还漏着 —— 它们是**每一家各自实现** read_inbox,不修就还是漏。

## 缺陷

列表按 Agent 列(整个收件箱),而 read_inbox 按契约把**列出来的都标成已读**
⇒ A 会话的回合会把 B 会话的未读标掉 ⇒ B 之后按 `?status=unread` 补投时
再也看不到那封信(静默丢信,不是"少看一封")。用户是在别的 Agent 上看到它的。

## 四家的修法(各自平台能力不同,但都要"并发安全")

| 桥 | 会话来源 | 为什么这样做 |
|---|---|---|
| dsh | 工具第二参数 `exec.agent.id` → `reverseMap` | 平台就在上下文里给了会话;**不能用模块级"当前会话"变量**(同进程可能同时跑多条会话的回合,会互相覆盖) |
| opencode | 工具第二参数 `context.sessionID` → `reverseMap` | 同上 |
| zcode | `AGENTMAIL_SESSION_ID`(在**调用时**读) | 一轮一个进程,驱动本来就注入它给授权钩子用;调用时读,避免将来复用进程拿到旧值 |
| homeagent | `p.currentSessionID`(回合开始设、结束清) | Go 插件,本来就有这个状态 |

取不到会话一律**退回整体收件箱**(历史行为),不猜 —— 猜错就是静默丢信。

## 判据

- 服务端语义:`server/internal/repo/session_scope_test.go`(读 A 不动 B、列表收窄、
  计数与列表口径一致)。
- 桥侧接线:dsh 4 条、opencode 3 条、zcode 3 条、homeagent Go 1 条
  (`TestInboxURLScopedBySession`,直接断言拼出来的 URL)。
  每家都带**判据自检**:拿旧写法喂进来必须判红;dsh/opencode 还专门断言
  "不得用模块级当前会话变量"。
- **部署件**(不是仓库):四家的部署快照里都能 grep 到 `session_id=`。
- **线上实测**:用 opencode 自己的 Agent 身份请求收窄列表 —— 会话 A 3 封、
  会话 B 0 封、两者无交集、且都是全量的子集。

套件:opencode **331**、dsh **381**、zcode **385**、homeagent ok,全绿。
四家桥已重新部署(dsh/opencode/zcode 快照切换 + homeagent 新 plugin.bin 并重启),
四个服务均 active。
2026-09-14 12:07:45 +08:00

46 lines
1.6 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 main
import (
"net/url"
"strings"
"testing"
)
/*
会话收窄:一个 Agent 同时在多条会话里干活时,收件箱列表必须只列**当前这条**。
缺陷(用户报的):「不同 session 的 agent 都可以看到全部邮件」。列表按 Agent 列,
且 read_inbox 按契约把列出来的都标已读 ⇒ A 会话的回合标掉 B 会话的未读 ⇒
B 之后按 `?status=unread` 补投时再也看不到那封信(静默丢信)。
homeagent 原先根本没有这个参数(另外三家桥也是),所以这里不只测"带上了"
也测"不在回合里时不带"(那是历史行为,桥启动自检要用整箱)。
*/
func TestInboxURLScopedBySession(t *testing.T) {
p := &Plugin{gwURL: "http://127.0.0.1:8180"}
outside := p.inboxURL("unread", 5)
if strings.Contains(outside, "session_id=") {
t.Fatalf("不在回合里时不应带 session_id%s", outside)
}
p.currentSessionID = "593988da-0000-0000-0000-000000000001"
inside := p.inboxURL("unread", 5)
if !strings.Contains(inside, "session_id=593988da-0000-0000-0000-000000000001") {
t.Fatalf("回合中必须带上当前会话:%s", inside)
}
if !strings.Contains(inside, "status=unread") || !strings.Contains(inside, "limit=5") {
t.Fatalf("原有参数不能被改坏:%s", inside)
}
// 会话 id 里不可能出现的东西也要被转义(否则拼出坏 URL网关会 400
p.currentSessionID = "a b&c"
esc := p.inboxURL("all", 1)
if strings.Contains(esc, "&c") {
t.Fatalf("session_id 未转义:%s", esc)
}
if _, err := url.Parse(esc); err != nil {
t.Fatalf("拼出的 URL 不合法:%v", err)
}
}