// 夹具自检:真包必须覆盖全部阶段,且保留座位差异 const fs = require('fs'); const path = require('path'); const { ROOT } = require('./_load'); const t = require('./_assert')(); const fx = JSON.parse(fs.readFileSync(path.join(ROOT, 'client/tests/fixtures/packets.json'), 'utf8')); t.eq('三个座位', fx.seats.length, 3); const rpcsOf = seat => fx.seats[seat].map(p => p.rpc); const allRpcs = [].concat(rpcsOf(0), rpcsOf(1), rpcsOf(2)); ['fapai','jiaofen','shangzhuang','xuanzhu','maipai','chupai1','chupai2','chupai3','jiesuan'] .forEach(rpc => t.eq('夹具覆盖 ' + rpc, allRpcs.indexOf(rpc) >= 0, true)); // 差异化下发确实存在:bottomcards 恰好只发给庄家一人(不是"少于三家"这种可被空集合蒙混过去的弱断言) const withBottom = [0,1,2].filter(s => fx.seats[s].some(p => p.rpc === 'shangzhuang' && p.data.hasOwnProperty('bottomcards'))); t.eq('底牌恰好发给1个座位(庄家)', withBottom.length, 1); // deskinfo 快照必须覆盖全部 5 个阶段(下游任务要用 step3 埋牌快照做回归断言) const steps = fx.deskinfo.map(d => d.step).filter((v, i, a) => a.indexOf(v) === i).sort((a,b) => a - b); t.eq('deskinfo 覆盖全部阶段 1/2/3/5/6', steps, [1, 2, 3, 5, 6]); // roomtype 必须存在且非空,Task 12 要用它还原房间选项 t.eq('roomtype 存在且非空', typeof fx.roomtype === 'string' && fx.roomtype.length > 0, true); // startwar 是增量路径的真实起点(开局握手,结构=deskwar=deskinfo),必须每座位各一份, // 且不得混进 seats[] 数组头部——那会让全部 packetIndex 集体错位一位(见夹具补捕说明) t.eq('startwar 存在且为 3 份', Array.isArray(fx.startwar) && fx.startwar.length, 3); [0, 1, 2].forEach(s => { const dw = fx.startwar[s] && fx.startwar[s].deskwar; t.eq('startwar[' + s + '].deskwar 存在', !!dw, true); t.eq('startwar[' + s + '] 结构=deskinfo(带 step/PlayerInfo/MyCards)', dw && dw.hasOwnProperty('step') && dw.hasOwnProperty('PlayerInfo') && dw.hasOwnProperty('MyCards'), true); t.eq('startwar[' + s + '].PlayerInfo 为开局初值(三家均 0)', dw && dw.PlayerInfo, [0, 0, 0]); }); // seats[] 的第一包必须仍是 fapai——确认 startwar 没有被误插进 seats[] 头部 [0, 1, 2].forEach(s => { t.eq('seats[' + s + '][0] 仍是 fapai(startwar 未混入 seats)', fx.seats[s][0].rpc, 'fapai'); }); // packetIndex 必须落在 (0, 该座位包总数] 区间内,且与 info.step 自洽 fx.deskinfo.forEach(d => { const inRange = d.packetIndex > 0 && d.packetIndex <= fx.seats[d.seat].length; t.eq('packetIndex 落在有效区间 (step' + d.step + ' seat' + d.seat + ')', inRange, true); t.eq('deskinfo.step 与 info.step 一致 (seat' + d.seat + ' pi' + d.packetIndex + ')', d.step, d.info.step); }); // C-1 回归锁:同一座位的两条 step5 快照,playproc(当前轮桌面牌/currseat/round)不得相同—— // 否则说明 deskinfo 快照存的是活引用,被后续出牌篡改成了终局状态(曾经的真实缺陷) const step5BySeat = {}; fx.deskinfo.filter(d => d.step === 5).forEach(d => { (step5BySeat[d.seat] = step5BySeat[d.seat] || []).push(d); }); Object.keys(step5BySeat).forEach(seat => { const list = step5BySeat[seat]; if (list.length < 2) { return; } for (let i = 1; i < list.length; i++) { const same = JSON.stringify(list[0].info.PushCards.playproc) === JSON.stringify(list[i].info.PushCards.playproc); t.eq('seat' + seat + ' 两条 step5 快照 playproc 不相同(未被活引用污染)', same, false); } }); // 每个包都有 success(协议 §0.1:每个下发包的 data 必带 success) const noSuccess = []; [0,1,2].forEach(s => fx.seats[s].forEach(p => { if (!p.data || !p.data.hasOwnProperty('success')) { noSuccess.push(s + ':' + p.rpc); } })); t.eq('每个下发包都带 success', noSuccess, []); process.exit(t.done('fixture') ? 0 : 1);