/** * read_inbox 必须把列表收窄到**自己所在的工作区**(2026-09-26 用户报的缺陷)。 * * # 缺陷(用户当场指出,此前已提过多次) * * 在 `mc` 工作区干活的 pi 读收件箱拿到 200 封,其中 **191 封属于 * `/home/program/agentmail`** —— 它照着那些信里的断言去改 agentmail 的代码, * 把手上 mc 的活丢在一边。用户当场问「你怎么干着干着修 agentmail 去了?」 * * 三维地址是 `name@path.session`:**path 位本来就该参与寻址**,而收件箱侧此前 * 完全没用它(`ListInboxScoped` 的 WHERE 里只有 `to_name`)。 * * # 与 inbox-session-scope 的分工(两条判据、两维收窄,别混) * * · `session_id` —— 只列**这条线索**的信。防的是"A 会话的 worker 标掉 B 会话的未读"。 * · `workspace` —— 只列**这个工作区**的信。防的是"在 mc 干活却读到 agentmail 的信"。 * * 两维都要,缺一维就漏一类越界。这里只验**接线**;服务端语义 * (读 A 工作区不动 B)由 `server/internal/repo/workspace_scope_test.go` 负责。 */ 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 tools = readFileSync(join(HERE, '..', 'src', 'tools.mjs'), 'utf8'); const worker = readFileSync(join(HERE, '..', 'src', 'worker.mjs'), 'utf8'); const index = readFileSync(join(HERE, '..', 'src', 'index.mjs'), 'utf8'); test('read_inbox 的收窄参数带上 workspace', () => { assert.match(tools, /workspace=\$\{encodeURIComponent\(ws\)\}/, 'scopeQS 要真的拼出 workspace=…(服务端缺它直接 400)'); assert.match(tools, /const ws = wsParam\(\);/, 'scopeQS 要消费 wsParam,而不是只留着函数不调'); }); test('worker 把"这封信寄到哪个工作区"递给工具', () => { assert.match(worker, /getWorkspace: \(\) => mailContext\.workspace/, '要传闭包而不是快照(worker 每回合的 mailContext 会变)'); assert.match(worker, /workspace: msg\.data\?\.to_workspace/, '★ 取**信封的 path 位**(to_workspace),不是会话文件的 cwd —— ' + '前者是"这封信寄到哪个工作区",后者是"会话上次落在哪"'); }); test('补投逐工作区走,不再读一次全局收件箱', () => { // 原来:client.get('/mail/inbox?status=unread&limit=20') —— 不带收窄, // 于是桥会把所有工作区的漏投一起重放。 assert.match(index, /pending_workspaces/, '心跳要消费 pending_workspaces 清单'); assert.match(index, /mail\/inbox\?status=unread&limit=20&workspace=\$\{encodeURIComponent\(ws\)\}/, '每个工作区各读一次,且带上 workspace'); assert.doesNotMatch(index, /client\.get\('\/mail\/inbox\?status=unread&limit=20'\)/, '旧的"不带收窄读全局"那条路必须消失(它就是跨工作区重放的载体)'); }); test('★ 判据自检:拿没带工作区收窄的旧写法喂进来必须判红', () => { const oldScope = "const sid = typeof getMailSessionId === 'function' ? getMailSessionId() : '';\n return sid ? `session_id=${encodeURIComponent(sid)}` : '';"; assert.equal(/workspace=\$\{encodeURIComponent\(ws\)\}/.test(oldScope), false); const oldCatchup = "const box = await client.get('/mail/inbox?status=unread&limit=20');"; assert.match(oldCatchup, /client\.get\('\/mail\/inbox\?status=unread&limit=20'\)/, '自检:旧写法确实能被那条 doesNotMatch 抓到'); });