Files
MailUI4Agents/plugins/dsh-mail-bridge/test/read-scope-wiring.test.mjs
JianFeeeee 2a5e3d7d15 fix(auth): 四家桥的读端点也带上会话收窄 + 转发同一条命(工作区隔离第 2 步)
第 1 步(1b8cd43)把工作区判据放在服务端、pi 桥接上了线。这一步补齐另外四家,
并把**转发**纳入:转发是"把原文引出去",能转发就等于能读到那条线索的全部内容,
与 read_mail 同一条命(服务端 ForwardMail 也加了同一道校验)。

四家各自的会话来源,与各自的 read_inbox 同一处(不引入第二个来源):
- dsh:`mailSessionOf(exec)`(工具第二个参数)—— 五个读工具原本没接 exec,这次补上
- opencode:`reverseMap.get(context.sessionID)`
- zcode:`process.env.AGENTMAIL_SESSION_ID`(一轮一个进程)
- homeagent:`p.currentSessionID`(新增 `scopeQuery(sep)`,与 inboxURL 同构)

判据(每条两侧都钉:包住了 / 没包住的不存在):
- dsh:静态对照,且额外钉 **dist** —— 那是真被 dsh 加载的那份(main: dist/index.js),
  src 改了忘了 build 就是"源码对、线上旧代码"
- opencode / zcode:同上(opencode 还钉"会话来自 context 而不是模块级变量")
- homeagent:起 httptest 当网关,**五个读工具 + 转发真调一遍**,断言请求 URL 带
  session_id;对照侧:不在回合里(currentSessionID 为空)时不许带
- pi:把 post 的 URL 也纳入记录,forward 进用例表

★ zcode 那条判据我第一版**对照组写错**了:对照组只写裸 URL,而它本来就是
`withScope(\`裸URL\`)` 的子串 ⇒ `!includes(bare)` 恒假。夹具形状不对时判据会以
"恒红/恒绿"的方式骗人(这次是恒红,一眼可见;恒绿就麻烦了)。

变异:homeagent 去掉 read_mail 的收窄 ⇒ 恰好那条断言红。

(工作区共享,只 add 了上面这 12 个文件;dsh 的 dist 是 gitignore 的,由
redeploy-plugin.sh 在 staging 里构建。)
2026-09-14 23:18:12 +08:00

70 lines
3.8 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.

/**
* 五个读类端点的请求都要带上**自己那条邮件会话**(dsh)。
*
* read_inbox 早就有这一维(缺陷:列表按 Agent 列且按契约标已读 ⇒ A 会话标掉
* B 会话的未读 ⇒ 静默丢信)。服务端现在拿它多干一件事:**由这条会话反查工作区**,
* 只有同工作区的会话才放行 —— 一个 Agent 同时服务所有工作区,不收窄时在 TrueAgent
* 里干活的 worker 能读到 agentmail 的整条线索(用户 2026-09-14 报的越界)。
*
* 服务端语义由 server/internal/repo/workspace_scope_test.go 负责;这里只验接线。
* 两侧都钉:包住了 / 没包住的不存在 —— 只验前者的话,把 withScope 写成恒等函数也能过。
*
* 注意 dsh 的工具签名原本只有 `execute(args)`,exec 是**第二个参数**(read_inbox 一直
* 在用)—— 这次给五个读工具都补上了,否则拿不到平台会话就没有收窄可言。
*/
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 src = readFileSync(join(HERE, '..', 'src', 'index.ts'), 'utf8');
const dist = readFileSync(join(HERE, '..', 'dist', 'index.js'), 'utf8');
const ENDPOINTS = [
['read_mail',
'withScope(`/agent/mail/${args.mail_id}`, exec)',
'client.get(`/agent/mail/${args.mail_id}`)'],
['read_thread',
'withScope(`/agent/mail/${args.mail_id}/thread${qs}`, exec)',
'client.get(`/agent/mail/${args.mail_id}/thread${qs}`)'],
['list_contacts',
"withScope('/agent/contacts', exec)",
"client.get('/agent/contacts')"],
['session_participants',
'withScope(`/agent/sessions/${args.session_id}/participants`, exec)',
'client.get(`/agent/sessions/${args.session_id}/participants`)'],
];
for (const [name, scoped, bare] of ENDPOINTS) {
test(`★ ${name} 的请求走 withScope(...)`, () => {
assert.ok(src.includes(scoped), `${name} 的 URL 没有包在 withScope 里:${scoped}`);
assert.ok(!src.includes(bare), `${name} 还有一处没包住的写法:${bare}`);
// dist 是**真正被 dsh 加载**的那份(main: dist/index.js)。src 改了忘了 build
// 就是"源码对、线上旧代码"——这一条正是为此存在的。
assert.ok(dist.includes(scoped), `dist 里的 ${name} 没有 withScope —— 忘了 npm run build?`);
});
}
test('★ forward_mail 也带上收窄(它读的是原文)', () => {
const scoped = 'withScope(`/mail/${args.mail_id}/forward`, toolCtx)';
const bare = 'client.post(`/mail/${args.mail_id}/forward`, {';
assert.ok(src.includes(scoped), '转发的 URL 没有包在 withScope 里');
assert.ok(!src.includes(bare), '转发还有一处没包住的写法');
assert.ok(dist.includes(scoped), 'dist 里没有 —— 忘了 npm run build?');
});
test('★ suggest_address 的会话候选也带上收窄(它列的是别的会话的别名与标题)', () => {
assert.ok(src.includes("qs.set('session_id', sid)"), 'suggest 没把 session_id 放进查询串');
assert.ok(dist.includes("qs.set('session_id', sid)"), 'dist 里没有 —— 忘了 npm run build?');
});
test('withScope 的会话来自平台上下文(并发安全),不是模块级变量', () => {
assert.ok(src.includes('const sid = mailSessionOf(exec);'), '要走 mailSessionOf(exec)');
assert.match(src, /function mailSessionOf\(exec: any\)/, 'mailSessionOf 得在');
assert.ok(src.includes("const sep = path.includes('?') ? '&' : '?';"), '自己判断分隔符');
assert.ok(src.includes('if (!sid) return path;'), '拿不到会话就原样返回');
assert.ok(!/let\s+currentMailSessionID/.test(src), '不得用模块级"当前会话"变量');
});