Files
erqiwang_youle/client/tests/test_consistency.js
T
joywayerandClaude Opus 5 8b2cfdaf27 二七王:前端删掉 9 处阶段/控制权推导,一律读服务端包字段
I-3 前端不再持有对局状态机(配合服务端上一提交的下发面补齐):
- 删掉 5 处按 rpc 名硬编码的 aset.step(Deal=1 / Call=2 / Main=3 / Bury=5 / Result=6),
  一律改 _apply 读包里的 step
- 删掉 4 处 turn.seat = aset.banker(CallHandler / MainHandler / ResyncHandler×2),
  增量路径读 nextseat、重连路径读分组里的 seat;同一条规则不再有 4 个写入处

I-2 ResyncHandler._applyBalance 由只接 aset 扩为接 aset/bottom/account,
结算面板开着时重连/硬刷新不再丢抠底明细与末局大结算。
一致性测试 EXCLUDE 随之从 4 条收缩为 2 条(去掉 result.bottom / result.account,
只留一次性事件 table.ancard3s 与归下一轮的 result.chupai),未新增任何豁免条目。

夹具按新包结构重跑(种子不变,牌局不变):diff 纯为 67 行新增字段、0 行删除;
test_fixture 补上 step/nextseat/Balance 同源断言(68→82 checks)。
export_packets.js 里关于 playproc/seatlist 活引用的注释已过时,更正为仍存在的
Balance.readystate 与三份结算快照。

各 handler 单测补齐新字段并新增守卫用例:刻意构造 nextseat != banker、
step 为非预期值的包,使「退回硬编码/banker 反推」的写法必然转红(11/11 已验证)。

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

113 lines
5.7 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',
// 收尾墩:牌局最后一手不发 chupai3,整包换成 jiesuan,其 data.chupai 由 ResultHandler
// 归档进 result.chupai;deskinfo 的 Balance 不带这一墩(它属于「出牌过程」而非结算数据)。
// 【本条是本清单仅剩的松口处】result.bottom / result.account 已随服务端 Balance 补发而移除。
'result.chupai'
];
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;
}
// 每个对齐点独立起步,不共用进程内任何残留(下面两个 build 函数各自从
// EQW_GameState.reset() + 显式赋值房间级字段开始,互不依赖调用顺序/次数):
//
// 【曾经的假绿】EQW_GameState.reset() 按设计不清 room.playerScores(它是"跨局不清"的
// 房间级累积量,见 GameState.js 注释)。旧版路径 A 从不写 playerScores(fapai 包不带),
// 于是它的值全靠"上一次由谁最后写过"——如果上一次恰好是路径 B(deskinfo 带 PlayerInfo)
// 跑过,路径 A 就会【继承】那份残留,凑巧与路径 B 的期望值相等,测试假绿;换一个座位
// 顺序、或把它放到第一个跑,残留没了就立刻转红。
// 真实客户端不存在这个残留——它在 fapai 之前先收到 StartWar(deskwar,见夹具补捕),
// 由它给出 room.playerScores 的开局初值。所以路径 A 的真实起点也改成先喂 StartWar,
// 每次都显式重新赋值,不再依赖任何"恰好还没被冲掉的旧值"。
function buildIncremental(seat, packetIndex) {
EQW_GameState.reset();
EQW_GameState.room.mySeat = seat;
EQW_GameState.room.options = EQW_RoomOptions_Parse.parse(fx.roomtype);
EQW_ResyncHandler.handleStartWar({ data: fx.startwar[seat] });
fx.seats[seat].slice(0, packetIndex).forEach(p => {
EQW_Dispatcher.dispatch({ rpc: p.rpc, data: p.data });
});
return EQW_GameState.snapshot();
}
function buildFull(seat, info) {
EQW_GameState.reset();
EQW_GameState.room.mySeat = seat;
EQW_GameState.room.options = EQW_RoomOptions_Parse.parse(fx.roomtype);
EQW_ResyncHandler.handleDeskinfo(info);
return EQW_GameState.snapshot();
}
// 对每个座位、每个 deskinfo 快照点做比对
let compared = 0;
fx.deskinfo.forEach(snapPoint => {
const seat = snapPoint.seat;
const incremental = buildIncremental(seat, snapPoint.packetIndex);
const full = buildFull(seat, snapPoint.info);
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);