diff --git a/CLAUDE.md b/CLAUDE.md index d14c0e3c..246d498c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ ## 运行与验证(工作目录 = 含 project.godot 的切片目录) - **交付前验收 = 跑 `tools/run_all_gates.sh`(不带 `--quick`),全绿才合回 master**——它是全部验证闸的唯一入口与唯一清单,闸件增减只改它,文档禁另列清单。跑完写回执 `game/tests/.gates/receipt.json`,合回时由 worktree-guard 校验(缺回执/quick 模式/回执 commit ≠ HEAD/含红项四况一律拦截) -- 单件直跑(调试期最窄确认用,**不构成验收**):冒烟 `python tools/run_smoke.py` · 场景探针 `…console.exe --headless --path . res://smoke/ui_probe.tscn` · 单元 `… --script res://tests/run_all.gd` +- 单件直跑(调试期最窄确认用,**不构成验收**):系统级断言面 `python tools/run_smoke.py`(名带 smoke 但不止存活检查,术语见 `game/tests/README.md`)· 场景探针 `…console.exe --headless --path . res://smoke/ui_probe.tscn` · 单元 `… --script res://tests/run_all.gd` - 调参批量:`--script res://smoke/adventure_monte_carlo.gd`(全冒险期望)· `res://smoke/reward_expectation.gd`(奖励期望);通关率/难度类测量一律走全冒险 - 数值验证:`python tools/balance_verifier.py`(采集端 `res://smoke/verifier_batch.gd`,报告落 `design/qa/`) - 熵增闸(新类别 = 同批修订 Project Layout + 白名单):`python tools/repo_structure_audit.py` diff --git a/game/tests/README.md b/game/tests/README.md index c959ad58..5acb442a 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -1,8 +1,8 @@ # Test Infrastructure — Dungeon Sweeper **Engine**: Godot 4.7.2-stable(GDScript 单语言) -**Test Framework**: 双载体——`--headless --script` 逻辑冒烟 + headless 场景探针;gdUnit4 **deferred**(D2 OQ1 关闭形态,正式 CI 期再议) -**CI**: `.github/workflows/tests.yml`(首次 push 到 GitHub 后自动生效;仓库已初始化 git、尚未配置远端) +**Test Framework**: 双载体——`--headless --script` 单元套件 + headless 场景探针;gdUnit4 **deferred**(D2 OQ1 关闭形态——本仓库不引第三方测试框架,需求见下「门禁」节) +**门禁**: 本地闸——`tools/run_all_gates.sh`(全量,收口唯一验收入口)/`--quick`(中段自检)。本仓库**无远端、无 CI**(`.github/workflows/` 已删除,不建不留不参考);闸清单只在本脚本定义,文档不另列 **Setup date**: 2026-09-12(2026-09-27 `7f24b640` 三层收敛:根 tests/ 并入 `game/tests/`、docs 并入 `design/dev/`、production 解散) **参考实现**: `game/smoke/`(冒烟断言 + 场景探针族——正式工程的活样板;断言计数双口径见下节) @@ -13,7 +13,7 @@ tests/ unit/ # 隔离单元测试(公式/状态机/纯逻辑)——extends SceneTree + quit(code) smoke/ # critical-paths.md——人工 gate 清单(/smoke-check 读取) evidence/ # 截图与人工签核记录 - run_all.gd # CI 冒烟统一入口(套件 manifest 协议,D2 AC-2b) + run_all.gd # 单元套件统一入口(套件 manifest 协议,D2 AC-2b) static_scan.sh # 静态扫描域 S1–S8 可执行子集 ``` @@ -29,18 +29,18 @@ tests/ ## Running Tests ```bash -# 逻辑冒烟(双载体之一)——Windows 须用 console 版 exe 取 stdout;工作目录 = game/(含 project.godot 那一层) -godot --headless --script tests/run_all.gd # 统一入口(CI 调用点) +# 单元套件(双载体之一)——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/.tscn -# CI 旁必须配外部 wall-clock 超时(引擎无死锁探测) +# 本地闸必须配外部 wall-clock 超时(引擎无死锁探测;run_all_gates.sh 已内置) timeout 120s godot --headless --script tests/run_all.gd ``` -> 单元/契约套件状态(单元测试与契约层批 2026-09-28 立基,后随迭代 7/8 增补):`_suites()` manifest 已登记实跑 **18 套件**(第 18 套 = 迭代 19 卡④提灯载体批新增 `unit/core/fog_lantern_wiring_test` 提灯装配链〔行加载/schema/只读出口双态/enc 注入面〕——本分支基点早于卡④合并时实测 17 套全绿,合并后 18)——data_config(ConfigDB 加载/进度坐标 ceil 解析/fatal 退出码/统一随机流)、core(秘宝结构冻结/断点续玩/提灯装配链)、battle_settlement(DamageMath 纯函数/DamageController 伤害单据五配方)、contracts(系统间接口契约断言)、presentation(节奏队列/奖励浮层/字体资产/地形瓦片集);致命类独立进程 `expect=1`。期望值一律由表读值推导(零玩法数值字面量)、随机只经 RngService 统一流。 +> 单元/契约套件状态(单元测试与契约层批 2026-09-28 立基,后随迭代 7/8 增补):`_suites()` manifest 已登记实跑 **26 套件**(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)` @@ -65,13 +65,36 @@ timeout 120s godot --headless --script tests/run_all.gd | UI | Manual walkthrough OR interaction test(场景探针为首选) | `tests/evidence/` | | Config/Data | Smoke check pass | `design/qa/` | -## Static Scan(提交前双跑:本地 + CI) +## Static Scan(提交前本地跑) `tests/static_scan.sh` 实现静态扫描域 S1–S8 的可执行子集;规则全集与动机见 `design/dev/architecture/architecture.md`「测试与质量域」节。新违规案例必须**先补扫描规则再修码**。 -## CI +## 本地闸(唯一验收门禁) -Tests run automatically on every push to `main` and on every pull request. -A failed test suite blocks merging. **gdUnit4 安装说明(deferred,届时启用)**: -AssetLib → GdUnit4 → Install → Project Settings → Plugins 勾选 → 重启编辑器 → 确认 `res://addons/gdunit4/`。 +本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh`(全量)是收口唯一入口, +跑完写回执 `game/tests/.gates/receipt.json`,合回 master 由 worktree-guard 校验回执。 +闸清单的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。 + +**判定三硬闸**(各闸一律适用,缺一即判定有洞): +①**退出码为唯一权威判据**——日志里的 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/` 之名同理,里面既有存活面也有 +> 系统级断言与场景探针。判定「是不是冒烟」看语义,不看文件名。 diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 4c91021e..c23b5495 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -1,10 +1,10 @@ # tests/run_all.gd — 单元测试统一入口(双载体的逻辑测试调用点,套件 manifest 协议 D2 AC-2b) -# 用法:godot --headless --path game --script res://tests/run_all.gd (CI 侧包装外部超时:timeout 120s …) +# 用法:godot --headless --path game --script res://tests/run_all.gd (本地闸 02 闸包装外部超时:timeout 120s …) # 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。 # 执行形态:逐套件起独立 Godot 子进程实跑 #(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符, # 每个测试文件自带显式实例化夹具,见 tests/README 协议要点)。 -# 场景探针(.tscn 载体)由 CI/轻闸单独步骤调用,不经本入口。 +# 场景探针(.tscn 载体)由本地闸(probe_gate.sh)单独步骤调用,不经本入口。 extends SceneTree # 脚本错误标记与 tools/run_smoke.py 的 ERROR_MARKERS 同源(GDScript 无脚本错误陷阱 API,