/** * 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,不该阻塞注册'); });