# 二七王服务端单元测试 ## 运行 ```bash node server/games/erqiwang/test/run.js # 跑全部单测 node server/games/erqiwang/test/test_arith.js # 单独跑某一组 ``` 退出码 0 表示全过,非 0 表示有失败;`run.js` 逐个以独立进程 spawn 各 `test_*.js` 并汇总。 ## 原理:一套代码,两个运行时(dev-guide 01 §1) 正式代码(`class.*.js`)遵循平台的双运行时规范: - **友乐 / 浏览器**:无 `require`,由 `mod.js` 用 `min_loadJsFile` 加载为**全局对象**;文件顶部的 `if (typeof require !== 'undefined')` 守卫被跳过,跨模块按**同名全局**引用。 - **Node(本地 / 单元测试)**:有 `require`,守卫块 `var X = require('./class.X.js')` 取到依赖;文件底部 `if (typeof module !== 'undefined') module.exports = ...` 导出。 因此这两处守卫是**纯增量**:友乐运行时行为完全不变,只在 Node 下额外启用 require/exports。 ## `_shim.js`:平台全局垫片(仅测试用,不进正式代码) 正式代码按全局名引用少量平台工具(`min_ary_include/min_ary_deduct/min_random/min_now/min_ontimeout`)与模块入口 `youle_erqiwang.import`。友乐由平台提供,Node 单测由 `test/_shim.js` 提供等价实现(数组/随机工具忠实复刻 `server/minhttp.js`,`youle_erqiwang.import` 用 mock)。测试文件顶部先 `require('./_shim')` 再 `require('../class.*.js')`。 ## 测试计划 完整的"规则 → 用例"覆盖矩阵、缺口清单与优先级见 [`../docs/compliance/02-测试计划.md`](../docs/compliance/02-测试计划.md)。 ## 覆盖 - `test_arith.js`:主牌排序/相邻链(§3)、扣底倍数(§6.3)、算子常规+爬坡逐档+分界+升3级(§7)、算奖 get_chongguan(§8.1)、甩牌分解/最大性/合法性(§5.4)、跟甩牌强制分量拆解(§5.4.4)。 - `test_follow.js`:正常跟牌/毙牌/垫牌,单张/对子/拖拉机的必出与缺门毙牌数量对应(§5.1/5.2)。 - `test_callgrade.js`:叫分坐庄 do_callgrade(叫5立即/两家不叫/后叫更低)(§4.2)。 - `test_config.js`:roomtype 位串解析、局数/扣卡映射(§10.1)。 - `test_paiju.js`:亮牌 get_liangpai(§8.2,含「埋牌后静态快照、出牌后重连不缩水」回归)、结算 get_paiju_account 集成(§7/§8,大光/过庄/升级/爬坡/傍王/投降)、扣底触发 get_bottom_account(§6.3)。 - `test_deal.js`:发牌构成——92张/去3-4/三家28+底8/分值(§2/§6.1)。 - `test_desk.js`:坐庄轮换 do_prepare(庄赢连庄/闲赢·投降下家)(§4.6)。 - `test_endgame.js`(L4 端到端,用真实发牌跑完整一局):叫分→上庄→选主→埋牌→28 轮出牌→小局结算的流程闭合(§12.1),以及与牌面无关的不变量(牌张守恒 84/8/16、捡分=闲家赢得分牌+扣底分、`X` 分配庄±2X/闲∓X、算奖 `X×(2Ni−Nj−Nk)`、结算零和、`result` 与判定一致);另覆盖 §4.4/§7.1 投降局与 §12.2 中途解散两条支线。 - `test_rpc.js`(L3,用 `_rpc.js` 装配 mod.js + 捕获下发包):明牌 mingpai、出牌 chupai 的 seatlist/baozhu 查牌门控(§9,含「seatlist 整表带三家」)、投降 touxiang 条件、jiaofen 入参校验、70分上庄暗牌亮牌下发(§4)。 ### 两组差分测试(按 design 原文另写参考实现,与代码对拍) 下面两组与上面的手写用例互补:它们不枚举"我想到的场景",而是**把 design 的规则整条转写成参考实现**,再和代码大面积比对,专治手写用例覆盖不到的欠约束分支。 - `test_calc.js`:**§7 算子全量对拍**。参考实现逐字转写自 §7.1/§7.2/§7.3 的判定表,与 `get_base_bycall` / `get_qvalue` / `get_upgrade` 比对 **2 种算子模式 × 14 个叫分档 × grade 0~260 = 7308 格**(每格比 base/Q/判定倍率/最终子数四项),另按 design 的 17 张表逐格抽查 141 个"写死的最终子数"。断言按(模式,叫分档)与表名分组,失败能直接定位到档位。 - `test_followdiff.js`:**§5.2 跟牌强制层级穷举差分**。参考裁定按 §5.2 原文写(缺门/不足/够牌 × 单张/对子/N 连对 + 最长拖拉机优先的档案比较),对每手牌**枚举全部 C 张出牌组合**逐个比对"参考允许 vs `can_followcard` 允许",任何一侧多放行或多拒绝都会被抓到(约 11 万组)。 - `test_flow.js`:**阶段机迁移矩阵 + 叫分起始者**(design §4.2/§4.6/§12.1)。① 8 个 RPC × 5 个阶段 = **40 格**逐格验证「该受理的受理、不该受理的一律回 `STEP` 失败包」——此前每个 handler 只测了 1~2 条代表性失败路径,没系统覆盖「某请求在别的阶段会不会被误放行 / 会不会静默丢弃」;断言里带 `cells === 40`,防止驱动失败导致整行"跳过"而假绿。② 第一局起始叫分者 = 座位 0、之后每局 = 暂定庄家(**庄赢连庄 / 庄输下家**)、每局首家都不能"不叫";连庄分支(`result=0`)随机打不出来,用「叫 70 分 + 首家出最大牌」专门构造。 - `test_leak.js`:**下发面泄露审计**(server 红线「发全 ≠ 发多,按可见性下发」+ design §4/§9/§11)。跑 6 局(可查牌/不查牌 × 叫 65/70/5),对每一个「服务器 → 某座位」的下发面——含各 RPC 的逐座位包、各阶段重连快照、明牌应答——深度扫描出其中所有牌 id,逐个判定该座位此刻**是否有权知道这张牌**(自己的牌 / 已打出 / 庄家的暗牌与底牌 / 70 分亮 3 秒 / 结束亮底牌 / §9 明牌)。约 1800 个下发面、1.8 万次判定。此前各处门控只有逐条手工核对,没有系统验证。 - `test_fuzz.js`:**整局模糊 + 每墩胜者差分**。`test_endgame.js` 的驱动器只会出单张,所以对子/拖拉机/甩牌/甩错在「整局链路」里从未跑过——单测逐个覆盖了 `can_playcard`/`can_followcard`,但它们与 `do_playcard`/`mod.chupai`/结算的**集成**是空白。本文件用带牌型的对局补上(20 局约 480 墩,四种房间选项组合轮换),并对**每一墩**用独立参考实现重算"谁最大"与服务端比对(不是抽查,每墩都比),另独立重算闲家捡分、扣底倍数、算奖与捡分子数的分配。提交时还会把牌按**最坏顺序**摆(`[K,K,Q,Q]→[K,Q,K,Q]`,把每个对子拆开),压服务端判定的入参顺序无关性。 三组都做过**变异检验**(在副本上注入缺陷确认会转红):关掉 `follow_tractor_cover_ok` → `test_followdiff` 三种拖拉机首出全红;大光倍率改 3→4 / 常规 55 档 base 改 4→5 / 爬坡 40·35 档 Q 改 20→40 → `test_calc` 精确指出档位与 grade;牌的归属写给非胜者、单张毙牌不再压过副牌、算奖分配去掉 `×X`、跟牌牌面值改回未排序入参 → `test_fuzz` 全部转红。 改这三个文件时别破坏的写法约定: - **随机源必须是固定种子的 PRNG**,不用 `Math.random`——同一份代码每次跑的用例集完全相同。`test_fuzz.js` 连**发牌**也接到同一个种子上:它末尾的覆盖下限(甩牌/毙牌/扣底各出现多少次)是发牌相关的,用随机发牌时稀有事件会在某些运行里一次都不出现而让断言随机转红(实测 60 次里有 1 次 0 次扣底)。随机发牌的整局覆盖由 `test_endgame.js` 承担,两者互补。 - **关键结构要用确定性手牌钉死**,不能指望随机造牌覆盖到。例如「同花色三组互不相邻的两连对」(12 张)是唯一能区分"只出零散对子"与"先凑最长拖拉机"的形状(少于 3 组时任取 3 对必含相邻对,测不出差别);纯随机造牌造不出它,关掉覆盖度校验也全绿,差分就形同虚设。`structHands()` 就是为此存在的,另注意别让掺入的其他花色把手牌撑过枚举上限而被静默跳过。 - **不变量不能拿被测字段自证**。`test_fuzz` 里踩过两次:① 闲家捡分若按 `playowner` 累加再与 `aset.grade` 比,`playowner` 写错时两边一起错、断言照样通过——改成按**独立算出的墩胜者**累计;② 扣底倍数若再调 `get_bottom_multiple` 去比,倍数表被改成恒返回 2 也通过——改成按 design §6.3 独立重算。 ## 约定(dev-guide §10 测试纪律) - 测试代码可用现代 JS(不受严格 ES5 限制);**正式代码不得为测试放宽/加后门**。 - 正反面 + 边界都要覆盖;失败先裁根因(业务缺陷改业务、脚本缺陷改脚本),不软化断言。