/** * 自检(**一键跑**):`node self-check.mjs` * * 它证明本示例的五件事 —— 都不接真平台,靠 `mock-platform.mjs` 跑完整协议: * ① 登记成功,且送上平台的 manifest **不含函数**(跨进程只传数据); * ② 声明里 `onTimeout: 'deny'` 在位(回合制的命门); * ③ **同步发言规则**真的生效:非当前回合者 ⇒ 回 `{ok:false}`(**且原因可读**); * ④ **异步事件订阅**真的跑到:`event.deliver` ⇒ 你的 `onEvent` 执行 ⇒ 出了一帧; * ⑤ **未接线时行为不变**:缺 `baseUrl` / `token` ⇒ **不抛**、不启长轮询(插件其余功能不受影响)。 * * 退出码:0 = 全绿;1 = 有红。⛔ 红就是红 —— 别把"没报错"当通过。 */ import { decideTurn, manifest, start, ROOM_TYPE, PLUGIN_ID } from './index.mjs' import { startMockPlatform } from './mock-platform.mjs' let failed = 0 let passed = 0 function check(name, condition, extra = '') { if (condition) { passed += 1 console.log(` ✅ ${name}`) } else { failed += 1 console.log(` ❌ ${name}${extra === '' ? '' : ` —— ${extra}`}`) } } const ROOM_ID = 'room_demo_1' const eventOf = (authorId) => ({ name: 'message.created', roomId: ROOM_ID, roomType: ROOM_TYPE, at: 1_758_000_000_000, data: { messageId: 'm1', seq: 1, authorId, authorKind: 'user', via: 'client', visibility: 'room', payload: { kind: 'text', text: 'hi' } }, }) async function main() { console.log('── ① 纯函数判定(回合制语义,可单测)') check('未开局(turnOf 空)⇒ 放行', decideTurn({ config: { turnOf: '' }, authorId: 'u1' }).ok === true) check('轮到本人 ⇒ 放行', decideTurn({ config: { turnOf: 'u2' }, authorId: 'u2' }).ok === true) const rejected = decideTurn({ config: { turnOf: 'u1' }, authorId: 'u2' }) check('没轮到 ⇒ 拒,且原因可读', rejected.ok === false && typeof rejected.detail === 'string' && rejected.detail.length > 0, JSON.stringify(rejected)) check('bot / agent 代答不参与回合(⛔ 否则会自卡)', decideTurn({ config: { turnOf: 'u1' }, authorId: 'bot_x', authorKind: 'bot' }).ok === true) console.log('\n── ② 声明面(不接平台也能看)') check(`清单里 onTimeout = 'deny'`, manifest.speakRules[0].onTimeout === 'deny') const platform = await startMockPlatform() let host try { console.log('\n── ③ 接"平台",跑真实协议(登记 → 长轮询 → 回投)') const logLines = [] const boot = await start({ baseUrl: platform.url, token: platform.token, onLog: (e) => logLines.push(e), }) host = boot.host check('登记成功(reg.ok === true)', boot.reg.ok === true, JSON.stringify(boot.reg)) check('拼上了实例凭据', platform.lastToken === platform.token) // 送上平台的 manifest:有声明、但**没有函数**。 const sent = platform.registered?.manifest ?? {} check('manifest 送上去了且声明在', Array.isArray(sent.speakRules) && sent.speakRules.length === 1) check('🔴 manifest 里没有函数(check / onEvent 已被 SDK 剔除)', sent.speakRules[0].check === undefined && sent.events[0].onEvent === undefined, JSON.stringify(Object.keys(sent.speakRules[0]))) check('送上平台的 onTimeout 也是 deny', sent.speakRules[0].onTimeout === 'deny') check('长轮询已启动(callbacks.started)', host.callbacks.started === true) // ⑶ 同步发言规则:没轮到的发言必须被拒。 platform.queueSpeakCheck({ callId: 'c1', input: { roomId: ROOM_ID, roomType: ROOM_TYPE, authorId: 'u2', authorKind: 'user', config: { turnOf: 'u1' }, envelope: { via: 'client' } }, }) const r1 = await platform.waitResult('c1') check('⚠️ 非当前回合者 ⇒ 平台收到 ok:false(拒)', r1 !== null && r1.ok === false, JSON.stringify(r1)) check('拒绝原因可读(不是空 detail)', r1 !== null && typeof r1.detail === 'string' && r1.detail.includes('还没轮到'), JSON.stringify(r1)) // 轮到本人 ⇒ 必须放行(否则规则把正常发言也卡死 = 净变差)。 platform.queueSpeakCheck({ callId: 'c2', input: { roomId: ROOM_ID, roomType: ROOM_TYPE, authorId: 'u1', authorKind: 'user', config: { turnOf: 'u1' }, envelope: { via: 'client' } }, }) const r2 = await platform.waitResult('c2') check('✅ 轮到本人 ⇒ 放行(ok:true)', r2 !== null && r2.ok === true, JSON.stringify(r2)) // ⑷ 异步事件订阅:事件送达 ⇒ onEvent 在本进程执行 ⇒ 出一帧。 platform.queueEvent({ callId: 'e1', event: eventOf('u1') }) const r3 = await platform.waitResult('e1') check('事件回投成功(ok:true)', r3 !== null && r3.ok === true, JSON.stringify(r3)) const deadline = Date.now() + 2000 while (platform.frames.length === 0 && Date.now() < deadline) await new Promise((r) => setTimeout(r, 10)) check('⚠️ onEvent 真的执行了(平台收到 1 帧)', platform.frames.length === 1, `frames=${platform.frames.length}`) check('帧内容 = 事件里的作者', platform.frames[0]?.data?.authorId === 'u1', JSON.stringify(platform.frames[0])) check('出向带了 outboundId', platform.frames[0]?.outboundId === 'minimal_out') check('读数可观测(items / settled > 0)', host.callbacks.items >= 3 && host.callbacks.settled >= 3, JSON.stringify(host.callbacks)) check('整轮无 onLog 报错', logLines.length === 0, JSON.stringify(logLines)) await host.close() check('close() 后长轮询已停', host.callbacks.stopped === true) host = undefined } finally { await platform.close() } console.log('\n── ④ 未接线(缺 baseUrl / token)⇒ 行为必须不变') const offline = await start({ baseUrl: '', token: '', onLog: () => {} }) check('不抛(start 正常返回)', offline.host !== undefined) check('不启长轮询(callbacks.started === false)', offline.host.callbacks.started === false) check('登记失败回具名结论(not-configured)', offline.reg.ok === false && offline.reg.reason === 'not-configured', JSON.stringify(offline.reg)) check('出向回具名结论而非抛错', (await offline.host.frame({ roomId: ROOM_ID, channel: 'x', data: {} })).reason === 'not-configured') await offline.host.close() console.log(`\n══════ 结果:${passed} 例通过 / ${failed} 例失败 ══════`) console.log(`(插件身份 ${PLUGIN_ID} · 房间类型 ${ROOM_TYPE})`) process.exit(failed === 0 ? 0 : 1) } main().catch((err) => { console.error('自检自身崩了:', err) process.exit(1) })