Files
gplan-2/design/engineering/test-conventions.md
T

22 KiB
Raw Blame History

测试规范(Dungeon Sweeper)

约束范围:game/probe/ 与 game/sim/、game/tests/ 与一切场景探针的编写与验收。 分工:测试执行许可(什么时候允许跑测试)归 AGENTS.md「硬性红线·测试执行两入口铁规」;跑什么闸、什么顺序归 tools/run_all_gates.sh(脚本即清单,本文与 AGENTS.md 均不另列);本文管测试怎么写、怎么算合格。每件测试工具是什么、判什么、怎么单独跑、什么时候适合单独用——见 game/tests/README.md(测试工具手册),本文不重复工具介绍。 归属:随游戏逻辑写/改的测试=需求改动的一部分(随需求卡片同批落地);不伴随逻辑改动的纯测试维护=工程改动(不写卡,用户发令直改)。

定测试方案前必须通读本文。


1. 测试总纲——"存在"必须证明"可达且正确"

核心层验收标准通过不算有测试:GDScript 编译不查字典字段与时序,呈现层错误只在运行时暴露。 测试必须证明功能可达(用户真的能用到)且行为正确(结果符合验收标准)。

2. 场景级探针标配

每新增一个可交互 UI 区域(视图/覆盖层/面板),必须配套场景级 headless 探针: 真实实例化场景 → 驱动真实输入路径 → 断言可见性与状态。

凡模态覆盖层,探针必须断言:打开后自身 visible 且遮挡物已隐藏(模态被遮挡 = 功能存在但不可达)。

3. 缺陷修复必须沉淀回归断言

每个缺陷修复必须在探针或冒烟中留下能抓住该缺陷的断言(例:封印动作耗尽 AP → 必须进入第 2 轮; 选用道具 → 库存态必须变 Used)。没有回归断言的修复视为未完成。

4. 验证时机(合回后远端 CI 制,2026-11-03 换代)

开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6)。全量验证归合回后的远端 CI:git merge 合回 master 落地后 post-merge 钩子推送 origin master,远端按改动域分流跑 tools/run_all_gates.sh(游戏码批=不带参数档两域全量、单元与集成测试全套件——跨卡触面与关联档选测的盲区由全量兜底;文档批=文档域秒级闸;分流算法单点=tools/ci/domain_route.sh),远端结果用 tools/ci_check.sh <合并提交> 按需现查(读 run 自身 status/conclusion,只呈报、不落盘——合回不等待、不唤醒,process-conventions §1 第 3 条);CI 红态下修复合回落地即再推再验追绿(修复走新开 fix-<任务名> 修复 worktree,process-conventions §1)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。 行为探针使用随机化玩家行为(封印/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。

5. 测试函数怎么写——验收清单条目的落码规范

需求卡片 §8「改动—验收对照表」每一条都要落成可执行的验证;本文定统一写法,卡里定每批的具体内容(测什么/注入哪里)。写卡期查不出执行载体的条目=不可测,当场呈报,禁拖到实现期。

  1. 执行载体五选一,逐条声明:单元测试(tests/run_all)/系统测试断言(tests/system_test.gd)/场景探针(probe/ 下 *_probe)/MC 同种子逐项比对/人工核对(仅限无法自动化的纪律类需求点,如命名锁定、数值占位)。行为类条目以自动化载体为主。

  2. 粒度与对应关系:每条清单条目至少一个自动断言,且断言必须从该条目的真实入口进入;多条条目可合用一个测试函数,但每条都要有自己单独可判红的判据(取证口径见 §8.4 唯一口径——禁变异/禁撤销)。断言失败消息末尾带编号尾缀〔卡短名 Vn〕(如〔秘宝批 V3〕)——断言与卡片清单可互相 grep 查找。Kit.check_quiet 静默收集形态(计数不入判定即打印、失败消息进 problems 汇总)的断言已入 tools/extract_checks.py 扫描面(2026-11-02 提取器改版:三通道同口径提取,静默通道条目 quiet 列记 channel:check_quiet),验收尾缀 grep 对该通道有效。

  3. 断言词汇按验证对象五选一:

    词汇 适用场景
    逐位相等 同种子下确定性公式与结算的精确逐项比对
    区间/差值带 概率与平衡读数(MC 同种子配对比较,出带即红)
    快照比对 存档域结构、配置表行结构
    重放 同种子断点回放、改前改后配对
    状态/计数器断言 条件解锁、限次、充能——置数据→走路径→断计数值
  4. 调试注入两层(能力测试与流程体验两分法的落码面):自动化测试=测试代码直调核心层写口构造状态(直改数据表行/计数器/存档域,不经 UI、不走正常资源积累流程——冒烟现役做法);人工体验=DebugOverlay/DS_DEBUG 调试面,服务用户目检(发布态该面恒隐、无 UI 开启口,开发期临时启用=本地改 _debug_entry 缺省、不提交;能力测试不经 UI,本条口径不受影响)。清单条目的「怎么测」必须写到数据面粒度:改哪张表哪行、置哪个计数、调哪个核心写口。

  5. 换代批旧验证约束同生死:删除旧数据行/旧实现代码的批次,必须同批摘除只验证旧实现的断言与护栏校验,由新断言替代——旧件断言残留=冒烟假绿来源;合并后冒烟全绿且全仓无旧件残留引用为证。

  6. 违约通道类需求点必须落独立负面套件(2026-10-28 立):预期运行期违约/致命路径的需求点(handler 通道、fail-fast 分支)一律落独立负面套件并按 manifest 登记 expect=1, negative=true,禁落现役非负面套件——headless 下 assert 失败必产不可捕获的 SCRIPT ERROR,非负面套件的输出被错误扫描命中即假红(先例=reward_dangling_treasure/counter_prefix_fatal/extended_row_violation/run_time_fatal 四件)。

6. 判定三硬闸——闸承载的构造义务

任何新增或改写的闸承载(闸脚本/批跑编排/探针入口)必须三闸齐备;现行各闸状态登记归架构文档「测试与质量域」节(本文不另列闸件清单):

  1. 退出码为唯一权威判据——输出里的 PASS 串只作附加判据不作判据主体;打印 PASS 后 quit(1) 必须判红。
  2. SCRIPT ERROR / Parse Error 扫描面在位——GDScript 脚本错误不改进程退出码、不计失败数(机制事实见 §7),错误标记必须扫输出,且判据必须覆盖「跑了但半途脚本错误」态——退出码 0 但输出缺预期签名同样判红(防静默退出)。
  3. 断言数下限——零断言的正常返回与断言失败必须区分开(现行做法:Kit.finish 零断言返回专用退出码 2,不与「断言失败=1」混用,否则致命类套件的期望码协议会放过空跑)。
    • 负面套件(测试目标就是产生错误输出)仅可豁免第 2 闸,且须在 manifest 显式标记,其余两闸照判。
    • 子进程输出须经系统命令解释器重定向落盘再读——OS.execute 的输出管道在本机恒空。

新写测试件全类型标注——新写的测试件(冒烟断言/探针/单测)参数、返回值、容器一律带类型标注,与 S9 对产品码的要求同型;存量不强制清扫,被批次触碰时顺带换代。暂不机器闸(S9 扫描面=产品码,probe/sim/tests 不在扫描面是测量面直构惯例的既有裁量)。

7. 判定可靠性优先于覆盖率

  • 「跑一遍没崩=通过」无效:测试绿只证明当前代码没犯这个错,不证明犯了这个错会被抓住;断言存在不等于断言执行。
  • 脚本错误会静默中断所在函数的剩余断言,而套件仍报全绿(机制事实):GDScript 无脚本错误陷阱 API,错误不改进程退出码、不计失败数。三处实证:①「309 项 0 失败」实为断言未跑全;②敌人表行集纪律 10 条断言因定型数组赋值错误从未执行(修复后两条正向见红=夹具本缺锚行);③Callable 形参类型写错致解析函数返回 null、后续断言整段蒸发。
  • 由此产生两条义务:写闸件按 §6 配三硬闸;写验收函数必须自证断言全数执行——真实入口断言在当轮执行并通过(§8.4)为主证,零命中自检(扫描面为空即判红)为辅证。
  • 覆盖率不是目标:不引入行覆盖率度量(Godot 无内建,第三方属新基建);现行补洞=能力清单覆盖域表+人工判定,判定可靠性靠上列自证手段,不靠覆盖率数字。

8. 改动级验收闭合(2026-10-21 立)

8.1 验收单位是行为改动

一个行为改动 = 新增或修改一条函数、一条数据、一处接线、一个生命周期行为、一条玩家可见文案。纯改名、注释、格式化不算行为改动,但必须在对照表里单独列一行写「非行为改动」及判断依据。禁止用「重构」两个字把行为改动藏起来。

8.2 四层与真实入口

  • 单元:不依赖场景树和真实宿主的纯逻辑;测试从领域类的对外方法进出,断言钉对外可观测面(返回值/信号/查询接口/落盘内容)。内部字段直查=受控例外:仅当该行为没有公开可观测面可钉时允许,对照表该行注明理由;存量不回头清扫,被批次触碰时顺带换代。
  • 接口:模块边界的公开契约——数据表加载、存档与断点、事件载荷、命令接口;测试从公开入口进出。接口级断言随单元套件落(unit/contracts/),不另设承载目录。
  • 集成:跨宿主的接线、持久、生命周期——例如 LevelRunner 把 level_counters 传给 BattleOrchestrator、两场战斗共享同一个桶、断点续玩;测试必须实例化真实宿主链,禁止用裸 EffectEngine 代替。
  • 探针/golden:玩家可达性与观感;真实实例化场景、驱动真实输入。

分层不是形式。改动发生在哪一层,验收就必须从那一层的真实入口进入。测试绕过了改动点,就不算这个改动的验收——这正是「断言存在、接线坏了却不红」的来源。

8.3 改动—验收对照表

需求卡片列需求侧对照表;实现报告列实际对照表。每行至少六格:

需求点 改动(文件:行/行为) 验收测试(名称+文件:行+尾缀) 层级 真实入口 判据绑定面

实现报告另加「结果」格(绿/红/未过及依据)。两端都要对上:

  • 正向:每个需求点至少一行;每行必须有真实入口和判据绑定面。缺任一项=该需求点未验收。
  • 反向:diff 里每个行为改动至少一行;没有对应行的改动,必须写明「非行为改动」或「由第 X 行覆盖」。
  • 测试侧:新增或改写的每条断言至少挂一行;挂不上的孤儿断言删掉或补挂到需求点。

验收档申报(2026-11-04 立,自 2026-11-05 起对文件名日期不早于该日的需求卡片生效;旧卡不回头改):每张需求卡片在头部信息区(与设计事实源声明同区)申报一个验收档,三档:高/中/低。判据=失败代价单维——这条改动做错了,后果随存档或结算传播的(存档写读与迁移、RNG 流、战斗结算公式与乘区、经济入账、数值表结构校验)=高;后果玩家可见但不损持久状态的(常规玩法逻辑、门控、交互逻辑、呈现数据面)=中;后果即看即知且无状态残留的(文案、纯呈现样式、布局微调)=低。一张卡触多档按最高档申报;申报由写卡段落笔,用户批卡(U0)时确认或改判。档位只调节 §8.7 审计深度——闸机与 CI 的域划分、选闸算法、全量强制档一概不因档位变化,真实入口、判据绑定面、禁削弱等验收闭合判据全部照旧。未申报档位的卡一律按高档全量审计。机检=tools/pipeline_gate.py 判据 C4:卡内须含「验收档:高/中/低」申报行。

能力清单维护锚(2026-10-29 用户裁改版定稿):game/tests/README.md「能力清单」是实现指导+阅读导航+义务声明——只宣布「要验证什么」,不记「是否已验证/是否跑过」;落地账归现行验收闭合链(改动—验收对照表逐条挂断言+合回前独立验收审计),本清单不承载验证状态。表为四列:能力|系统|真实入口|代表用例(文件:函数)。维护归写卡段,当场入表:写需求卡片时,本批新增能力当场写入能力行——真实入口取自设计文档已定的入口,代表用例=卡里定死的计划测试(文件:函数);受影响存量行(入口/用例名变更)同批更新;能力退役删行。实现照单落名:测试函数名以清单为唯一依据,名实不符=卡片没写清=停卡退回。收尾核对:批末收尾只核对表上名字与实际落地一致(改名则回表),不刷任何批次字段;实现批禁写该文件的现行红线不变(process-conventions §3 第 11 条),核对义务归收尾提交。三层模型见 §8.9。改动—验收对照表纪律不变:新增或改写的每条断言仍须挂一行(上文「测试侧」);断言改判仍受 §8.4 真实入口口径约束。

新增 core 件的覆盖登记义务(2026-11-03 架构审查 P1 余项裁):拆件或重构产出的新 core 文件不触发 process-conventions §3 第 6 条「新功能必配新测试」(那条只管新增玩法/机制),因此单由本条承接——新 core 件要么被 game/tests/ 某套件按文件名或 class_name 点名,要么在承接它的能力行「代表用例」里申报「经宿主链覆盖(宿主=<件>)」;两者都无=批末收尾核对时补登。本条不是覆盖率要求,与 §7「覆盖率不是目标」同轨:登记的是能力义务的归属,不做文件计数——「每个 core 件必须被点名一次」的机器闸已裁否(它逼出为点名而点名的空套件,且与 §7 正面冲突)。落笔面沿用现行分工:申报行由写卡段入能力清单(实现批禁写该文件),核对归批末收尾提交。

8.4 真实入口断言(验收闭合的主判据)

  • 唯一口径(2026-10-24/2026-10-28 用户两裁):每个需求点与每个行为改动的验收=一条从真实入口进入的断言,可 grep 定位、在当轮真实跑批中执行并通过。禁变异类测试(把生产码或数据表改成错值看断言抓不抓得到);禁撤销类取证(回退本批改动或该 hunk 看断言红不红)——两者都是人为制造红,属伪造证据,一律禁止。
  • 判据绑可观测提交面:断言判据要绑「被测改动特有的可观测提交面」(信号发射、事件、入参、绘制记录、状态源取值),不绑多层防御之后的终态——否则改动被下游兜底吸收时终态不变、断言假绿。例:探针「雾不扩可点域」首版只看屏/选路态终态,命中面放宽后断言仍绿;改绑 route_chosen 发射面后即红。
  • 判据绑不上的出口=停卡呈报:某改动确实造不出可观测提交面(纯防御分支、release 独有位)时,走静态判据+代码评审注记并在对照表申报观测面局限;不得自造替代证明。
  • 正面效力自检:精确等式优于不等式与非空即过;扫描类断言带「扫描面 > 0」非空前置,防扫描面为空静默绿。
  • 持久化、生命周期、存档、跨战共享这类接线逐条配断言;判定可靠性细则见 §7。

8.5 禁止削弱测试

实现期不得为了让测试变绿而删除断言、放宽阈值、缩小扫描面、把真实入口换成下层直调。确需改判据=需求卡片没写清=上游缺陷:停该卡、呈报退回写卡段,不在实现期自行决定。卡片可预先声明允许改动的旧断言及理由;未声明的削弱由独立审计判红。

8.6 测试时机

不要求 TDD,不要求先红后绿。允许实现后补测试;但补的测试必须满足 §8.4 的真实入口断言口径。实现完再补、补完就绿,不算验收。

8.7 独立验收审计

合并前由没有参与实现的会话或子代理做一次审计。审计只读需求卡片、实际对照表、diff、测试文件和实现报告,自己跑最窄测试抽查,不采信报告里的「已全绿」。审计至少做四件事:

  1. 逐行核对对照表:测试存在、真的执行、尾缀能 grep 到、真实入口没有绕过改动点;
  2. 抽查判据绑定面:断言从真实入口进入、钉在被测改动的特有可观测提交面上,且当轮真实跑批执行过;
  3. 检查测试削弱:删除断言、放宽阈值、缩小扫描面、换下层直调,逐处要卡片申报或用户批准。
  4. 核对内部字段直查的受控例外:直查行是否已在对照表注明理由(该行为无公开可观测面可钉)。

审计深度分级(2026-11-04 立):审计按卡内申报的验收档(§8.3)逐卡定深度;梯度只落在昂贵的判断件上,机械且堵造假向量的两件(削弱检查、例外核对)三档全保留不抽。中/低档省略的测试复跑以远端 CI 读数为执行证据——tools/ci_check.sh <合并提交> 的现查输出(commit+run 号+conclusion)是机器产物,run 号在远端永久可复核,审计报告照抄该行即可;照抄前先核对读数必须是「绿」——「未验」(零触发批跑闸步全跳过)与「不可判」不作证据,那种批的回补办法是等一次含代码面的合回或用户自发全量回归(2026-11-04 用户裁:回执等待件退役,证据由落盘文件改为现查读数;「不采信实现者自述已全绿」的口径不变):

审计动作 高 中 低
对照表逐行核对 全量 全量 抽查 ≥1/3 行;新增行与改判行必抽
判据绑定面核对 抽查(现行口径) 抽查(现行口径) 抽 1 行
测试削弱检查 逐处 逐处 逐处
直查例外核对 逐条 逐条 逐条
最窄测试复跑 抽查(现行口径) 省略——以远端 CI 现查读数代证 省略——以远端 CI 现查读数代证

低档的抽查面与抽中依据照写审计报告;「每批必有审计、禁零审计批」不变;档位存疑按高档执行。

审计报告落 reports/iter<N>-验收审计-<日期>.md,逐行写「通过/缺漏/例外及依据」,逐卡标注验收档与实际执行深度。未通过不得合并。测试执行两入口铁规不变:审计在迭代 loop 内启动的子代理算 loop;另开会话须用户点名。

8.8 生效与旧卡

本规自 2026-10-21 起对文件名日期不早于该日的需求卡片生效;旧卡不回头改。旧卡在开工前缺对照表的,由写卡段补,补完再开工。

8.9 三层模型(验收意图·义务清单·执行证据,2026-10-29 定稿)

验收体系分三层,各层各管一件事,互不代行:

层 载体 切分轴 一行是什么
验收意图 系统文档「验收标准」节 按 owner 文档 一条玩家或系统可观测的判据
义务清单 game/tests/README.md「能力清单」 按玩家旅程与系统归属 一条待验证义务:能力+真实入口+代表用例(文件:函数)
执行证据 测试套件断言 按代码归属 一条从真实入口进入的断言

四条法则:

  1. 每格非空:每个系统文档有验收标准节;每个能力行有真实入口与至少一个代表用例;每条断言挂在某个需求点或能力行上。
  2. 每条断言有归属:孤儿断言删掉或补挂;不建无归属断言。
  3. 数量不守恒、承接守恒:三层条数天然不等——一条验收意图可由多个能力承接,一条能力义务由多条断言覆盖。要守的是两个满射:每条验收意图至少有一条能力义务承接,每条能力义务至少有一条真实入口断言落地。数量对账只在批内做,见第 4 条。
  4. 无全局对账总账:不建逐断言账本,不做跨层条数对账;对账只在每批的改动—验收对照表里做(§8.3)。历史教训:阶段 2 退役的逐断言账本正因把义务清单层与证据层压成一本账而失修。

维护时序(2026-10-29 改版):能力清单归写卡段维护——写需求卡片时当场入表:本批新增能力当场增行(真实入口取自设计文档已定的入口,代表用例=卡内定死的计划测试 文件:函数),受影响存量行(入口/用例名变更)同批更新,能力退役删行;实现批禁写该文件(process-conventions §3 第 11 条)。实现照单落名:测试函数名以清单为唯一依据,名实不符=卡片没写清=停卡退回。批末收尾只核对表上名字与实际落地一致(改名则回表),不刷任何批次字段——清单不记「是否已验证/是否跑过」,落地账在改动—验收对照表(§8.3)与独立验收审计(§8.7)。

文档纯度:验收意图与能力描述只写玩家或系统可观测面,禁写代码路径(.gd/.py/.sh)、测试文件名、断言名;唯一例外=配置文件索引(game/data/tables/*.tres 这类表路径与表名)。规则正文见 doc-conventions §5「禁实现内容」。

机器闸:本节的结构由文档域闸「能力审计」机检——GDD 纯度(同款禁实现内容口径)、系统文档验收标准节非空、能力行四列与非空、代表用例(文件:函数)与真实入口可核(双通道:代码在或卡在——计划中的用例与入口由需求卡片锚定)、覆盖域表与能力行对得上;红项按新增红处理。闸件清单与域划分的唯一出处=tools/run_all_gates.sh 头部。