Files
erqiwang_youle/client/tests/test_consistency.js
T
joywayerandClaude Opus 5 33c18a2c17 二七王:增量 vs 全量一致性测试(B 阶段核心验收)
把前端红线「视图 = f(服务端快照)」变成可执行断言:按序喂推送包累积出的
GameState,必须与同一时点 deskinfo 全量重建的完全相等。

不一致只有两种可能,都要修:前端私存了服务端不知道的状态,或服务端漏发了字段。
EXCLUDE 白名单只收「一次性事件」与「重连不重放」两类,每条都写明理由。

本测试当前保持失败:比对发现了 7 处前端 handler 缺陷(含 table.playproc 字段
结构与协议不符等高优先级问题)与 1 处疑似服务端下发顺序不一致,均未在本次
改动中修复,详见 task-12-report.md,留待裁决。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 18:48:08 +08:00

96 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 【核心验收】增量累积的 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);