# 致命类用例(独立进程,期望退出码 1) # reward_dangling_aux——RewardTable aux 投放行双表校验(迭代 7 T1):aux 行 effect_id 引用 retired # 道具(loot_compass——2026-09-29 拍板①乙退役)时,config_db 跨表校验必须报 reward_dangling_aux、 # load_tables 整体拒绝(退役件/失义件不得出现在掉落投放面)。 # 运行:godot --headless --path game --script res://tests/unit/data_config/reward_dangling_aux_test.gd # 退出码协议:1 = 校验正确拒绝(预期);2 = 违规表被放行(回归信号)。 extends SceneTree const ConfigDBScript := preload("res://core/config_db.gd") const SRC_DIR := "res://data/tables" const BAD_DIR := "user://aux_fixture_dangling_aux" func _copy_tables() -> bool: DirAccess.make_dir_recursive_absolute(BAD_DIR) var dir := DirAccess.open(SRC_DIR) if dir == null: return false dir.list_dir_begin() var f := dir.get_next() while f != "": if not dir.current_is_dir(): if dir.copy(SRC_DIR + "/" + f, BAD_DIR + "/" + f) != OK: return false f = dir.get_next() return true func _inject_violation() -> bool: var path := BAD_DIR + "/reward.tres" var f := FileAccess.open(path, FileAccess.READ) if f == null: return false var s := f.get_as_text() f.close() # 工兵令投放行 → 引用已退役的 loot_compass:双表校验须拦(retired 不入投放面) var blocks: PackedStringArray = s.split("}, {") var hit := false for i in blocks.size(): if '"reward_id": "aux_sapper_order"' in blocks[i]: blocks[i] = blocks[i].replace('"effect_id": "sapper_order"', '"effect_id": "loot_compass"') hit = true s = "}, {".join(blocks) var w := FileAccess.open(path, FileAccess.WRITE) if w == null or not hit: return false w.store_string(s) w.close() return true func _init() -> void: if not _copy_tables() or not _inject_violation(): push_error("[reward_dangling_aux_test] 夹具目录准备失败") quit(2) return var db = ConfigDBScript.new() var ok: bool = db.load_tables(BAD_DIR, false) if ok: push_error("[reward_dangling_aux_test] 违规表被放行——aux 双表校验失守(回归)") quit(2) return if db.diagnostics_count("error") >= 1: print("[reward_dangling_aux_test] 双表校验触发(reward_dangling_aux 诊断在案,加载被拒)") quit(1) return push_error("[reward_dangling_aux_test] 加载被拒但无 reward_dangling_aux 诊断——校验通道静默化(回归)") quit(2)