①退出码为唯一权威判据:probe_gate.sh 八件探针(轻闸 4 + full 4)判定统一走 verdict 函数, 退出码≠0 即红,PASS 串降级为附加判据(ui_click_probe 本就走退出码语义,签名标记传它自己的 click_probe 串,不强加 PASS)。 ②脚本错误扫描闸件 tools/error_scan.py(标记 import 自 run_smoke.py,禁第二份清单): probe_gate 逐探针段扫日志;run_all 侧因 --script 子进程的 stdout 管道恒空(OS.execute 在本机 Godot/Windows 组合下拿不到输出,且 GUI 子系统 exe 的 stdout/stderr 句柄无效),改为整条命令 交给系统命令解释器重定向落盘再读,退出码原样回传——引擎二进制取 *_console 版(GODOT_BIN 优先)。 ③断言数下限:Kit.finish 在 checks==0 时返回专用退出码 2(不是 1——致命类套件期望 1,返回 1 会被 expect 协议放过);run_all 汇总校验实跑套件数 == manifest 条目数。 ④example_test.gd 退出生产 manifest(协议活样板,文件保留;同类先例 fog_lantern 摘除)。 变异验证(三反例 T0 判绿、T1 后全部判红):零断言套件 exit=2 判红/脚本错误套件命中 SCRIPT ERROR 判红/探针打印 PASS 后 quit(1) 判红「PASS 串不抵」。 本卡附带裁决落地(均已用户裁准): · fx_grant_gp 补进行来源断言豁免表(grant_segment 的专长 GP 入账行无棋盘来源格,每局触发 7 次 断言失败却因脚本错误不改退出码长期漏过;迭代 21 同款修过 fx_reveal)。 · manifest 增 negative 字段,仅 reward_dangling_treasure 一件(坏表是其测试目标,错误扫描误伤)。 · config_db_test 的 rows 定型数组赋值改 rows.assign——原写法触发 Invalid assignment 中断整个函数, 敌人表行集三条纪律 10 条断言从未执行过(31 处 check 实跑 21 条);修复后 26 条全跑,其中两条正向 断言见红,根因是夹具缺 depth-0 锚行(validate 的 anchor 规则),补锚行后全绿。 · system_contracts_test 的 RngStub/UnitContainerStub 空桩换真实实例(定型成员赋值报 type 空)。
122 lines
6.2 KiB
GDScript
122 lines
6.2 KiB
GDScript
# tests/unit/contracts/system_contracts_test.gd — 系统间接口契约断言(宁缺勿错——只断言已定稿行为)
|
||
# 运行:godot --headless --path game --script res://tests/unit/contracts/system_contracts_test.gd
|
||
# 契约线清单来源:design/dev/architecture/architecture.md「API Boundaries」表 + 各系统 AC 前言(规范源)。
|
||
# 覆盖面:
|
||
# 各系统→D2 ConfigDB 类型化单点(TABLE_MANIFEST 词表封闭——伤害单据 kind/目标规则/结账政策常量);
|
||
# D2↔B1 掷点用途标签 ∈ RngService 封闭词表(EC12——settlement.armor_roll_stream 成员性);
|
||
# B1→D1 行协议公共字段形态(open_ticket 九栏缺省 + 协议公共键);
|
||
# B4→B3 增益授予布尔受理口径(域闭 EC1 / 未知 id EC2 / 受理后投影)。
|
||
extends SceneTree
|
||
|
||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||
const RngServiceScript := preload("res://core/rng_service.gd")
|
||
const StatusContainerScript := preload("res://core/status_container.gd")
|
||
const UnitContainerScript := preload("res://core/unit_container.gd")
|
||
const DamageControllerScript := preload("res://core/damage_controller.gd")
|
||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||
|
||
|
||
func _init() -> void:
|
||
var db = ConfigDBScript.new()
|
||
var load_ok: bool = db.load_tables("res://data/tables", true)
|
||
Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||
if load_ok:
|
||
_test_damage_controller_vocabularies()
|
||
_test_rng_tag_closure(db)
|
||
_test_ticket_nine_columns()
|
||
_test_status_boolean_acceptance(db)
|
||
quit(Kit.finish("system_contracts_test"))
|
||
|
||
|
||
# ---- B1 兑付配方词表:五配方/三目标规则/双结账政策——常量非空且互异(封闭枚举负空间) ----
|
||
|
||
func _test_damage_controller_vocabularies() -> void:
|
||
var kinds: Array = [
|
||
DamageControllerScript.KIND_HERO_ATTACK, DamageControllerScript.KIND_ENEMY_SKILL,
|
||
DamageControllerScript.KIND_BOARD_TYPED, DamageControllerScript.KIND_BOARD_BLAST,
|
||
DamageControllerScript.KIND_BOARD_FLAT_HERO,
|
||
]
|
||
var nonempty := true
|
||
for k in kinds:
|
||
if String(k).is_empty():
|
||
nonempty = false
|
||
Kit.check("五配方常量非空(开单面唯一分派键)", nonempty)
|
||
var unique_kinds: Dictionary = {}
|
||
for k in kinds:
|
||
unique_kinds[String(k)] = true
|
||
Kit.check("五配方常量互异(词表封闭无别名)", unique_kinds.size() == kinds.size())
|
||
var targets: Array = [
|
||
DamageControllerScript.TARGET_FRONT_ALIVE, DamageControllerScript.TARGET_HERO,
|
||
DamageControllerScript.TARGET_ALL_ALIVE,
|
||
]
|
||
var unique_targets: Dictionary = {}
|
||
for t in targets:
|
||
unique_targets[String(t)] = true
|
||
Kit.check("三目标规则常量互异(结账时刻现场解析词表)", unique_targets.size() == targets.size())
|
||
Kit.check("双结账政策常量互异(A 即结 / B 攒批回切位)",
|
||
String(DamageControllerScript.POLICY_IMMEDIATE) != String(DamageControllerScript.POLICY_SEGMENT))
|
||
|
||
|
||
# ---- D2↔B1:掷点用途标签 ∈ RngService 封闭词表(EC12——跨系统字面量契约的成员性断言) ----
|
||
|
||
func _test_rng_tag_closure(db) -> void:
|
||
var stream_label: String = String(db.settlement.armor_roll_stream)
|
||
Kit.check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
|
||
RngServiceScript.PURPOSE_TAGS.has(stream_label))
|
||
Kit.check("RngService 用途标签词表非空且互异(封闭语义由流改标签)",
|
||
RngServiceScript.PURPOSE_TAGS.size() > 0)
|
||
|
||
|
||
# ---- B1→D1 行协议:单据九栏缺省齐备 + 协议公共键形态 ----
|
||
|
||
func _test_ticket_nine_columns() -> void:
|
||
var ctrl = _bare_controller()
|
||
var t: Dictionary = ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
|
||
"契约探针", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 0)
|
||
var columns := ["kind", "source_class", "origin_cell", "round_index", "display",
|
||
"channel", "attack_type", "base_value", "target_rule", "rider", "highlight", "context",
|
||
"requires", "source_unit"]
|
||
var all_present := true
|
||
for c in columns:
|
||
if not t.has(c):
|
||
all_present = false
|
||
Kit.check("单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)", all_present)
|
||
Kit.check("缺省无类型轴(attack_type 空 = 恒不查克制矩阵)", String(t.attack_type) == "")
|
||
Kit.check("缺省无前置依赖(requires = -1)", int(t.requires) == -1)
|
||
|
||
|
||
func _bare_controller():
|
||
var db = ConfigDBScript.new()
|
||
# 只取 open_ticket 纯构造面——四个依赖一律传真实实例,不用空桩:
|
||
# DamageController 的 db/rng/units/effects 全是定型标注,传无类型的空桩会触发
|
||
# 「Trying to assign value of type '' to a variable of type '…'」脚本错误——该错误不改退出码,
|
||
# 却让赋值静默失败(成员留 null),脚本错误扫描落地后必红。三者均可零消费构造。
|
||
var units = UnitContainerScript.new(db)
|
||
var effects = StatusContainerScript.new(db)
|
||
return DamageControllerScript.new(db, RngServiceScript.new(), units, effects)
|
||
|
||
|
||
# ---- B4→B3:增益授予布尔受理口径(受理/拒绝二元面 + 投影一致性) ----
|
||
|
||
func _test_status_boolean_acceptance(db) -> void:
|
||
var effects = StatusContainerScript.new(db)
|
||
# EC1 域闭:notify_battle_start 前授予 = false(受理面封闭)
|
||
var any_row: Dictionary = {}
|
||
if db.status_effects.rows.size() > 0:
|
||
any_row = db.status_effects.rows[0]
|
||
Kit.check("前置:状态表非空", not any_row.is_empty())
|
||
if any_row.is_empty():
|
||
return
|
||
var probe_id: String = String(any_row.effect_id)
|
||
var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||
Kit.check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, src) == false)
|
||
Kit.check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", src) == false)
|
||
effects.notify_battle_start()
|
||
Kit.check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, src) == true)
|
||
var active: Array = effects.query_active(String(any_row.target_side))
|
||
var projected := false
|
||
for a in active:
|
||
if String(a.effect_id) == probe_id:
|
||
projected = int(a.effective_value) == int(a.layers) * int(a.snap_magnitude)
|
||
Kit.check("受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)", projected)
|