Files
gplan-2/design/dev/architecture/adr-0004-test-carrier-and-ci.md
T

133 lines
8.3 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.
# ADR-0004: 测试载体与 CI(Test Carrier & CI)
## Status
Accepted(2026-09-12,solo 模式用户授权代决;迭代期可推翻重议)
## Date
2026-09-12
## Engine Compatibility
| Field | Value |
|-------|-------|
| **Engine** | Godot 4.7.2-stable |
| **Domain** | Core / Scripting(测试与工具链) |
| **Knowledge Risk** | LOW(--script/探针模式为 4.x 基线;godot-specialist 校验通过,含两条规则升格) |
| **References Consulted** | `docs/engine-reference/godot/`(四文件);godotengine/godot#6360(--script 模式不加载 autoload + 不注册全局标识符——官方立场) |
| **Post-Cutoff APIs Used** | None |
| **Verification Required** | None(切片 88/88 + 三探针已实证;CI 外部超时在正式 CI 接入时核对) |
## ADR Dependencies
| Field | Value |
|-------|-------|
| **Depends On** | ADR-0003(core 隔离规则是扫描域的输入) |
| **Enables** | 全部 GDD AC 的可执行化;正式 CI 接入 |
| **Blocks** | `/gate-check` 前置项之一(`/test-setup` 以本 ADR 为基线) |
| **Ordering Note** | gdUnit4 复议登记为 D2 OQ1 的关闭形态(推迟,非否决) |
## Context
### Problem Statement
D2 GDD 定下 `--headless --script` 冒烟载体(脚本 extends SceneTree、autoload 显式实例化),D2 OQ1 留下"gdUnit4 是否引入"待决;切片新增了场景级 UI 探针形态;项目级静态扫描域散落在各 GDD AC(AC-9/17/21/22/43 等)需要一处统一登记。
### Constraints
- headless 全套 < 30s 预算(technical-preferences)
- 致命类用例独立进程协议("调用 → 期望退出码" runner manifest,D2 AC-2b 起)
- 呈现层测试必须有承载(切片教训:已定稿 AC 全覆盖 core、呈现层零测试承载 → 缺陷 ①②④⑤ 漏网)
### Requirements
- 双模式边界明确:逻辑冒烟(无 autoload)vs 场景探针(完整项目引导)
- 静态扫描规则统一载体,覆盖 D2/A1–B4 各 AC 登记的扫描项
- CI 可接入且防挂死
## Decision
**双载体 + 统一静态扫描域;gdUnit4 推迟到正式 CI 期。**
**1. 逻辑冒烟(`--headless --script`)**:脚本 extends SceneTree 以获得 `quit(code)`;ConfigDB/RngService 由脚本显式实例化(引擎事实:--script 模式**不加载 autoload 且不注册全局标识符**——裸引用 = parse error 编译期即炸,GH #6360 官方确认)。Windows 用 console 版 exe 取 stdout。承载:D2/A1–B4 全部纯逻辑 AC + 致命类独立进程 manifest。
**2. 场景探针(headless 跑 .tscn 探针场景)**:以探针场景为运行目标(`godot --headless --path . res://smoke/<probe>.tscn`)走正常项目引导——autoload 在位、真实 SceneTree、`await process_frame` 有效。承载:呈现层场景级测试(切片三探针 ui_probe / ui_click_probe / ui_tune_probe 为先例)。
- **口径修正(specialist 校验)**:click_probe 走 `BoardView.click_cell` 是"真实翻译出口",**绕过 Viewport GUI 管线**(无命中测试/悬停/焦点)。表述定为"合成输入走真实翻译出口";真正的端到端输入测试 = `Input.parse_input_event()` / `Viewport.push_input` 带坐标——若 ADR-0003 的 _draw 迁移执行(Button 命中测试消失),push_input 探针从加分项变为**必需项**(登记入迁移工作项)。
**3. 项目级静态扫描域(统一登记,CI 与冒烟前置双跑)**:
| # | 规则 | 来源/动机 |
|---|---|---|
| S1 | 玩法代码禁 `randomize()`、自建 `RandomNumberGenerator` | D2 Rule 5(既有) |
| S2 | **禁全局随机函数 `randi()` / `randf()` / `randf_range()`**(全局 RNG 绕开 RngService——specialist 补登漏洞) | D2 Rule 5 补全 |
| S3 | core/ 禁 ConfigDB 外 data 表加载(`load(` / `preload(` / `ResourceLoader.load`;schema class_name 直引豁免) | D2 Rule 2(既有) |
| S4 | core/ 禁 `extends Control/Node2D`、禁 `.tscn` preload | D2 Rule 7(既有) |
| S5 | **core/ 禁裸引用 autoload 全局标识符**(`ConfigDB` / `RngService` 直接写 = script 模式 parse error)——构造注入 only | specialist 升格(切片已合规,规则防复发) |
| S6 | core↔core 信号连接只许方法 Callable(禁 lambda 捕获 self);发射者禁持有订阅者反向引用 | ADR-0001(防循环引用/生命周期延长) |
| S7 | B1 编排唯一性:B2/B3/B4 API 符号命中文件 ⊆ B1 文件集;A2/B3/B4 零相互直调 | B1 AC-43(既有) |
| S8 | 结算路径 `await` 零命中(B1 结算/编排文件集);字面量扫描 = warning 级 + 人工三要素复核(D2 AC-14 口径) | B1(既有) |
**4. CI 防挂死**:任何探针/冒烟调用旁必须配**外部 wall-clock 超时**(命令行 timeout)——引擎自身无死锁探测;探针内 30 帧停滞检测(click_probe 先例)是第一道、外部超时是最后防线。headless 全套 < 30s 预算不变。
**5. gdUnit4:推迟到正式 CI 期再议**(D2 OQ1 以"deferred"关闭)。它运行在项目模式(autoload 在位),与 SceneTree 冒烟机制正交,引入不废弃既有用例。
### Architecture Diagram
```
提交 → [静态扫描 S1–S8(秒级)] → [逻辑冒烟 --script(独立进程 manifest,<30s)]
→ [场景探针 ×N(headless + 外部超时)] → 全绿放行
```
### Key Interfaces
- 冒烟 runner manifest:条目 = "调用 → 期望退出码"(独立进程、游戏通道)——D2 AC-2b 协议延续
- 探针退出码:0 = PASS;非 0 = 失败类别;停滞 quit(2)
## Alternatives Considered
### Alternative 1: 立即引入 gdUnit4 并迁移全部冒烟用例
- **Pros**: 标准 assertions/参数化/报告;编辑器面板。
- **Cons**: 88 条用例迁移成本即刻发生;切片框架已验证且快(~3s);MVP 期无 CI 基建需求。
- **Rejection Reason**: 收益在 CI 期才兑现——推迟(非否决),D2 OQ1 关闭形态。
### Alternative 2: 仅保留逻辑冒烟、UI 靠人工
- **Cons**: 切片缺陷 ①②④⑤ 全部漏过既有 AC 的教训直接否定——呈现层无承载 = 呈现层缺陷零防线。
- **Rejection Reason**: 呈现层契约测试是 code-conventions 硬性要求。
## Consequences
### Positive
- 双模式边界以引擎事实固化(parse 期炸 vs 完整引导),不再靠约定俗成
- S2/S5/S6 三条 specialist 补登规则堵住静默翻车路径(全局随机绕开、裸引用编译炸、lambda 成环)
- 呈现层探针形态沉淀为项目标配(code-conventions 承载)
### Negative
- 静态扫描规则 8 条的维护成本(新规则须挂真实反面案例——code-conventions 纪律)
- "真实 UI 路径"表述收窄后,端到端输入覆盖存在已知缺口(_draw 迁移前可接受)
### Risks
- 未被停滞检测覆盖的死锁挂死 CI → 缓解:外部 wall-clock 超时为硬性要求
- 扫描规则漏报(启发式)→ 缓解:行为级硬证据并行(可回放断言、headless 实跑合成判定——D2 AC-9/10 模式)
## GDD Requirements Addressed
| GDD System | Requirement | How This ADR Addresses It |
|------------|-------------|--------------------------|
| d2-data-config.md | Rule 6 校验双通道;AC-2b 独立进程协议;OQ1 gdUnit4 | 冒烟框架定稿;manifest 延续;OQ1 关闭为 deferred |
| a1–b1 各 GDD | 各自静态扫描 AC(AC-9/17/18/19/21/22/43 模式) | S1–S8 统一登记为项目级域,各 AC 引用本 ADR |
| docs/code-conventions.md | 场景探针标配 / 提交前全量 | 双载体 + CI 链路承载 |
| b1-settlement.md | AC-43 编排唯一性单向扫描 | S7 载体 |
## Performance Implications
- **CPU**: 全套冒烟 ~3s(切片实测),预算 <30s 富余
- **Memory**: headless dummy server 极低
- **Load Time**: 探针场景引导一次性
- **Network**: 不适用
## Migration Plan
切片 smoke/ 目录即载体(零迁移);正式 CI 接入时补外部超时包装与 S1–S8 脚本化;`/test-setup` 以本 ADR 为基线脚手架。
## Validation Criteria
- 冒烟 + 探针全绿为提交门(code-conventions"提交前全量")
- 扫描域脚本化后:新增违规案例必须先补扫描规则再修码(缺陷-规范对照表纪律)
- `<30s` 预算在 CI 环境复核
## Related Decisions
- ADR-0001(S6 规则来源)、ADR-0003(S4/S5 规则来源、探针形态耦合)
- docs/code-conventions.md(缺陷-规范对照表)、各 GDD AC 前言(规范源)