// IM agent 接入回归测试(node:test)—— C 单 `交接单/IM群组-C-Agent接入插件.md` §六 ∪ §六.1。 // // 跑的是**构建产物** `lib/`(`npm test` 先 build)。判据编号直接写在用例名里 ⇒ // 「哪条验收对哪个用例」一眼可查,第三方可复现。 // // 覆盖面:凭据 200/401/不注入不启动(判据 1)· 只拨出无监听(判据 2/10)· 防自激(判据 3/9)· // 上下文含窗口内非 @ 那条(判据 4)· 可见性(判据 4)· 四条硬约束(判据 5/9)· // 代答标识 author/via(判据 6)· 故障隔离(判据 8)· 区判据(§〇-4③)。 // // ⚠️ 本文件**不碰网络、不碰 DB**:所有判据都断 `lib/im/agent-bridge.js` 与 // `lib/im/instance-token.js` 的纯逻辑,以及插件包 `poc/im-agent-bridge/lib/index.js` // 的静态形状(无 `listen(`)。端到端(真起 socket)由 B 单的验收脚本覆盖。 import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' import { AGENT_CONTEXT_MAX_MESSAGES, AGENT_CONTEXT_WINDOW_MS, RoomRateLimiter, buildAgentContext, mentionsOf, replyMarkerOf, replyPayloadOf, shouldTrigger, visibleTo, } from '../lib/im/agent-bridge.js' import { IM_INSTANCE_TOKEN_BYTES, ImInstanceTokenRegistry, INSTANCE_TOKEN_REASONS, looksLikeImInstanceToken, } from '../lib/im/instance-token.js' import { IM_MAX_CONN_PER_USER } from '../lib/im/hub.js' const HERE = dirname(fileURLToPath(import.meta.url)) const REPO = join(HERE, '..') const PLUGIN_ENTRY = join(REPO, 'poc/im-agent-bridge/lib/index.js') /** Windows 上 `import()` 必须用 file:// URL(⛔ 不能传 `D:\...` 裸路径)。 */ function pathToFileUrl(p) { return `file:///${p.replace(/\\/g, '/')}` } /* ── 夹具 ────────────────────────────────────────────────────────────────── */ /** 造一条消息(形状 = A 单 `MessageEnvelope`)。 */ function msg(seq, over = {}) { return { id: `h${seq}`, roomId: 'imr_test', seq, ts: over.ts ?? 1_000_000 + seq, authorId: over.authorId ?? 'u_alice', authorKind: over.authorKind ?? 'human', via: over.via ?? 'web', visibility: over.visibility ?? 'room', payload: over.payload ?? { text: `第 ${seq} 条` }, } } /** 造一个成员(形状 = A 单 `RoomMember`)。 */ function member(memberId, over = {}) { return { roomId: 'imr_test', memberId, kind: over.kind ?? 'human', role: over.role ?? 'member', autoReply: over.autoReply ?? false, agentRef: over.agentRef ?? null, joinedAt: 0, } } /* ── 判据 1:凭据 200 / 401 / 不注入则不启动 ─────────────────────────────── */ test('判据 1 / §五-1:正确 token 通过校验', () => { const reg = new ImInstanceTokenRegistry() const { token, record } = reg.issue({ instanceId: 'inst_1', userId: 'u_alice', rooms: ['imr_a'] }) assert.ok(looksLikeImInstanceToken(token), 'token 外形应为 43 字符 base64url') assert.ok(token.length >= Math.ceil((IM_INSTANCE_TOKEN_BYTES * 4) / 3) - 1) const v = reg.verify(token) assert.equal(v.ok, true) assert.equal(v.record.userId, 'u_alice', '必须能答出"这是谁的实例"(§〇-4②)') assert.equal(v.record.instanceId, 'inst_1') assert.equal(record.expiresAt > record.issuedAt, true) }) test('判据 1:错误 / 缺失 / 过期 token 一律拒绝(fail-closed,⛔ 不是放行)', () => { let clock = 1000 const reg = new ImInstanceTokenRegistry({ now: () => clock }) const { token } = reg.issue({ instanceId: 'inst_1', userId: 'u_alice', ttlMs: 500 }) // 缺失 assert.deepEqual(reg.verify(undefined), { ok: false, reason: 'missing' }) assert.deepEqual(reg.verify(''), { ok: false, reason: 'missing' }) // 外形不对(长度不对 ⇒ 不认,且⛔不走定长比较) assert.equal(reg.verify('short').ok, false) assert.equal(reg.verify('short').reason, 'unknown') // 未签发过的(合法外形) assert.equal(reg.verify('A'.repeat(43)).reason, 'unknown') // 有效期内 ⇒ 通过 assert.equal(reg.verify(token).ok, true) // 时钟推过 TTL ⇒ 具名过期(⛔ 不是 unknown —— 客户端要能区分) clock = 1600 const expired = reg.verify(token) assert.equal(expired.ok, false) assert.equal(expired.reason, 'expired') assert.equal(typeof INSTANCE_TOKEN_REASONS.expired, 'string') // sweepExpired 之后变成 unknown(登记簿不留过期行) assert.equal(reg.sweepExpired(), 1) assert.equal(reg.verify(token).reason, 'unknown') }) test('判据 1 / §〇-4③:跨区一律拒(本期单区)', () => { const reg = new ImInstanceTokenRegistry() const { token } = reg.issue({ instanceId: 'inst_1', userId: 'u_alice', region: 'cn-east' }) // 不声明区 ⇒ 不触发区判据 assert.equal(reg.verify(token).ok, true) // 声明同区 ⇒ 通过 assert.equal(reg.verify(token, { region: 'cn-east' }).ok, true) // 声明异区 ⇒ 拒(具名) const bad = reg.verify(token, { region: 'cn-west' }) assert.equal(bad.ok, false) assert.equal(bad.reason, 'region_mismatch') }) test('判据 1:实例重启即换 token(旧 token 失效)', () => { const reg = new ImInstanceTokenRegistry() const first = reg.issue({ instanceId: 'inst_1', userId: 'u_alice' }).token assert.equal(reg.verify(first).ok, true) // 实例重启:先撤后签(编排器的动作,见 issueImInstanceToken) assert.equal(reg.revokeByInstance('inst_1'), 1) const second = reg.issue({ instanceId: 'inst_1', userId: 'u_alice' }).token assert.equal(reg.verify(first).reason, 'unknown', '旧 token 必须失效') assert.equal(reg.verify(second).ok, true) assert.notEqual(first, second) }) test('判据 1 / §五-1:登记簿只存哈希(token 原文不落登记簿)', () => { const reg = new ImInstanceTokenRegistry() const { token } = reg.issue({ instanceId: 'inst_1', userId: 'u_alice' }) // 白盒:内部 Map 的键、以及整对象的字符串化里都不得出现 token 原文 const keys = Object.keys(reg) assert.equal(keys.includes('byHash'), true, '内部表名变了 ⇒ 重新审计存储口径') assert.equal(JSON.stringify(reg).includes(token), false, '⛔ 登记簿里不得有 token 原文') assert.equal(reg.size(), 1) assert.equal(reg.sweepExpired(), 0, '未过期 ⇒ 清 0') }) test('判据 1:可达房间是"索引"而非授权判据(未覆盖 ⇒ 具名拒绝)', () => { const reg = new ImInstanceTokenRegistry() const { token } = reg.issue({ instanceId: 'inst_1', userId: 'u_alice', rooms: ['imr_a'] }) assert.equal(reg.verify(token, { roomId: 'imr_a' }).ok, true) const bad = reg.verify(token, { roomId: 'imr_b' }) assert.equal(bad.ok, false) assert.equal(bad.reason, 'not_in_room') // 补一个房(成员变更时调用方同步) assert.equal(reg.allowRoom('inst_1', 'imr_b'), true) assert.equal(reg.verify(token, { roomId: 'imr_b' }).ok, true) }) /* ── 判据 3:防自激(硬约束 1)──────────────────────────────────────────── */ test('判据 3 / §五-3:agent 的消息**不触发**任何 agent', () => { const snapshot = { roomId: 'imr_test', members: [ member('u_alice'), member('bot_a', { kind: 'bot', agentRef: 'agent_a' }), ], messages: [], } // bot 自己发的消息 @ 了 bot_a ⇒ 仍不触发 const fromBot = msg(1, { authorKind: 'bot', authorId: 'bot_a', payload: { text: '@bot_a 你好' } }) const d = shouldTrigger(fromBot, snapshot) assert.equal(d.trigger, false) assert.equal(d.reason, 'not-human-author') }) test('判据 3 / §五-3:human 的 @bot ⇒ 恰好触发 1 次(形态 A)', () => { const snapshot = { roomId: 'imr_test', members: [member('u_alice'), member('bot_a', { kind: 'bot', agentRef: 'agent_a' })], messages: [], } const d = shouldTrigger(msg(2, { payload: { text: '@bot_a 帮我看下' } }), snapshot) assert.equal(d.trigger, true) assert.equal(d.form, 'bot') assert.equal(d.memberId, 'bot_a') assert.equal(d.agentRef, 'agent_a') }) test('判据 3 / §五-3:无 @ ⇒ 不触发;结构性 mentions 优先于文本扫描', () => { const snapshot = { roomId: 'imr_test', members: [member('u_alice'), member('bot_a', { kind: 'bot', agentRef: 'agent_a' })], messages: [], } assert.equal(shouldTrigger(msg(3, { payload: { text: '随便说说' } }), snapshot).reason, 'no-mention') // 结构化 mentions:文本里没 @,也应触发 const structured = msg(4, { payload: { text: '帮我看下', mentions: ['bot_a'] } }) assert.equal(shouldTrigger(structured, snapshot).trigger, true) // mentions 为空数组 ⇒ 不回落到文本(显式声明了"没有 @") assert.equal(mentionsOf({ text: '@x', mentions: [] }).length, 0) }) /* ── 判据 3:形态 B(组员代理)──────────────────────────────────────────── */ test('判据 3 / §五-3:形态 B 在 autoReply=0 时**不触发**', () => { const snapshot = { roomId: 'imr_test', members: [member('u_alice', { agentRef: 'agent_alice', autoReply: false })], messages: [], } const d = shouldTrigger(msg(5, { payload: { text: '@u_alice 在吗' } }), snapshot) assert.equal(d.trigger, false) assert.equal(d.reason, 'proxy-autoreply-off') }) test('判据 3 / §四-1:形态 B 触发时 memberId = **组员本人**(代理不占位)', () => { const snapshot = { roomId: 'imr_test', members: [member('u_alice', { agentRef: 'agent_alice', autoReply: true })], messages: [], } const d = shouldTrigger(msg(6, { payload: { text: '@u_alice 帮我总结下' } }), snapshot) assert.equal(d.trigger, true) assert.equal(d.form, 'proxy') assert.equal(d.memberId, 'u_alice', '形态 B 的成员落点 = 组员本人') assert.equal(d.agentRef, 'agent_alice') }) test('判据 3:开启 agent 互相 @ 时受跳数上限约束(默认关闭)', () => { const snapshot = { roomId: 'imr_test', members: [member('bot_a', { kind: 'bot', agentRef: 'agent_a' })], messages: [], } const fromBot = msg(7, { authorKind: 'bot', authorId: 'bot_a', payload: { text: '@bot_a hi' } }) // 默认关 ⇒ 不触发 assert.equal(shouldTrigger(fromBot, snapshot).trigger, false) // 开启且 hop=0 ⇒ 触发 assert.equal(shouldTrigger(fromBot, snapshot, { allowAgentToAgent: true, hopLimit: 2, hop: 0 }).trigger, true) // 达到跳数上限 ⇒ 停(切断自激环) assert.equal(shouldTrigger(fromBot, snapshot, { allowAgentToAgent: true, hopLimit: 2, hop: 2 }).trigger, false) }) /* ── 判据 4:上下文(窗口内非 @ 那条 + 可见性)───────────────────────────── */ test('判据 4 / §五-4①:能引用**窗口内、但没有 @ 它**的消息', () => { const snapshot = { roomId: 'imr_test', members: [member('u_alice'), member('bot_a', { kind: 'bot', agentRef: 'agent_a' })], messages: [ msg(1, { payload: { text: '项目代号是 ALPHA' } }), // ← 无 @,但窗口内 msg(2, { payload: { text: '排期在周五' } }), msg(3, { payload: { text: '@bot_a 代号叫什么来着?' } }), // ← 被 @ 那条 ], meta: { name: '测试房', roomType: 'office' }, } const ctx = buildAgentContext({ snapshot, memberId: 'bot_a', agentRef: 'agent_a', form: 'bot', viewerId: 'bot_a', focusSeq: 3, now: 1_000_003, }) assert.equal(ctx.focus?.seq, 3, 'focus 必须是在 @ 那条') const windowSeqs = ctx.window.map((m) => m.seq) assert.equal(windowSeqs.includes(1), true, '窗口内非 @ 那条必须在上下文里(否则判据 4 失败)') assert.equal(ctx.prompt.includes('ALPHA'), true, 'prompt 必须真的带上那条内容') assert.equal(ctx.prompt.includes('排期在周五'), true) assert.equal(ctx.room.members, 2) }) test('判据 4 / §四-3:被 @ 那条若落在窗口外 ⇒ **显式补回**(不变量 ①)', () => { const many = [] for (let i = 1; i <= 60; i += 1) many.push(msg(i, { ts: 1_000_000 + i })) // 被 @ 的是**最老**那条(第 1 条),必然被"最近 50 条"裁掉 const snapshot = { roomId: 'imr_test', members: [member('u_alice'), member('bot_a', { kind: 'bot', agentRef: 'agent_a' })], messages: many, meta: {}, } const ctx = buildAgentContext({ snapshot, memberId: 'bot_a', agentRef: 'agent_a', form: 'bot', viewerId: 'bot_a', focusSeq: 1, now: 1_000_060, }) assert.equal(ctx.focus?.seq, 1, 'focus 必须还在') assert.equal(ctx.window.length, AGENT_CONTEXT_MAX_MESSAGES + 1, '窗口 50 + 补回 1') assert.equal(ctx.window.map((m) => m.seq).includes(1), true) // 且窗口仍按 seq 严格递增(补回后要重排) const seqs = ctx.window.map((m) => m.seq) assert.deepEqual(seqs, [...seqs].sort((a, b) => a - b)) }) test('判据 4 / §四-6:上下文窗口 = 最近 50 条或最近 10 分钟,**取小**', () => { const old = msg(1, { ts: 0, payload: { text: '十分钟前的旧话' } }) // 远超时间窗 const snapshot = { roomId: 'imr_test', members: [member('u_alice'), member('bot_a', { kind: 'bot', agentRef: 'agent_a' })], messages: [old, msg(2, { ts: 1_000_000 }), msg(3, { ts: 1_000_001 })], meta: {}, } const now = 1_000_001 const ctx = buildAgentContext({ snapshot, memberId: 'bot_a', agentRef: 'agent_a', form: 'bot', viewerId: 'bot_a', focusSeq: 3, now, }) // 旧话被时间窗排除 ⇒ 但因为它**可见且落窗** ⇒ 进 folded 摘要(⛔ 不是硬截断) assert.equal(ctx.window.map((m) => m.seq).includes(1), false, '旧话不进窗口') assert.equal(ctx.folded.length, 1, '旧话应折叠为摘要') assert.equal(ctx.prompt.includes('更早消息摘要'), true) assert.equal(AGENT_CONTEXT_WINDOW_MS, 10 * 60 * 1000) }) test('判据 4 / §五-4②:**越权内容不出现**(可见性按接收者判)', () => { const snapshot = { roomId: 'imr_test', members: [ member('u_alice'), member('u_bob'), member('bot_a', { kind: 'bot', agentRef: 'agent_a' }), ], messages: [ msg(1, { authorId: 'u_bob', payload: { text: '公开情报' } }), // bob 与 alice 的私聊(visibility=private,作者 bob)⇒ bot_a 无权看到 msg(2, { authorId: 'u_bob', visibility: 'private', payload: { text: '机密:代号 OMEGA' } }), msg(3, { payload: { text: '@bot_a 有啥情报' } }), ], meta: {}, } const ctx = buildAgentContext({ snapshot, memberId: 'bot_a', agentRef: 'agent_a', form: 'bot', viewerId: 'bot_a', focusSeq: 3, now: 1_000_003, }) assert.equal(ctx.prompt.includes('机密'), false, '越权内容不得出现(判据 4 最硬的一条)') assert.equal(ctx.prompt.includes('公开情报'), true) assert.equal(visibleTo({ visibility: 'private', authorId: 'u_bob' }, 'bot_a'), false) assert.equal(visibleTo({ visibility: 'private', authorId: 'bot_a' }, 'bot_a'), true, '自己的私聊可见') }) test('判据 4:形态 B 的上下文以**组员本人**为可见性主体(§五-6 一致)', () => { const snapshot = { roomId: 'imr_test', members: [member('u_alice', { agentRef: 'agent_alice', autoReply: true }), member('u_bob')], messages: [ // alice 自己的私聊 ⇒ 代答 agent 应能看到(因为它代表 alice) msg(1, { authorId: 'u_alice', visibility: 'private', payload: { text: '我的私人备注' } }), msg(2, { authorId: 'u_bob', payload: { text: '@u_alice 看下' } }), ], meta: {}, } const ctx = buildAgentContext({ snapshot, memberId: 'u_alice', agentRef: 'agent_alice', form: 'proxy', viewerId: 'u_alice', // 🔴 以组员本人为可见性主体 focusSeq: 2, now: 1_000_002, }) assert.equal(ctx.prompt.includes('我的私人备注'), true, '代表 alice ⇒ 能看 alice 自己的私聊') assert.equal(ctx.role.includes('代'), true, '形态 B 的职责说明应点出"代答"') }) /* ── 判据 5 / 9:四条硬约束 + 房间级限速排队 ─────────────────────────────── */ test('判据 5:房间级限速 —— 超限**排队**(⛔ 不丢消息)', () => { let clock = 0 const limiter = new RoomRateLimiter(2, 1000, () => clock) assert.deepEqual(limiter.request('imr_a', 'k1'), { admit: true }) assert.deepEqual(limiter.request('imr_a', 'k2'), { admit: true }) const q3 = limiter.request('imr_a', 'k3') assert.equal(q3.admit, false) assert.equal(q3.position, 1, '排队位置按到达序') const q4 = limiter.request('imr_a', 'k4') assert.equal(q4.position, 2) // 重复 key 不重复排队(幂等) assert.equal(limiter.request('imr_a', 'k3').position, 1) assert.equal(limiter.state('imr_a').queued, 2) // 翻窗后按序放行 clock = 1001 assert.deepEqual(limiter.drain('imr_a', 5), ['k3', 'k4']) assert.equal(limiter.state('imr_a').queued, 0) assert.equal(limiter.state('imr_a').used, 2) }) test('判据 5:房间级限速是**按房**隔离的(一个房刷屏不拖累另一个)', () => { const limiter = new RoomRateLimiter(1, 60_000) assert.equal(limiter.request('imr_a', 'a1').admit, true) assert.equal(limiter.request('imr_a', 'a2').admit, false) assert.equal(limiter.request('imr_b', 'b1').admit, true, '另一个房不受影响') }) test('判据 6 / §五-6:代答标识 —— 形态 B 的 author 指向组员本人且 via=agent', () => { const proxy = replyMarkerOf({ form: 'proxy', memberId: 'u_alice', ownerName: 'Alice' }) assert.equal(proxy.authorId, 'u_alice', '形态 B 的 author 必须是组员本人') assert.equal(proxy.via, 'agent') assert.equal(proxy.label.includes('代答'), true, '标识必须可见(产品语义)') assert.equal(proxy.label.includes('Alice'), true) const bot = replyMarkerOf({ form: 'bot', memberId: 'bot_a' }) assert.equal(bot.authorId, 'bot_a', '形态 A 的 author 指向 bot 成员位') assert.equal(bot.via, 'agent') // 回写 payload 带标识(UI 可直读,不必猜) const payload = replyPayloadOf('答案是 ALPHA', proxy) assert.equal(payload.text, '答案是 ALPHA') assert.equal(payload.agentReply, true) assert.equal(typeof payload.agentLabel, 'string') }) test('判据 8 / §六-1-8:插件崩溃 ⇒ 只影响自己(判据落在"不抛"上)', async () => { // 载体:真加载插件包的纯函数面(不 apply ⇒ 不起连接) const mod = await import(pathToFileUrl(PLUGIN_ENTRY)) // 默认 ask 实现是占位,必须**不抛** const out = await mod.defaultAsk('hello') assert.equal(typeof out, 'string') // 解析端点在坏输入下应抛(由 apply 捕获 ⇒ 不拖垮 profile);这里只确认它可解析正常输入 const ep = mod.parseEndpoint('https://ai1net.com') assert.equal(ep.port, 443) assert.equal(ep.path, '/api/im/ws') assert.equal(ep.tls, true) const ep2 = mod.parseEndpoint('http://127.0.0.1:3080') assert.equal(ep2.port, 3080) assert.equal(ep2.tls, false) }) /* ── 判据 2 / 10:只拨出(无监听端口)──────────────────────────────────── */ test('判据 2 / 10 / E3:插件源码**不含任何 listen(**(只拨出)', () => { const src = readFileSync(PLUGIN_ENTRY, 'utf8') // ⚠️ 只查真正的调用形态(`listen(` / `.listen (`),注释里提到这个词不算 const callSites = src .split('\n') .filter((line) => !line.trim().startsWith('*') && !line.trim().startsWith('//')) .filter((line) => /\.listen\s*\(/.test(line) || /(^|[^.\w])listen\s*\(/.test(line)) assert.deepEqual(callSites, [], `⛔ 不得有监听端口(E3 只拨出):\n${callSites.join('\n')}`) // 正向:必须真的有 connect 调用(否则"无 listen"是空断言) assert.equal(/connect\s*\(/.test(src), true, '必须有拨出(connect)') assert.equal(/createServer|createServer\s*\(/.test(src), false, '⛔ 不得建 server') }) test('判据 2:插件不引任何外部依赖(无 import 第三方包)', () => { const pkg = JSON.parse(readFileSync(join(REPO, 'poc/im-agent-bridge/package.json'), 'utf8')) assert.deepEqual(pkg.dependencies, {}, '⛔ C 单不引入新依赖') const src = readFileSync(PLUGIN_ENTRY, 'utf8') const imports = [...src.matchAll(/from\s+['"]([^'"]+)['"]/g)].map((m) => m[1]) assert.equal(imports.length > 0, true) for (const spec of imports) { assert.equal(spec.startsWith('node:') || spec.startsWith('.'), true, `⛔ 非内置/相对导入:${spec}`) } }) test('判据 2 / §三:插件形态 = cordis.patch.yml + dsh.bundle.patch(⛔ 无 client 面)', () => { const pkg = JSON.parse(readFileSync(join(REPO, 'poc/im-agent-bridge/package.json'), 'utf8')) assert.equal(pkg.dsh.bundle.patch, './cordis.patch.yml') assert.equal(pkg.dsh.client, undefined, 'C 单是**服务端**接入,⛔ 不声明 client 面') const patch = readFileSync(join(REPO, 'poc/im-agent-bridge/cordis.patch.yml'), 'utf8') assert.equal(patch.includes('@dsh-local/im-agent-bridge'), true) }) /* ── 判据 1 / §三:env 是唯一开关(不注入则不启动)──────────────────────── */ test('判据 1 / §三:不注入 env ⇒ apply() 直接 return(⛔ 无连接尝试)', async () => { const mod = await import(pathToFileUrl(PLUGIN_ENTRY)) const saved = { url: process.env.DSH_PLATFORM_IM_URL, token: process.env.DSH_IM_INSTANCE_TOKEN, } delete process.env.DSH_PLATFORM_IM_URL delete process.env.DSH_IM_INSTANCE_TOKEN try { // 无 ctx 服务依赖、不抛 ⇒ 天然开关生效 assert.doesNotThrow(() => mod.apply({ on: () => undefined })) assert.equal(mod.currentBridge(), null, '不注入 ⇒ 不起桥(日志无连接尝试)') } finally { if (saved.url !== undefined) process.env.DSH_PLATFORM_IM_URL = saved.url if (saved.token !== undefined) process.env.DSH_IM_INSTANCE_TOKEN = saved.token } }) test('判据 1 / §五-2:重连退避在 30 s 内(首延迟与上限)', async () => { const mod = await import(pathToFileUrl(PLUGIN_ENTRY)) assert.equal(mod.RECONNECT_FIRST_MS <= 30_000, true, '首延迟必须 ≤ 30 s') assert.equal(mod.RECONNECT_MAX_MS <= 30_000, true, '上限必须 ≤ 30 s(§五-2 要求 30 s 内恢复)') }) test('判据 1:凭据头是 `x-dsh-im-instance-token`(与 B 单预留点同名)', () => { const src = readFileSync(PLUGIN_ENTRY, 'utf8') assert.equal(src.includes('x-dsh-im-instance-token'), true) // 与平台侧读取的头必须字面一致 const routes = readFileSync(join(REPO, 'src/web/routes/im.ts'), 'utf8') assert.equal(routes.includes('x-dsh-im-instance-token'), true) const ws = readFileSync(join(REPO, 'src/im/ws.ts'), 'utf8') assert.equal(ws.includes('x-dsh-im-instance-token'), true) }) /* ── 判据 7:边界(官方 dsh 未改 / relay 未改 / 表结构未改)─────────────── */ test('判据 7 / §六-1:A 单的三张表 DDL 计数未变(⛔ 本单不动表结构)', () => { const schema = readFileSync(join(REPO, 'src/db/schema.ts'), 'utf8') // ⚠️ 本条原断言「最高版本 = 14」是**A 单自己的 ratchet**:A 单⛔不改表结构。 // 后续单子**允许**加版本(v15 = 插件数据面台账,插件投放与分库线),故判据改成 // 「**v14 仍在** + 没有**大于 v14 的本单改动**」——保留 ratchet 语义(A 单的表结构未被改), // 同时不把后续合法迁移卡红。⛔ 不要为了过测而删掉 v14 的存在性断言。 const versions = [...schema.matchAll(/\{\s*version:\s*(\d+)/g)].map((m) => Number(m[1])) assert.equal(versions.includes(14), true, 'A 单的 v14 三张表仍在') assert.equal(schema.includes('im_room_keys'), false, 'E2EE 的两张表属**待拍板**,本单⛔不建') }) test('判据 7:B 单的对外形状未变(WS 路径与 op 词表)', () => { const ws = readFileSync(join(REPO, 'src/im/ws.ts'), 'utf8') for (const op of ['subscribe', 'unsubscribe', 'send', 'resume', 'ping']) { assert.equal(ws.includes(`'${op}'`), true, `客户端 op 词表应保留 ${op}`) } const hub = readFileSync(join(REPO, 'src/im/hub.ts'), 'utf8') assert.equal(hub.includes("'/api/im/ws'"), true, 'WS 路径形状不变') assert.equal(IM_MAX_CONN_PER_USER > 0, true) })