127 lines
6.6 KiB
JavaScript
127 lines
6.6 KiB
JavaScript
/**
|
||
* 自检(**一键跑**):`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)
|
||
})
|