Files
gplan-2/game/tests/unit/contracts/system_contracts_test.gd
T
DKChan 5f51a916d4 test(unit): T1 单元测试与契约层——七套件实跑入口落 tests/run_all.gd
- data_config: ConfigDB 表加载+跨表护栏零诊断/进度坐标 ceil-clamp 解析与警告去重/变异池门控/shop·meta 消费面/manifest↔目录同步/越域 fatal 退出码(独立进程 expect=1,deferred 触发保 quit 通道)/统一随机流(派生确定性/计数器账本/标签不参与派生/负种子归一化)
- battle_settlement: DamageMath 四纯函数(池乘区/克制分量/掷点查档/吃掷点分量)+DamageController 伤害单据五配方+B/A 双结账模式+来源敌死作废+requires 前置门+主角倒下 halt+AC 合流三通道+池 clamp 与 refresh 政策
- contracts: 五配方/三目标/双政策词表封闭+掷点标签∈RNG 封闭词表(EC12)+单据九栏缺省+增益布尔受理口径(EC1/EC2)
- 红线: 期望值全由表读值推导(零玩法数值字面量)/零自建 RNG(夹具种子仅求复现)
- 轻闸: 冒烟 557/0 + ui_input_probe 十一断言全绿(基线零漂移)
2026-09-28 02:52:19 +08:00

147 lines
6.2 KiB
GDScript
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.
# 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 DamageControllerScript := preload("res://core/damage_controller.gd")
var _failures := 0
var _checks := 0
func _init() -> void:
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
_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)
if _failures > 0:
push_error("[system_contracts_test] %d/%d 项失败" % [_failures, _checks])
quit(1)
return
print("[system_contracts_test] all green (%d checks)" % _checks)
quit(0)
func _check(desc: String, ok: bool) -> void:
_checks += 1
if ok:
print(" [PASS] " + desc)
else:
_failures += 1
push_error(" [FAIL] " + desc)
# ---- 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
_check("五配方常量非空(开单面唯一分派键)", nonempty)
var unique_kinds: Dictionary = {}
for k in kinds:
unique_kinds[String(k)] = true
_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
_check("三目标规则常量互异(结账时刻现场解析词表)", unique_targets.size() == targets.size())
_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)
_check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
RngServiceScript.PURPOSE_TAGS.has(stream_label))
_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
_check("单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)", all_present)
_check("缺省无类型轴(attack_type 空 = 恒不查克制矩阵)", String(t.attack_type) == "")
_check("缺省无前置依赖(requires = -1)", int(t.requires) == -1)
func _bare_controller():
var db = ConfigDBScript.new()
# 只取 open_ticket 纯构造面——容器/依赖传桩(构造期不消费)
var units = UnitContainerStub.new()
var effects = StatusContainerStub.new()
return DamageControllerScript.new(db, RngStub.new(), units, effects)
# 桩容器:构造期零消费(DamageController._init 只 connect unit_died 信号——桩需同名信号)
class UnitContainerStub:
signal unit_died(u: Dictionary)
class StatusContainerStub:
pass
class RngStub:
pass
# ---- 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]
_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}
_check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, src) == false)
_check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", src) == false)
effects.notify_battle_start()
_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)
_check("受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)", projected)