Files
MailUI4Agents/plugins/dsh-mail-bridge/test/default-session.test.mjs
JianFeeeee 86e05e7027 feat(dsh桥): withScope 在非邮件轮次回落到 /tmp 默认会话 —— 配套 15e4fe9 的收严
`reverseMap` 只在邮件投递时填 ⇒ 在 dsh 界面里直接对话时 `mailSessionOf(exec)` 为空
⇒ withScope「取不到就原样返回」⇒ 请求不带 session_id ⇒ 服务端 403
(AgentMayReadSession 收严后,2026-09-15 的迁移期放行已作废)。

不改的后果很具体:**在 dsh 界面里读不到任何信**。

回落答案只能向服务端问 —— dsh 侧给不出 session_id:
opencode 有宿主 session id 可反查(`sessionWorkspace`),pi 有 worker 闭包,
而 dsh 的 exec 里只有 dsh 自己的会话 id,与 AgentMail 会话 uuid 无映射。
这与 workspace 不同(workspace 能从目录/cwd 得到)。

装配期问一次(`loadDefaultSession`),不塞进 withScope:
withScope 是取值函数,IO 混进来就变成「拼 URL 时顺带发 HTTP」,
判据也无法用裸对象驱动(同 homeagent 那次的教训)。

问不到就当没有:不带 session_id → 服务端 403(可见的错误),
而不是静默放行成越权。

判据 4 格(接线形状 + 常量一致性 + 不得伪造 id + 失败要留日志)。
三个变异各红 3 格(判据之间交叉命中,说明它们各自都在钉不同的东西)。
tsc --noEmit 通过;桥全量 429/429 绿。
2026-10-02 01:12:35 +08:00

67 lines
3.3 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:`withScope` 在**非邮件轮次**也必须声明一个合法 session_id(2026-10-02)。
*
* ## 为什么
*
* 服务端 `AgentMayReadSession` 收严后(`15e4fe9`),未声明 session_id 的读信
* 一律 403。而 dsh 的 `reverseMap` **只在邮件投递时**才填 ⇒ 在界面里直接对话时
* `mailSessionOf(exec)` 为空。
*
* 旧写法是「取不到就原样返回 path」,那在 2026-09-15 之前是服务端的迁移期兜底;
* 那个放行已作废。所以不改 = **在 dsh 界面里读不到任何信**。
*
* 回落答案只能向服务端问:`session_id` 是 AgentMail 会话的 UUID,
* dsh 侧的目录/会话 id 给不出它(这一点与 opencode 不同 —— 它有宿主 session id 可反查)。
*
* ## 为什么默认落点是 /tmp
*
* 与服务端 `handler.DefaultFallbackWorkspace` 必须一致,不一致 = 永远问不到。
*/
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');
test('★ withScope 在 mailSessionOf 为空时回落到默认会话', () => {
assert.ok(
/const sid = mailSessionOf\(exec\) \|\| defaultSessionId;/.test(src),
'withScope 必须有默认会话回落(否则界面里直接对话时读信一律 403)');
assert.ok(
!/function withScope\(path: string, exec: any\): string \{\s*\n\s*const sid = mailSessionOf\(exec\);\s*\n\s*if \(!sid\) return path;/.test(src),
'withScope 仍是「取不到就原样返回」—— 那是 2026-09-15 前的迁移期兜底,服务端已不再放行');
});
test('默认会话在装配期问一次(不是每次 withScope 都问)', () => {
assert.ok(/let defaultSessionId = '';/.test(src), '缺少 defaultSessionId 缓存');
assert.ok(/await loadDefaultSession\(\);/.test(src), '装配期应调 loadDefaultSession');
assert.ok(
!/withScope[\s\S]{0,400}await\s+(client\.get|fetch)/.test(src),
'withScope 不该自己发请求:它是取值函数,IO 混进来会让「拼 URL 时顺带发 HTTP」,' +
'也让判据无法用裸对象驱动');
});
test('★ 默认落点与服务端一致(不一致 = 永远问不到)', () => {
const m = src.match(/const DEFAULT_FALLBACK_WORKSPACE = '([^']+)'/);
assert.ok(m, '缺少 DEFAULT_FALLBACK_WORKSPACE 常量');
assert.equal(m[1], '/tmp',
'必须与 handler.DefaultFallbackWorkspace 一致;不一致则桥问的 key 与服务端认的 key 不同');
});
test('问不到默认会话时不得编一个 id(那是把「不知道」记成「知道」)', () => {
const m = src.match(/const loadDefaultSession = async[\s\S]*?\n \};/);
assert.ok(m, '未找到 loadDefaultSession');
const body = m[0];
assert.ok(
/String\(r\?\.session_id \?\? ''\)/.test(body),
'应把缺失的 session_id 当空串(端点是纯只读的,没通信过就没有)');
assert.ok(
!/defaultSessionId\s*=\s*['"`][0-9a-f-]{36}['"`]/.test(body),
'不得硬编码或伪造一个 session_id');
assert.ok(
/catch[\s\S]*?ctx\.logger\.error/.test(body),
'问不到要留日志(否则「默认会话一直拿不到」这件事不可见)');
});