Files
MailUI4Agents/plugins/zcode-mail-bridge/test/inbox-session-scope.test.mjs
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

33 lines
1.5 KiB
JavaScript
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.

/**
* read_inbox 必须收窄到**自己那条会话**(用户:「你还是没修好不同 session agent
* 收件箱隔离的问题」)。
*
* zcode 的特殊之处:一轮一个进程,驱动已经把本轮邮件会话注入 AGENTMAIL_SESSION_ID
* (授权钩子本来就用它)⇒ 直接读 env 即可,天然并发安全。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const tools = readFileSync(join(HERE, '..', 'lib', 'tools.mjs'), 'utf8');
const index = readFileSync(join(HERE, '..', 'src', 'index.mjs'), 'utf8');
test('驱动确实注入了本轮邮件会话', () => {
assert.match(index, /AGENTMAIL_SESSION_ID: sessionId/, '驱动要把邮件会话传下去');
});
test('read_inbox 会拼上会话收窄,且在调用时读 env', () => {
assert.match(tools, /const mailSessionID = process\.env\.AGENTMAIL_SESSION_ID \|\| ''/, '调用时读(不是 import 时读死)');
assert.match(tools, /mail\/inbox\?status=\$\{encodeURIComponent\(status\)\}&limit=\$\{limit\}\$\{scope\}/);
assert.match(tools, /session_id=\$\{encodeURIComponent\(mailSessionID\)\}/);
});
test('★ 判据自检:旧写法(不带 env、不带 scope必须判红', () => {
const old = '`/mail/inbox?status=${encodeURIComponent(status)}&limit=${limit}`';
assert.ok(!/\$\{scope\}/.test(old));
assert.ok(!/AGENTMAIL_SESSION_ID/.test(old));
});