测试层合并批(用户裁 2026-11-07): 「集成测试」层级退役归并单元层——tests/integration/ 三套件(存档⇄RNG 往返/节点图确定性/跨系统流×落盘)实体迁移 tests/unit/ 同闸同协议, run_all manifest 65 套全量单元; 闸名「单元与集成测试」→「单元测试」(run_all_gates/基线键/coverage_receipts 同步); 选闸词 integration 族→save_rng/cross_system/node_map 文件名词; 测试层识别换三元组(职责×触发时间点×覆盖面积, 权威表=test-conventions §1.1 测试层三元组), 层级名降为代称; 术语表退役词对照+doc_term 收「集成测试/用例/层/侧/宿主/族」, 全仓旧字样清零(gdd/规约/README/套件头注/断言标签), 跨系统面性质归属由各套件头注承载; extract_checks 缺省遍历面收 unit/**; test-conventions §8.2 验收四层「集成」改「跨宿主」; architecture 测试层级表三级换代+65 实数; domain_route/AGENTS 字样同步。凭据: 三套件单跑 exit=0; --code 域闸验收通过(冒烟/单元全量/系统/场景探针/渲染闸/静态扫描/熵增闸全绿); --docs 域闸验收通过(文档域七闸零新增红)

This commit is contained in:
DKChan committed 2026-11-07 01:59:49 +08:00
1 parent 40a3ac1281
commit 9f80609518
28 files changed
+139 -127

No files matched your search

@@ -0,0 +1,150 @@
# tests/unit/node_map_determinism_test.gd — 跨系统真实路径闭环套件:关卡节点图生成·确定性承载
# 断言面 = 生成图确定性套(批 B 断言迁移自 smoke_all.gd,G12 施工组;
# 主会话裁决六整套不拆——跨四系统且走落盘)。
# 跨系统面 = RngService(纯派生流零计数器占用)×
# LevelRunner(start_level/start_level_external 受理·生成图装配·单一图源)×
# AdventureRunner(开局种子承载·断点写读后同图)×
# SaveManager+MetaProgression(首次冒险布尔位收束置位落盘——真实档写盘)。
# 与单元侧同入口同协议:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入;表加载失败不中止(沿被迁断言面的源语义——诊断展开,红由具体断言落)。
# 运行:godot --headless --path game --script res://tests/unit/node_map_determinism_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const SaveManagerScript := preload("res://core/save_manager.gd")
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
var _db
var _bp_path := "user://smoke_node_map_save.json"
func _init() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_db = load("res://core/config_db.gd").new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上)
var load_ok: bool = _db.load_tables("res://data/tables", true)
if not load_ok:
push_error("[node_map_determinism_test] 表加载返回失败——诊断如下(红由具体断言落)")
for d in _db.diagnostics():
if d.severity == "error" or d.severity == "fatal":
push_error(" [%s] %s: %s" % [d.severity, d.category, d.message])
_smoke_generated_map_determinism()
quit(Kit.finish("node_map_determinism_test"))
# ---- 关卡节点图生成·确定性承载断言面 ----
func _smoke_generated_map_determinism() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:134 (起关/选路/断点重生成同图组)
# ① 纯函数复现:图 = f(参数档, 种子)——同档同种子两次产图逐位同图,换种子必变图
var gen = NodeMapGenerator.new(_fresh_rng(910001))
var params2: Dictionary = _db.run.level_gen_params.row_by_level_index(2)
var g_a: Dictionary = gen.generate(params2, 910002)
var g_b: Dictionary = gen.generate(params2, 910002)
var g_c: Dictionary = gen.generate(params2, 910003)
var nodes_a: Array = g_a.get("nodes", [])
Kit.check("同参数档同种子两次产图逐位同图(D2 执行边界)〔v7〕",
nodes_a == (g_b.get("nodes", []) as Array))
Kit.check("同参数档换种子即变图(图确为种子函数,非恒定输出)",
nodes_a != (g_c.get("nodes", []) as Array))
Kit.check("产图节点携图内唯一句柄 id 与自入口 depth(消费侧寻址面)",
_not_empty_graph(nodes_a) and _handles_unique(nodes_a))
# ② 消费侧单一图源:起关装配的图与生成器直取逐位一致(runner 内不存第二份图数据)
var runner1 = load("res://core/level_runner.gd").new(_db, _fresh_rng(910004))
Kit.check("单关起关受理(种子 910004——生成图直供,无兜底图)",
runner1.start_level(2, 910004))
Kit.check("起关生效图 = 生成器同档同种子直取逐位一致(消费侧无第二图源)〔v7〕",
runner1.level_row.get("nodes", []) == (gen.generate(params2, 910004) as Dictionary).get("nodes", []))
# ③ 产图零统一流消费(纯派生流不占计数器号位——断点在任意号位重生成同图的机制承载)
var rng_c = _fresh_rng(910005)
var counter_before: int = rng_c.counter()
var runner2 = load("res://core/level_runner.gd").new(_db, rng_c)
runner2.hero_hp = 20 # 冒险模式生命经主角账预置(旧 ledger 注入参数的等效构造面)
Kit.check("冒险模式起关受理(入口停点)", runner2.start_level_external(3,
Callable(self, "_smoke_rest_stub"), true))
Kit.check("起关产图零计数器消费(同图不依赖取数号位)", rng_c.counter() == counter_before)
var params3: Dictionary = _db.run.level_gen_params.row_by_level_index(3)
var entry_count: int = int(params3.get("entry_count", -1))
Kit.check("入口停点候选数=参数档入口个数(L3 档 %d——多入口首跳即选路,禁写死单入口)〔v2〕" % entry_count,
entry_count >= 1 and (runner2.next_candidates() as Array).size() == entry_count)
if entry_count >= 2:
var eids: Array = runner2.next_candidate_ids()
Kit.check("第二入口经句柄显式进入(句柄 %d)〔v2〕" % int(eids[1]),
runner2.pick_next_node(int(eids[1])) and int(runner2.current_node_id()) == int(eids[1]))
# ④ 开局种子承载:新档(首局)种子 42 两次开局逐位同图,且与生成器直取一致
DirAccess.remove_absolute(_bp_path) # 测试隔离——上轮落盘残留先删,断点面从零档起
var sv_w = SaveManagerScript.new(_bp_path)
var adr_x = AdventureRunnerScript.new(_db, _fresh_rng(42))
adr_x.set_save_manager(sv_w) # 先挂档再开局——停点快照由真实写手落盘
Kit.check("首局口径种子 42 开局受理(defer_entry 路径)", adr_x.start_adventure(42,
{"hero_override": {}, "treasure_unlocked": {}}))
var adr_y = AdventureRunnerScript.new(_db, _fresh_rng(42))
Kit.check("同种子第二次开局受理", adr_y.start_adventure(42,
{"hero_override": {}, "treasure_unlocked": {}}))
Kit.check("种子 42 两次开局图逐位同图(教学局恒同图——首局固定种子的确定性承载)〔v7〕",
adr_x.runner().nodes() == adr_y.runner().nodes())
Kit.check("开局图 = L1 参数档在种子 42 下的生成图(开局无隐式取数分叉)",
adr_x.runner().nodes() == (gen.generate(_db.run.level_gen_params.row_by_level_index(1), 42) as Dictionary).get("nodes", []))
# ⑤ 断点写→恢复→再产图同图(续玩按主种子+参数档重生成,断点不存图快照)
Kit.check("停点断点落盘在档", sv_w.has_breakpoint())
var bp: Dictionary = sv_w.breakpoint_snapshot()
var adr_z = AdventureRunnerScript.new(_db, load("res://core/rng_service.gd").new())
Kit.check("断点续玩受理(代差标记键在档)〔v8〕", adr_z.resume_adventure(bp,
{"hero_override": {}, "treasure_unlocked": {}}))
Kit.check("续玩重生成图与中断前逐位同图〔v7〕", adr_z.runner().nodes() == adr_x.runner().nodes())
var adr_old = AdventureRunnerScript.new(_db, load("res://core/rng_service.gd").new())
var bp_old: Dictionary = bp.duplicate(true)
bp_old.erase("graph_gen_v2") # 换代前旧断点形态(模板池时代无代差标记键)
Kit.check("缺生成图代差标记键的旧断点被显式拒绝(禁按新语义静默回放)〔v8〕",
adr_old.resume_adventure(bp_old, {}) == false)
# ⑥ 关卡表不再携带图数据(图源换代后 level 行只剩行级字段)
var tpl_free := true
for li in range(1, _db.run.level.level_count() + 1):
if (_db.run.level.row_by_level_index(li) as Dictionary).has("templates"):
tpl_free = false
Kit.check("level 表三行均无 templates 键(模板数据已清出)〔v9〕",
tpl_free and _db.run.level.level_count() == 3)
# ⑦ 收束置位:新档 false → settle 置 true 恰一次(幂等)
var sv = SaveManagerScript.new(_bp_path + ".flag.json")
sv.load_from_disk()
Kit.check("新档首次冒险布尔位 = false(默认域——首局恒走固定种子 42)",
sv.meta_state.career_state().first_adventure_done == false)
var mp = load("res://core/meta_progression.gd").new(_db, sv)
mp.settle_adventure({"levels_cleared": 1, "keystone_total": 10, "cleared": false, "result": "failed"})
Kit.check("冒险收束点置位(三结局同写——写入只在收束挂点)", sv.meta_state.career_state().first_adventure_done == true)
mp.settle_adventure({"levels_cleared": 0, "keystone_total": 0, "cleared": false, "result": "failed"})
Kit.check("置位幂等(重复收束不翻转)", sv.meta_state.career_state().first_adventure_done == true)
DirAccess.remove_absolute(_bp_path)
DirAccess.remove_absolute(_bp_path + ".flag.json")
# 图数据良构前置(防空集假绿——节点数 > 1、句柄唯一、depth 与 next 键在位)
func _not_empty_graph(nodes: Array) -> bool:
if nodes.size() < 2:
return false
for node in nodes:
var n := node as Dictionary
if not n.has("id") or not n.has("depth") or not n.has("next"):
return false
return true
func _handles_unique(nodes: Array) -> bool:
var seen := {}
for node in nodes:
var id: int = int((node as Dictionary).get("id", -1))
if id < 0 or seen.has(id):
return false
seen[id] = true
return true
func _fresh_rng(seed_val: int):
var r = load("res://core/rng_service.gd").new()
r.new_adventure(seed_val)
return r
# ---- 恢复执行体(LevelRunner 注入用——与 AdventureRunner.apply_rest 同式占位;
# smoke_all.gd helper 的宿主自持私有副本——源文件收尾整体删除,宿主自给自足)----
func _smoke_rest_stub(pct: int, hp: int, max_hp: int) -> int:
return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp)