与 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)。
85 lines
4.2 KiB
JavaScript
85 lines
4.2 KiB
JavaScript
/**
|
||
* 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,不该阻塞注册');
|
||
}); |