Files
gplan-2/game/tests/README.md
T
DKChan c162fca9ad chore(引用可核闸建档注册): 追加行删除后的文档关联面清净
- game/tests/README.md:静态扫描段 S1–S11 → S1–S12(S12 旧件零残留闸缺登记,补一段实现说明);
  S11 说明句删去「显式追加『完美封印』」——该追加行已删,且原句连词名都写错(被禁的是「完美排雷」);
  改后写明禁词全由术语表第一列解析供给、脚本内不内联词面;
  「规则全集见 architecture.md」指针句加限定:S1–S10 在架构文档,S11/S12 自持于本文件与脚本头注(迭代 32 两卡为代码批禁写 design/)
- 待办清单:「game/ 域文档两处措辞订正欠账」整条删除(未净项已由用户合入完成);
  「效果引擎系统无状态原则缺架构文档条目」现状里指向已删条目「计数器寿命域未统一」的悬挂指路句改写成凭据句
2026-10-21 17:05:22 +08:00

149 lines
13 KiB
Markdown
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.
# Test Infrastructure — Dungeon Sweeper
**Engine**: Godot 4.7.2-stable(GDScript 单语言)
**Test Framework**: 双载体——`--headless --script` 单元套件 + headless 场景探针;gdUnit4 **deferred**(D2 OQ1 关闭形态——本仓库不引第三方测试框架,需求见下「门禁」节)
**门禁**: 本地闸——`tools/run_all_gates.sh`(收口唯一验收入口,按改动域选档 `--code`/`--docs`,跨两域或改闸件本身不带参数=全量;`--quick` 为中段自检)。本仓库**无远端、无 CI**(`.github/workflows/` 已删除,不建不留不参考);闸清单与域划分只在本脚本定义,文档不另列
**Setup date**: 2026-09-12(2026-09-27 `7f24b640` 三层收敛:根 tests/ 并入 `game/tests/`、docs 并入 `design/engineering/`、production 解散)
**参考实现**: `game/smoke/`(冒烟断言 + 场景探针族——正式工程的活样板;断言计数双口径见下节)
## Directory Layout
```
tests/
unit/ # 隔离单元测试(公式/状态机/纯逻辑)——extends SceneTree + quit(code)
integration/ # L2 集成用例(跨系统闭环——与 unit/ 的隔离断言面分工,见下「分层」节)
smoke/ # critical-paths.md——人工 gate 清单(/smoke-check 读取)
evidence/ # 截图与人工签核记录
run_all.gd # 单元套件统一入口(套件 manifest 协议,D2 AC-2b)
static_scan.sh # 静态扫描域 S1–S12 可执行子集
```
## 测试三层与承载目录
| 层 | 判什么 | 承载 | 入闸方式 |
|---|---|---|---|
| **L1 单元** | 最单元的逻辑代码对不对(公式/状态机/纯逻辑) | `tests/unit/` | `run_all.gd` manifest(闸 02) |
| **L2 集成** | 跨系统的闭环对不对 | `tests/integration/` | 同上(与单元套件同一入口、同一协议) |
| **L3 系统/场景** | 跑起来之后 UI 与场景切换对不对 | `game/smoke/*.tscn` 探针 | `probe_gate.sh`(闸 03/04) |
> **integration/ 已建承载**(迭代 29 T3——此前只有章程句无实体,空壳曾于迭代 19 卡① T4 删过一次)。
> 首个集成用例 = `save_rng_roundtrip_test.gd`(存档⇄RNG 快照往返)。章程登记的四类集成用例里,
> 回放确定性已由 `tools/parity_anchor.py` 承担并收编为重闸 11 号条件闸件;
> Core-Core 与 A1×A2 端到端仍随属主批增量登记;各 GDD 登记的 deferral 集成用例以对侧登记为生效条件。
>
> **分层判据**:能不能在 `--script` 模式下不加载 autoload 就验完?能 = L1;
> 需要两个以上真实系统协同并走落盘/落库 = L2;需要完整项目引导与场景树 = L3。
## 断言计数双口径(2026-10-08 迭代 19 卡① T4 订正)
历史 README 写死「752+ 断言/十五套件」必然漂移,改为两种口径并注明语义:
- **运行时口径**:`python tools/run_smoke.py` 打印的通过项数(迭代 19 卡①分支基点实测 **840 项 0 失败**,2026-10-08;master 合并卡②④后为 846——随断言面增减浮动,以当次跑测打印为准)——循环内断言按执行次数计。
- **静态口径**:`game/smoke/` 源码 `check(` 调用点计数(2026-10-08 本分支实测 **764 处**)——按源码位置计,一处在循环内即多次执行,故两口径数值不同且各自浮动,禁互相换算或写死。
## Running Tests
```bash
# 单元套件(双载体之一)——Windows 须用 console 版 exe 取 stdout;工作目录 = game/(含 project.godot 那一层)
godot --headless --script tests/run_all.gd # 统一入口(本地闸 02 闸调用点)
godot --headless --script tests/unit/example_test.gd
# 场景探针(双载体之二)——以探针场景为运行目标,走完整项目引导(autoload 在位)
godot --headless --path . res://smoke/<probe>.tscn
# 本地闸必须配外部 wall-clock 超时(引擎无死锁探测;run_all_gates.sh 已内置)
timeout 120s godot --headless --script tests/run_all.gd
```
> 单元/契约套件状态(单元测试与契约层批 2026-09-28 立基,后随迭代 7/8 增补):`_suites()` manifest 已登记实跑 **35 套件**(2026-10-21 迭代 32 卡② 后现况,含 `counter_prefix_fatal_test` 负面套件;上一次计数变动为 2026-10-16 迭代 29 T1:样板件 `example_test.gd` 退出生产 manifest,27 → 26;`fog_lantern_wiring_test` 随提灯载体退役摘除在前)——data_config(ConfigDB 加载/进度坐标 ceil 解析/fatal 退出码/统一随机流)、core(秘宝结构冻结/断点续玩/提灯装配链)、battle_settlement(DamageMath 纯函数/DamageController 伤害单据五配方)、contracts(系统间接口契约断言)、presentation(节奏队列/奖励浮层/字体资产/地形瓦片集);致命类独立进程 `expect=1`。期望值一律由表读值推导(零玩法数值字面量)、随机只经 RngService 统一流。
**协议要点**(D2 冒烟协议):
- 冒烟脚本 `extends SceneTree` 以获得 `quit(code)`;成功 `quit(0)`、失败 `quit(1)`
- `--script` 模式**不加载 autoload 且不注册全局标识符**(裸引用 `ConfigDB` = parse error)——ConfigDB/RngService 由脚本显式实例化,core 一律构造注入
- 致命类用例独立进程,runner manifest 登记"调用 → 期望退出码"
- 每用例先 `reset_diagnostics()`(诊断收集器协议)
- 期望值由表读值推导,不写死玩法数值
## Test Naming
- **Files**: `[system]_[feature]_test.gd`(领域命名,禁 ID 前缀——design/engineering/code-conventions.md)
- **Functions**: `test_[scenario]_[expected]`
- **Example**: `reward_roll_test.gd` → `test_power_interval_rolls_within_bounds()`
## Story Type → Test Evidence
| Story Type | Required Evidence | Location |
|---|---|---|
| Logic | Automated unit test — must pass | `tests/unit/[system]/` |
| Integration | Integration test OR playtest doc | 落地时随批建 `tests/integration/[system]/`(目录已删空壳,章程见上) |
| Visual/Feel | Screenshot + lead sign-off | `tests/evidence/` |
| UI | Manual walkthrough OR interaction test(场景探针为首选) | `tests/evidence/` |
| Config/Data | Smoke check pass | 跑批回执 `game/tests/.gates/receipt.json`(不入库)+结论随批记 `reports/` |
## game/smoke/ 游离脚本台账(迭代 29 T7 逐个定性)
`game/smoke/` 有 29 个 `.gd`,其中 14 个在闸(探针有 `.tscn` 宿主、被 `probe_gate.sh`/`run_smoke.py` 调用),
其余 15 个**不带 `.tscn`、不入任何闸**——它们是手动入口的测量/诊断件。逐个定性如下,
判据 = 头注自陈用途 + 有无外部引用(工具脚本/产品码/文档)。**存疑一律按在役登记并标注**(不退役)。
| 脚本 | 定性 | 用途与触发方式 | 结果由谁判 |
|---|---|---|---|
| `adventure_monte_carlo.gd` | **在役**(`AGENTS.md` 在册入口) | 全冒险期望 MC;`tools/parity_anchor.py` 的对拍载体 | 工具脚本逐位比对 |
| `verifier_batch.gd` | **在役**(`AGENTS.md` 在册入口) | `tools/balance_verifier.py` 的采集端(数值验证) | 报告与原始数据落 `sim-data/` |
| `reward_expectation.gd` | **在役**(`AGENTS.md` 在册入口) | 奖励期望测量(非封锁格分布/资源类型频次) | 人工读 stdout |
| `monte_carlo.gd` | 在役 | 战斗期望 MC(胜轮数/承伤/清图/开格分布) | 人工读 stdout |
| `auto_player.gd` | 在役(库件) | 自动玩家策略单一实现——被 `battle_driver` preload | 宿主脚本 |
| `battle_driver.gd` | 在役(库件) | 战斗驱动;被 `damage_probe` preload | 宿主脚本 |
| `level_autoplay.gd` | 在役(库件) | 被 `presentation/tune_sim_controller.gd` load | 宿主脚本 |
| `board_stat_probe.gd` | 在役 | `tools/clear_dominance_audit.py` 的取数端 | 工具脚本 |
| `calibrate_boards.gd` | 在役 | 生成器真值分母;`sim-data/analyze.py` 引用 | 工具脚本 |
| `damage_probe.gd` | 在役(诊断件) | 零承伤根因分割探针(头注自陈:非验收件·不入闸) | 人工读 stdout |
| `defense_scan.gd` | 在役(诊断件) | AC 叠层减伤读数分割(高 AC 轮 vs 基线轮) | 人工读 stdout |
| `skill_rhythm_probe.gd` | 在役(分析工具) | 技能池节奏定位(表侧池快照 + 摇号份额) | 人工读 stdout |
| `rhythm_batch.gd` | 在役(批量采集) | 技能池节奏批量读数(06 在册项的机器半边) | 人工/工具 |
| `dump_battle.gd` | **在役·一次性** | 打印一场完整战斗的步骤流与棋盘摘要(肉眼验收归因质量) | 人工(用完即止) |
| `experiment_density.gd` | **在役·一次性** | 不同 mine_density 下的推理率/残余挖掘量取数(历史调参用) | 人工(用完即止) |
> **纪律**:本表是这些件的唯一在册处。新增手动测量脚本必须同批登记(用途 + 触发方式 + 结果由谁判);
> 退役走 `design/engineering/doc-conventions.md` 第 10 节退役规则,禁止「没看见=死件」式清理。
## Static Scan(提交前本地跑)
`tests/static_scan.sh` 实现静态扫描域 S1–S12 的可执行子集;S1–S10 的规则全集与动机见
`design/engineering/architecture/architecture.md`「测试与质量域」节,**S11/S12 两段的规则说明自持于本文件与
`tests/static_scan.sh` 头注**——迭代 32 两卡是代码实现批,按流水线红线禁写 `design/`,故这两段未进架构文档。
新违规案例必须**先补扫描规则再修码**。
S11 代码注释禁用词:扫描 game/ 全域 .gd/.tres/.md/.json/.sh 的注释面;禁词全部由 `design/gdd/术语表.md`「退役词对照」第一列解析供给(脚本内不内联任何词面——单一事实源);解析结果再按「巢穴」「旗」「flag_*」三条整行排除;三层豁免(闸门自指/显式留档 4 处/非注释面天然不取);两道自检(扫描面文件数非空+词表解析非空)。
S12 效果引擎无状态化旧件零残留:扫描 game/ 全域 `.gd` 与 `game/data/tables/*.tres`,禁已删符号(`counters_export`/`counters_seed`/`_reset_tree_limited_counters` 等)与带引号的旧计数器 id 字面(引号锚定,不误伤 `adv_`/`round_` 前缀新名);扫描面文件数为零即判红。
## 本地闸(唯一验收门禁)
本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh` 是收口唯一入口,按本批改动域选档(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量),
跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 字段),合回 master 由 worktree-guard 按 diff 推域校验回执。
闸清单与域划分的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。
**判定三硬闸**(各闸一律适用,缺一即判定有洞):
①**退出码为唯一权威判据**——日志里的 PASS 串只是日志,不作判据;
②**脚本错误扫描命中即红**——GDScript 的 `SCRIPT ERROR` / `Parse Error` 不改进程退出码、
不计失败数,且会静默中断所在函数的剩余断言,只能由外部扫输出(闸件 `tools/error_scan.py`,
错误标记与 `tools/run_smoke.py` 同源);
③**断言数为零即红**——`test_kit.gd` 的 `finish()` 在零断言时返回专用退出码 2(与「断言失败=1」
区分,避免致命类套件的 expect 协议把它当成预期结果放过)。
**gdUnit4 维持 deferred**:不引入第三方测试框架,本仓库在既有双载体(`--script` 套件 + 探针场景)内解决。
## 术语(本项目内的唯一语义)
| 词 | 语义 | 载体 |
|---|---|---|
| **冒烟(smoke)** | **秒级存活检查**——能否启动、能否加载表、能否进主屏。判断「这版还活着吗」,不判断「这版对不对」 | `game/smoke/` 的存活面 |
| **单元套件** | 隔离的公式/状态机/纯逻辑断言面,逐套件独立进程实跑 | `game/tests/unit/`、`run_all.gd` |
| **场景探针** | 以探针场景为运行目标、走完整项目引导(autoload 在位)的 UI/场景断言 | `game/smoke/*.tscn`、`probe_gate.sh` |
| **集成用例** | 跨系统测试(Core-Core/回放确定性/存档⇄RNG 往返/端到端) | `game/tests/integration/` |
> ⚠ **两处历史遗留命名,读代码时别被字面骗了**:`tools/run_smoke.py` 之名来自冒烟,但**它承载的是
> 系统级断言面**(1127 项检查,远不止存活检查);`game/smoke/` 之名同理,里面既有存活面也有
> 系统级断言与场景探针。判定「是不是冒烟」看语义,不看文件名。