diff --git a/client/tests/test_consistency.js b/client/tests/test_consistency.js new file mode 100644 index 0000000..7ee45c6 --- /dev/null +++ b/client/tests/test_consistency.js @@ -0,0 +1,95 @@ +// 【核心验收】增量累积的 GameState 必须与 deskinfo 全量重建的完全相等 +const fs = require('fs'); +const path = require('path'); +const { ROOT, load } = require('./_load'); +const t = require('./_assert')(); + +load('client/js/gameabc-framework/system/EventBus.js'); +load('client/js/01_SubGame/codes/state/Events.js'); +load('client/js/01_SubGame/codes/state/GameState.js'); +load('client/js/01_SubGame/codes/state/RoomOptions.js'); +load('client/js/01_SubGame/codes/net/handlers/DealHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/CallHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/MainHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/BuryHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/PlayHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/QueryHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/ReadyHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/ResultHandler.js'); +load('client/js/01_SubGame/codes/net/handlers/ResyncHandler.js'); +load('client/js/01_SubGame/codes/net/Dispatcher.js'); + +const fx = JSON.parse(fs.readFileSync(path.join(ROOT, 'client/tests/fixtures/packets.json'), 'utf8')); + +// 这些字段是「一次性事件」或「重连不重放」的,不参与一致性比对。 +// 每加一条都必须写清理由——这个白名单是本测试唯一的松口处,滥用它等于废掉这条守卫。 +const EXCLUDE = [ + // 70 分坐庄的 3 秒开底是上庄时的一次性事件,协议明确 deskinfo 不重放 + 'table.ancard3s', + // 本轮必出牌只对当前轮有效;deskinfo 仅在轮到本家时才带 mustcard + 'my.mustCard', + // 结算明细来自 jiesuan 包,deskinfo 的 Balance 只在结算阶段带 aset + 'result.chupai', 'result.bottom', 'result.account' +]; + +function pick(snap, exclude) { + const out = JSON.parse(JSON.stringify(snap)); + exclude.forEach(p => { + const parts = p.split('.'); + let o = out; + for (let i = 0; i < parts.length - 1; i++) { o = o && o[parts[i]]; } + if (o) { delete o[parts[parts.length - 1]]; } + }); + return out; +} + +// 对每个座位、每个 deskinfo 快照点做比对 +let compared = 0; +fx.deskinfo.forEach(snapPoint => { + const seat = snapPoint.seat; + + // —— 路径 A:增量累积 —— + EQW_GameState.reset(); + EQW_GameState.room.mySeat = seat; + EQW_GameState.room.options = EQW_RoomOptions_Parse.parse(fx.roomtype); + fx.seats[seat].slice(0, snapPoint.packetIndex).forEach(p => { + EQW_Dispatcher.dispatch({ rpc: p.rpc, data: p.data }); + }); + const incremental = EQW_GameState.snapshot(); + + // —— 路径 B:deskinfo 全量重建 —— + EQW_GameState.reset(); + EQW_GameState.room.mySeat = seat; + EQW_GameState.room.options = EQW_RoomOptions_Parse.parse(fx.roomtype); + EQW_ResyncHandler.handleDeskinfo(snapPoint.info); + const full = EQW_GameState.snapshot(); + + t.eq('座位' + seat + ' step' + snapPoint.step + ' 增量 == 全量', + pick(incremental, EXCLUDE), pick(full, EXCLUDE)); + compared++; +}); + +t.eq('比对点数量 > 0', compared > 0, true); + +// ================= Ruling 3:step3(埋牌)deskinfo 直读断言 ================= +// 一致性比对无法单独覆盖 ResyncHandler._applyBuryCards 的字段映射正确性—— +// 若增量路径压根不写某字段,两边可能「同为初始值」而比对照样绿。 +// 这里独立取 step3 快照,逐字段直读 GameState 并核对具体值。 +const step3 = fx.deskinfo.filter(d => d.step === 3)[0]; +if (!step3) { + console.warn('[test_consistency] 夹具里没有 step3 快照,Ruling 3 的直读断言无法执行'); +} else { + EQW_GameState.reset(); + EQW_ResyncHandler.handleDeskinfo(step3.info); + const bc = step3.info.BuryCards; + t.eq('step3 直读 aset.banker', EQW_GameState.aset.banker, bc.banker); + t.eq('step3 直读 aset.call', EQW_GameState.aset.call, bc.call); + t.eq('step3 直读 aset.multiple', EQW_GameState.aset.multiple, bc.multiple); + t.eq('step3 直读 aset.flower', EQW_GameState.aset.flower, bc.flower); + t.eq('step3 直读 my.bottomCards', EQW_GameState.my.bottomCards, bc.bottomcards); + t.eq('step3 直读 turn.countdown', EQW_GameState.turn.countdown, bc.countdown); + t.eq('step3 直读 turn.seat(埋牌由庄家做)', EQW_GameState.turn.seat, bc.banker); + t.eq('step3 直读 aset.step', EQW_GameState.aset.step, step3.info.step); +} + +process.exit(t.done('consistency') ? 0 : 1);