/** * ★ 2026-10-01:read_inbox 在**非邮件驱动**的会话上也得能带出 workspace。 * * ## 缺陷 * * `sessionWorkspace` 只在**邮件驱动回合**建会话那一刻被填 * (`resolveSessionForMail` 里 `sessionWorkspace.set(sessionID, wantDir)`)。 * 而插件是 `export default` 全局注册的 —— 从 webui/界面里直接对话时调 * `read_inbox`,那次调用落在一条**不是邮件驱动**的会话上 ⇒ 查表为空 * ⇒ URL 拼不出 `&workspace=` ⇒ **服务端 400**。 * * 与 homeagent 那次是同一个形状(那边实测 3 次失败 / 0 次成功),但成因不同: * homeagent 的 handler 拿不到 exec 上下文,只能靠字段暂存; * 本桥的 handler 拿得到 `context.sessionID`,且 opencode 的 `Session` 带 * `directory`(`types.gen.d.ts` 里 `export type Session` 可见)⇒ 可以现场问出来。 * * ⚠ 静默失败:工具返回一句错误文本,模型照旧往下走 * ⇒ **线上至今 0 次报错不代表没问题**(这一点与 homeagent 那次不同: * 那边至少有人工触发的失败日志)。 */ 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('read_inbox 不再直接查 sessionWorkspace(那是非邮件驱动会话的空来源)', () => { // 旧写法:查表为空 → workspace 空 → 400 assert.ok( !/const workspace = sessionWorkspace\.get\(String\(context\?\.sessionID/.test(src), 'read_inbox 仍在直接查 sessionWorkspace —— 非邮件驱动会话上它必为空'); assert.ok( src.includes('await resolveWorkspace(context'), 'read_inbox 应改走 resolveWorkspace()'); }); test('resolveWorkspace 有信封之外的第二道取值', () => { assert.ok(src.includes('async function resolveWorkspace('), '缺少 resolveWorkspace()'); // 第二道:向宿主问权威的 Session.directory assert.ok(src.includes('client.session.get'), 'resolveWorkspace 应向 opencode 问 Session.directory(本桥已在别处用过这个调用)'); assert.ok(src.includes('.directory'), '应读取 Session 的 directory 字段'); }); test('★ 缓存只存成功值(失败的探测不得被记成事实)', () => { // 缓存若把失败也记下来,一次抖动会变成永久的"这个会话没有工作区" // ⇒ 而那正是本缺陷本身(静默地少带参数 → 400)。 const m = src.match(/async function resolveWorkspace[\s\S]*?\n}/); assert.ok(m, '未找到 resolveWorkspace 函数体'); const body = m[0]; assert.ok( !/catch\s*\{[\s\S]{0,200}?\n\s*\}\s*\n?\s*workspaceProbeCache\.set/.test(body) || /catch[\s\S]*?\n\s*return\s+""/.test(body), 'catch 分支里不得写入缓存'); // 有缓存写入的话,必须发生在成功判据之后 const setIdx = body.indexOf('workspaceProbeCache.set'); const dirIdx = body.indexOf('.directory'); assert.ok(setIdx > dirIdx, '缓存写入必须发生在确认 directory 之后(否则会把空值记成事实)'); }); test('★ 拿不到时不猜目录(宁可让服务端 400)', () => { const m = src.match(/async function resolveWorkspace[\s\S]*?\n}/); const body = m[0]; // 回落到某个"看起来像工作区"的默认值 = 跨工作区误读,正是 2026-09-26 那个缺陷 assert.ok(!/__dirname|process\.cwd\(\)/.test(body), '不得用 __dirname / process.cwd() 当工作区回落 —— 那会读到别处的信'); assert.ok(body.includes('return "";'), '拿不到时必须返回空串(让服务端报 400)'); }); test('模块级工具对象拿得到宿主 client(否则第二道取值形同虚设)', () => { assert.ok(src.includes('let hostClient = null;'), '缺少 hostClient —— readInboxTool 是模块级常量,拿不到 mailBridge(input) 的参数'); assert.ok( /if \(client\) hostClient = client;/.test(src), '插件初始化时必须把 client 存下来'); assert.ok( src.includes('await resolveWorkspace(context, hostClient)'), 'read_inbox 应把 hostClient 传给 resolveWorkspace'); });