Files
MailUI4Agents/plugins/opencode-mail-bridge/test/default-session.test.mjs
JianFeeeee 85de9ee87b feat(opencode桥): withScope 在非邮件轮次回落到 /tmp 默认会话 —— 配套 15e4fe9 的收严
与 dsh 同一形状:`reverseMap` 只在邮件投递时填 ⇒ 用户在界面里新开一轮对话时
查表必空 ⇒ withScope「取不到就原样返回」⇒ 不带 session_id ⇒ 服务端 403。

实测它至今只裸奔过 1 次(2026-09-28),但**形状在**,且旧兜底的服务端放行已作废。
一致性比实测频率重要:留着一个已知失效的分支,下次有人读它会以为那是可用的。

回落答案只能问服务端 —— opencode 自己的 session id 与 AgentMail 会话 uuid
没有映射。★ 这与 `workspace` 那一维**不同**:那一维可以从 `Session.directory`
问出来(见 workspace-probe.test.mjs,同一天修的),所以这一维只能走默认会话。

装配期 fire-and-forget 问一次(不 await):它的失败后果只是那次读信 403,
不该拖住注册流程(而 dsh 那边是 await,因为 dsh 的注册路径本来就顺序执行)。
这个差异是有意的,不是抄漏。

判据 5 格。三个变异各红 3 格(交叉命中,说明各自在钉不同东西)。
全量 364/364 绿(359 + 5)。
2026-10-02 01:15:58 +08:00

85 lines
4.2 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.

/**
* opencode:`withScope` 在**非邮件轮次**也必须声明一个合法 session_id(2026-10-02)。
*
* ## 为什么
*
* 服务端 `AgentMayReadSession` 收严后(`15e4fe9`),未声明 session_id 的读信一律
* 403。而 `reverseMap` **只在邮件投递时**填 ⇒ 用户在 opencode 界面里新开一轮
* 对话时查表必空 ⇒ 读信 403。
*
* 旧写法「取不到就原样返回」是 2026-09-15 之前的迁移期放行,服务端已不再放行。
*
* 回落答案只能问服务端:opencode 自己的 session id 与 AgentMail 会话 uuid
* 没有映射 —— 与 `workspace` 那一维不同(那一维可以从 `Session.directory` 问出来,
* 见 workspace-probe.test.mjs)。所以这一维只能走默认会话。
*
* ## 形状要求
*
* 装配期问一次(不塞进 withScope):withScope 是取值函数,
* IO 混进来就变成「拼 URL 时顺带发 HTTP」。
*/
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, '..', 'index.js'), 'utf8');
test('★ withScope 在 reverseMap 未命中时回落到默认会话', () => {
assert.ok(
/reverseMap\.get\(String\(context\?\.sessionID \?\? ""\)\) \|\| defaultSessionId \|\| ""/.test(src),
'withScope 必须有默认会话回落(否则界面里直接对话时读信一律 403)');
assert.ok(
!/function withScope\(path, context\) \{[\s\S]{0,200}?if \(!mailSessionID\) return path;/.test(src)
|| /defaultSessionId/.test(src.match(/function withScope[\s\S]{0,400}/)[0]),
'withScope 仍是「取不到就原样返回」—— 那是迁移期兜底,服务端已不再放行');
});
test('默认会话在装配期问一次,而不是每次 withScope 都问', () => {
assert.ok(/let defaultSessionId = "";/.test(src), '缺少 defaultSessionId 缓存');
assert.ok(/let defaultSessionProbed = false;/.test(src), '缺少「已问过」标记');
assert.ok(/await loadDefaultSession|loadDefaultSession\(/.test(src), '装配期应调 loadDefaultSession');
assert.ok(
!/function withScope[\s\S]{0,500}await\s+fetch/.test(src),
'withScope 不该自己发请求:它是取值函数');
});
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(那是把「不知道」记成「知道」)', () => {
// ⚠ 定位方式踩过两次坑:
// ① 正则 `/\n}/` 收尾会截到 catch 块里那个独占一行的 `}`;
// ② 改用「下一个顶层声明」作边界也不行(后面紧跟注释块)。
// 改成按行切:函数头到**行首**的 `}` 为止(缩进 0 的闭括号)。
// 失败时是这一格红,不会伪装成「实现有 bug」。
const lines = src.split('\n');
const start = lines.findIndex((l) => l.includes('async function loadDefaultSession'));
assert.ok(start > 0, '未找到 loadDefaultSession');
let end = start;
while (end < lines.length && lines[end] !== '}') end++;
const body = lines.slice(start, end + 1).join('\n');
assert.ok(
/String\(data\?\.session_id \?\? ""\)/.test(body),
'应把缺失的 session_id 当空串(端点是纯只读的,没通信过就没有)');
assert.ok(
!/defaultSessionId\s*=\s*["'`][0-9a-f-]{36}["'`]/.test(body),
'不得硬编码或伪造一个 session_id');
assert.ok(
/catch[\s\S]*?log\?\./.test(body),
'问不到要留日志(否则「默认会话一直拿不到」这件事不可见)');
});
test('装配期调用不 await(问不到不该拖住注册流程)', () => {
assert.ok(
/(?:^|[^a-zA-Z])loadDefaultSession\(/.test(src)
&& !/await\s+loadDefaultSession\(/.test(src),
'应 fire-and-forget:它的失败后果只是那次读信 403,不该阻塞注册');
});