chore: initial commit

This commit is contained in:
DKChan committed 2026-09-14 18:18:49 +08:00
commit ddd2960fa6
317 files changed
+41717

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
# Test Infrastructure — Dungeon Sweeper
**Engine**: Godot 4.7.2-stable(GDScript 单语言)
**Test Framework**: 双载体(ADR-0004)——`--headless --script` 逻辑冒烟 + headless 场景探针;gdUnit4 **deferred**(D2 OQ1 关闭形态,正式 CI 期再议)
**CI**: `.github/workflows/tests.yml`(首次 push 到 GitHub 后自动生效;仓库 2026-09-12 尚未初始化 git)
**Setup date**: 2026-09-12
**参考实现**: `prototypes/dungeon-sweeper-vertical-slice/smoke/`(冒烟 96/96 + 三 UI 探针——正式工程的活样板)
## Directory Layout
```
tests/
unit/ # 隔离单元测试(公式/状态机/纯逻辑)——extends SceneTree + quit(code)
integration/ # 跨系统测试(Core-Core 集成、回放确定性、存档往返)
smoke/ # critical-paths.md——人工 gate 清单(/smoke-check 读取)
evidence/ # 截图与人工签核记录
run_all.gd # CI 冒烟统一入口(套件 manifest 协议,D2 AC-2b)
static_scan.sh # ADR-0004 静态扫描域 S1–S8 可执行子集
```
## Running Tests
```bash
# 逻辑冒烟(双载体之一)——Windows 须用 console 版 exe 取 stdout
godot --headless --script tests/run_all.gd # 统一入口(CI 调用点)
godot --headless --script tests/unit/example_test.gd
# 场景探针(双载体之二)——以探针场景为运行目标,走完整项目引导(autoload 在位)
godot --headless --path . res://tests/probes/<probe>.tscn
# CI 旁必须配外部 wall-clock 超时(ADR-0004:引擎无死锁探测)
timeout 120s godot --headless --script tests/run_all.gd
```
**协议要点**(D2 冒烟协议 + ADR-0004):
- 冒烟脚本 `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 前缀——docs/code-conventions.md)
- **Functions**: `test_[scenario]_[expected]`
- **Example**: `reward_roll_test.gd` → `test_fog_cell_filters_below_tier_min()`
## 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 | `production/qa/smoke-*.md` |
## Static Scan(提交前双跑:本地 + CI)
`tests/static_scan.sh` 实现 ADR-0004 扫描域 S1–S8 的可执行子集;规则全集与动机见
`docs/architecture/adr-0004-test-carrier-and-ci.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/`。
+6
View File
@@ -0,0 +1,6 @@
# tests/evidence/ — 人工测试证据
截图、录屏、人工签核记录。命名:`[日期]-[里程碑]-[检查项].md`。
面向表现/手感的验收(视觉锚三硬约束、格子样张测试、结算屏可读性、呈现义务台账核对)归此处——
自动化探针(场景级)能覆盖的走 `tests/unit` / 探针场景,不能 G-W-T 的走本目录。
+5
View File
@@ -0,0 +1,5 @@
# tests/integration/ — 跨系统测试
承载:Core-Core 集成(真 A3 + 真 B2/B3/B4 容器实例)、回放确定性(同种子步骤流逐行相同——B1 AC-33 形态)、存档/RNG 快照往返(D3 落地后)、A1×A2 双包含端到端。
各 GDD 登记的 deferral 集成用例(以对侧登记为生效条件)随对应系统实现迁入本目录。
+41
View File
@@ -0,0 +1,41 @@
# tests/run_all.gd — CI 冒烟统一入口(ADR-0004 双载体的逻辑冒烟调用点)
# 用法:godot --headless --script tests/run_all.gd (CI 侧包装外部超时:timeout 120s …)
# 协议:套件清单逐条"套件 → 期望退出码"(D2 AC-2b manifest);致命类独立进程。
# 现状:正式 Godot 工程尚未在根目录创建(代码在 prototypes/dungeon-sweeper-vertical-slice/,
# 其 smoke/smoke_all.gd 96/96 为参考实现)——本入口当前仅验证运行协议自身;
# 正式工程落地后,把套件清单登记进 _suites()。
extends SceneTree
func _init() -> void:
print("[run_all] formal smoke entry — protocol check")
if DisplayServer.get_name() != "headless":
push_warning("[run_all] 建议以 --headless 运行(D2 冒烟协议)")
var failures := 0
for suite in _suites():
failures += _run_suite(suite)
if failures > 0:
push_error("[run_all] %d 个套件失败" % failures)
quit(1)
return
print("[run_all] all green (%d suites)" % _suites().size())
quit(0)
# 返回 Array of Dictionary:{ "path": String, "expect": int }
# 正式工程落地后在此登记;探针场景(.tscn 载体)由 CI 单独步骤调用,不经本入口。
func _suites() -> Array:
return [
# {"path": "res://tests/unit/example_test.gd", "expect": 0},
# {"path": "res://tests/unit/reward_economy/reward_roll_test.gd", "expect": 0},
# 致命类(独立进程,期望非 0):
# {"path": "res://tests/unit/d2/fixture_missing_table_test.gd", "expect": 1},
]
func _run_suite(suite: Dictionary) -> int:
# SceneTree 脚本内不能直接以 OS.execute 递归调 godot 的成熟形态留待正式工程
# (需解析 godot 二进制路径);当前协议检查阶段只断言清单形态合法。
var path: String = suite.get("path", "")
if path.is_empty():
push_error("[run_all] suite missing path")
return 1
print("[run_all] registered: %s (expect=%d)" % [path, suite.get("expect", 0)])
return 0
+33
View File
@@ -0,0 +1,33 @@
# Smoke Test: Critical Paths — Dungeon Sweeper
**Purpose**: QA 交接前 15 分钟内人工跑完的关键路径清单。
**Run via**: `/smoke-check`(读本文件)。
**Update**: 新核心系统落地后增补条目。
**分工注记**:自动化冒烟(切片 96/96)覆盖规则正确性;本清单面向**表现与手感**(自动化探针不可 G-W-T 的部分)。
## Core Stability(恒跑)
1. 启动到开始界面无崩溃;开始界面响应全部输入
2. 开战:装板后初始展开可见、小人落位出生区、AP 显示 8
## Core Mechanic
3. 挖掘:单格 1 AP、数字亮起与 8 邻雷数一致、连锁免费撕开一片
4. 插旗/拔旗/挪旗:AP 价目正确、连锁止于旗(旗挡链)属正常
5. 移动:已开格瞬移、**已引爆雷格可通行可读**(工程债呈现半边)
6. 停手:RAP 1:1 转存、下轮 AP 硬重置同图继续
7. 轮末结算:明细逐行呈现、来源标注可读(P2——"这一行是我哪步扫雷带来的"可答)
8. 道具使用窗口:授予段后弹出、至多选一件、满血治疗包 usable_now 禁选态正确
9. 清图:清图奖励兑付 + 完美排雷高光(零误标时双份)+ 换新图;未刷新的停手轮不换图
10. 轮内刷新(方案 A):Active 当场换图、AP 保留、RAP 扣减、每轮 ≤2 次、跨图事实结算完整
## Battle & Level
11. 战斗段:主角先手攻击行(hero_attack 来源)、敌方行为环读怪(姿态↔下一拍)、死亡即不动手
12. 胜负判定与 battle_over 清理全序;库存跨战斗延续、关卡末"剩余道具 ×N 清空"反馈
13. 关卡推进:节点链兑付(奖励点/恢复点)、Boss 房(Dmax 行)、层间 HP 携带
## Data & Perf
14. 数值调整板:**打开可见**(覆盖层层级回归——缺陷⑤)、滑条即时生效、批量模拟可跑
15. 无明显卡顿(60fps 目标);连续 5 分钟无内存异常增长;窗口/结算屏文案中文显示正常(CJK 字体核对)
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# tests/static_scan.sh — ADR-0004 静态扫描域 S1–S8 可执行子集
# 规则全集与动机:docs/architecture/adr-0004-test-carrier-and-ci.md §Decision 3
# 正式工程(core/ 存在)时执行;否则优雅跳过(切片阶段代码在 prototypes/,不受本扫描管辖)。
# 纪律:新违规案例必须先补本脚本规则再修码(docs/code-conventions.md 缺陷-规范对照表)。
set -u
cd "$(dirname "$0")/.."
if [ ! -d core ]; then
echo "[static_scan] core/ 不存在(正式工程未创建)——跳过,退出 0"
exit 0
fi
fail=0
# S1/S2 — 玩法代码禁 randomize()/自建 RandomNumberGenerator/全局随机函数(绕开 RngService 的口子)
hits=$(grep -rnE '\brandomize[[:space:]]*\(|\bRandomNumberGenerator\b|\brandi[[:space:]]*\(|\brandf[[:space:]]*\(|\brandf_range[[:space:]]*\(' \
core presentation --include='*.gd' 2>/dev/null | wc -l)
if [ "$hits" -gt 0 ]; then echo "[static_scan] S1/S2 FAIL: 全局随机/RNG 自建命中 $hits 处"; grep -rnE '\brandomize[[:space:]]*\(|\bRandomNumberGenerator\b|\brandi[[:space:]]*\(|\brandf[[:space:]]*\(|\brandf_range[[:space:]]*\(' core presentation --include='*.gd' 2>/dev/null; fail=1; else echo "[static_scan] S1/S2 ok"; fi
# S4 — core/ 禁 extends Control/Node2D、禁 .tscn preload(headless 债)
hits=$(grep -rnE 'extends[[:space:]]+(Control|Node2D)\b|\.tscn' core --include='*.gd' 2>/dev/null | wc -l)
if [ "$hits" -gt 0 ]; then echo "[static_scan] S4 FAIL: core 引擎/UI 依赖命中 $hits 处"; grep -rnE 'extends[[:space:]]+(Control|Node2D)\b|\.tscn' core --include='*.gd' 2>/dev/null; fail=1; else echo "[static_scan] S4 ok"; fi
# S3 — core/ 禁 ConfigDB 外 data 表加载(schema class_name 直引豁免)
hits=$(grep -rnE '\b(load|preload)[[:space:]]*\(|ResourceLoader\.load' core --include='*.gd' 2>/dev/null \
| grep -v 'res://tests/' | wc -l)
if [ "$hits" -gt 0 ]; then echo "[static_scan] S3 WARNING(人工复核——class_name 直引误报需逐条排除): $hits 处"; grep -rnE '\b(load|preload)[[:space:]]*\(|ResourceLoader\.load' core --include='*.gd' 2>/dev/null; fi
# TODO(正式工程落地后精化为扫描配置并升格为硬失败):
# S5 core/ 禁裸引用 ConfigDB/RngService 全局标识符(--script 模式 parse error;grep 近似误报多,需 AST 级或精确白名单)
# S6 core↔core 信号连接只许方法 Callable(禁 lambda 捕获 self);发射者禁反向引用
# S7 B2/B3/B4 API 符号命中文件 ⊆ B1 文件集(编排唯一性)
# S8 结算路径 await 零命中(B1 结算/编排文件集)
exit $fail
+6
View File
@@ -0,0 +1,6 @@
# tests/unit/ — 隔离单元测试
每系统一个子目录(`reward_economy/`、`board_generator/`…)。协议与命名见 `tests/README.md`。
**活样板**:`example_test.gd`(本目录)——第一个真实测试请以它为模板。
**参考实现**:`prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd`(96 条断言的组织方式)。
+37
View File
@@ -0,0 +1,37 @@
# tests/unit/example_test.gd — 示例测试:项目测试协议的活样板(gate-check 硬性要求 ≥1 示例测试)
# 运行:godot --headless --script tests/unit/example_test.gd
# 协议要点(ADR-0004 + D2 冒烟协议):
# extends SceneTree 以获得 quit(code);断言失败 → push_error + quit(1);全绿 → quit(0)
# --script 模式无 autoload 且无全局标识符(裸引用 ConfigDB = parse error)——
# 涉及表值的测试须显式实例化 ConfigDB,期望由表读值推导不写死
# 第一个真实测试:复制本文件为 tests/unit/<system>/<system>_<feature>_test.gd 并替换占位断言
extends SceneTree
func _init() -> void:
var failures := 0
failures += _check("headless SceneTree 协议可用(quit(code) 通道)", _test_headless_protocol())
failures += _check("命名规范可执行:test_[scenario]_[expected]", _test_naming_convention_followed())
if failures > 0:
push_error("[example_test] %d 项失败" % failures)
quit(1)
return
print("[example_test] all green")
quit(0)
func _check(desc: String, ok: bool) -> int:
if not ok:
push_error("[example_test] FAIL: " + desc)
return 1
print("[example_test] PASS: " + desc)
return 0
func _test_headless_protocol() -> bool:
# 占位断言:验证脚本本身在 SceneTree 上下文可执行。
# 真实测试写法示例(引用不执行):
# var cfg := ConfigDBStub.new() # 显式实例化,非全局标识符
# return cfg.action_economy.ap_per_round >= 1
return true
func _test_naming_convention_followed() -> bool:
# 占位断言:本文件函数名均以 test_ 前缀 + 场景_期望 结构(docs/code-conventions.md)
return true