# tests/integration/cross_system_flow_test.gd — L2 集成用例:跨系统真实流×落盘(批 B 转办块宿主) # 断言面 = 四块 22 条(批 B 断言迁移自 smoke_all.gd,主会话裁决一/二/三/七改判集成,装配批落位; # 标签与 cond 逐字搬运,宿主通道换线 Kit.check——与单元侧同一对账口径): # RNG 事件日志会话落盘 1 条(源 `_initialize` :70,裁决一——RngService 会话日志×磁盘) # 专长断点往返 3 条(源 `_smoke_feats` 基点 :1420–1426,裁决二——AdventureRunner 兑付态×SaveManager 断点快照) # 存档摘要集成 10 条(源 `_smoke_save` ⑨ 基点 :6807–6831,裁决三——level_autoplay 真局×SaveManager 入账×写盘读回) # 试玩记录仪 8 条(源 `_smoke_playtest_logger` 整套,裁决七——PlaytestLogger×BattleOrchestrator 真实结算流×JSONL 落盘) # 协议照集成侧惯例(与单元同入口同协议,tests/README「测试层级」):extends SceneTree+Kit.check 主通道; # --script 模式无 autoload,ConfigDB/RngService 显式实例化构造注入;表加载失败不中止(沿被迁断言面的 # 源语义——诊断在此展开,红由具体断言落,承 G2–G5 同款);冒烟库件迁移期按现路径 preload(账本 §五夹具前提)。 # 驱动代码随条自持、零新增断言(验证集不扩大为绝对约束——前置受理失败自然落到后续断言红)。 # 运行:godot --headless --path game --script res://tests/integration/cross_system_flow_test.gd extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") const ConfigDBScript := preload("res://core/config_db.gd") const LevelAutoplay := preload("res://sim/level_autoplay.gd") # 真局驱动(迁移期按现路径 preload——账本 §五夹具前提) var _db func _init() -> void: _db = ConfigDBScript.new() # 表加载失败不中止(承 G2–G5 同款:表损坏的红落在具体断言上) var load_ok: bool = _db.load_tables("res://data/tables", true) if not load_ok: push_error("[cross_system_flow_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]) # 套执行序沿源编排(_initialize 尾条 → _smoke_feats 断点块 → _smoke_save ⑨ → _smoke_playtest_logger) _rng_event_log_session() _feats_breakpoint_roundtrip() _save_summary_integration() _smoke_playtest_logger() quit(Kit.finish("cross_system_flow_test")) # 冒烟隔离用全新 RngService(同 master 种子——回放确定性纪律;smoke_all.gd helper 的宿主自持私有副本) func _fresh_rng(seed_val: int): var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_val) return rng # ---- ① RNG 事件日志会话落盘(源 `_initialize` :70,主会话裁决一改判集成—— # 驱动沿源 :60–70 取号序列自持:master 种子 42 → 前置取号 → new_adventure 开日志会话 → 会话内取号)---- func _rng_event_log_session() -> void: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(42) rng.open_stream("board_gen") rng.open_stream("encounter") rng.new_adventure(4242) rng.open_stream("gp_drop") Kit.check("事件日志会话落盘(2026-09-27 统一流裁决)", FileAccess.file_exists("user://logs/rng_log_4242.jsonl")) # ---- ② 专长断点往返(源 `_smoke_feats` 基点 :1420–1426 三条,主会话裁决二改判集成—— # 驱动沿源 :721–797 重建:5 行临时表注入+seed 72 起局+兑付 x3+save_manager 装配+ # _on_level_finished(victory)+breakpoint_snapshot 取读+clear_breakpoint; # 起局与兑付两步的受理断言留单元宿主(adventure_feats_test),此处纯驱动——前置失败自然落红)---- func _feats_breakpoint_roundtrip() -> void: var rt := FeatTable.new() rt.rows = [ {"feat_id": "x1", "display_name": "试一", "rarity": "common", "desc": "测试揭示", "cap": 1, "triggers": [{"on": "mine_hit", "do": {"kind": "reveal_tiles", "value": 1}}]}, {"feat_id": "x2", "display_name": "试二", "rarity": "excellent", "desc": "测试回复", "cap": 1, "triggers": [{"on": "mine_hit", "do": {"kind": "heal", "value": 2}}]}, {"feat_id": "x3", "display_name": "试三", "rarity": "rare", "desc": "测试群伤", "cap": 1, "triggers": [{"on": "seal_hit", "do": {"kind": "open_ticket", "channel": "blast", "value": 3}}]}, {"feat_id": "x4", "display_name": "试四", "rarity": "rare", "desc": "测试减伤", "cap": 2, "triggers": [{"on": "damage_taken", "when": [{"fact": "hp_ratio", "op": "lte", "value": 50}], "do": {"kind": "damage_reduce", "value": 2}}]}, {"feat_id": "x5", "display_name": "试五", "rarity": "legendary", "desc": "测试AC", "cap": 1, "triggers": [{"on": "seal_hit", "do": {"kind": "ac_bonus", "value": 1, "mode": "round"}}]}, ] var live_rows: FeatTable = _db.feats _db.feats = rt var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72)) a_r.start_adventure(72, {"first_script": true}) # 源 :723(受理断言留单元宿主) a_r._curio_pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}} a_r.pick_curio("relic", "rare", "x3") # 源 :781(兑付断言留单元宿主) # 断点往返(持有集+余库 feat_id 随行) var bp_path := "user://_smoke_feats_bp.json" # 段首清场(存档域隔离纪律——上轮残留断点先删) if FileAccess.file_exists(bp_path): DirAccess.remove_absolute(bp_path) var sm_r = load("res://core/save_manager.gd").new(bp_path) sm_r.load_from_disk() a_r.set_save_manager(sm_r) a_r._on_level_finished({"result": "victory", "hp": int(a_r.ledger.get("hero_hp", 26)), "node_index": 1, "node_id": 1, "node_type": "shop"}) var bp_r: Dictionary = sm_r.breakpoint_snapshot() Kit.check("断点附随件:relics 持有集入快照(迭代 12 T1 扩列)", (bp_r.get("relics", []) as Array).has("x3")) Kit.check("断点附随件:adventure 计数器账本键在案(T6/T7 扩列)", bp_r.has("adventure_counters")) var bp_deck_x3 := 0 for t in (bp_r.get("bonus_deck", []) as Array): if String((t as Dictionary).get("feat_id", "")) == "x3": bp_deck_x3 += 1 Kit.check("断点余库:已兑付 relic 令牌逐枚还原(x3 零残留——续玩不重复获得)", bp_deck_x3 == 0) sm_r.clear_breakpoint() if FileAccess.file_exists(bp_path): # 段尾清理(与段首清场对称) DirAccess.remove_absolute(bp_path) _db.feats = live_rows # 还原现行表(共享 _db 防污染——后续集成块零感知) # ---- ③ 存档摘要集成(源 `_smoke_save` ⑨ 基点 :6807–6831 十条,主会话裁决三改判集成—— # level_autoplay 真局摘要 → apply 原语 → 域读数 → 写盘读回保持;临时档 user://smoke_save_integration.json # 段首清场+段尾清理(源 _smoke_save 隔离纪律),绝不碰 user://save.json(红线))---- func _save_summary_integration() -> void: var sm_script = load("res://core/save_manager.gd") var path9 := "user://smoke_save_integration.json" # 源 paths[8] if FileAccess.file_exists(path9): DirAccess.remove_absolute(path9) # ⑨ 摘要集成(冒险结构批产物驱动):run_adventure 真局摘要 → apply 原语 → 域读数 → 写盘读回保持 var st9: Dictionary = LevelAutoplay.run_adventure(_db, 42, 900, 400) var f9 = sm_script.new(path9) f9.apply_adventure_summary(st9.get("summary", {})) var ks_run := 0 for li in range(1, int(_db.level.rows.size()) + 1): ks_run += int(_db.meta_boss_keystone_row(li).get("first_kill", 0)) Kit.check("摘要集成:冒险驱动满配通关(seed 42)", String(st9.get("result", "")) == "completed") Kit.check("集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" % f9.keystone_balance(), f9.keystone_balance() == ks_run) Kit.check("集成通关次数表全 1", f9.clear_count(1) == 1 and f9.clear_count(2) == 1 and f9.clear_count(3) == 1) Kit.check("集成三关首杀标记齐", f9.boss_first_kill_marked(1) and f9.boss_first_kill_marked(2) and f9.boss_first_kill_marked(3)) Kit.check("集成 recent 1 条且 seed 42", f9.recent_summaries().size() == 1 and int(f9.recent_summaries()[0].get("master_seed", -1)) == 42) var st9b: Dictionary = LevelAutoplay.run_adventure(_db, 42, 900, 400) f9.apply_adventure_summary(st9b.get("summary", {})) Kit.check("第二局后通关次数全 2", f9.clear_count(1) == 2 and f9.clear_count(2) == 2 and f9.clear_count(3) == 2) Kit.check("第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)", f9.keystone_balance() == 2 * ks_run) Kit.check("集成 recent 长度 2", f9.recent_summaries().size() == 2) Kit.check("集成写盘成功", f9.write_to_disk()) var r9 = sm_script.new(path9) Kit.check("集成写盘读回全量保持", r9.load_from_disk() and r9.keystone_balance() == 2 * ks_run and r9.clear_count(3) == 2 and r9.recent_summaries().size() == 2 and r9.boss_first_kill_marked(3)) if FileAccess.file_exists(path9): # 段尾清理(与段首清场对称——源 _smoke_save 同款) DirAccess.remove_absolute(path9) # ---- ④ 试玩记录仪(源 `_smoke_playtest_logger` 整套八条,主会话裁决七改判集成—— # PlaytestLogger 挂真实结算流:会话文件/逐结算快照/字段齐备/spawn_count 回归/诊断转储/JSONL 回读)---- func _smoke_playtest_logger() -> void: print("--- PlaytestLogger(结算快照 JSONL + 诊断转储)---") var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(20260912) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var logger = load("res://presentation/playtest_logger.gd").new() var path: String = logger.start_session(20260912) logger.attach_battle(settlement) var snaps: Array = [] settlement.settlement_snapshot.connect(func(snap): snaps.append(snap)) Kit.check("会话文件开启(%s)" % path, path.contains("playtest_20260912")) Kit.check("快照战斗启动", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})) var minesweeper = settlement.minesweeper for i in range(2): minesweeper.settle_batch([{"action": "stop"}]) Kit.check("每结算一快照(%d 次结算 → %d 快照)" % [2, snaps.size()], snaps.size() == 2) var s1: Dictionary = snaps[0] var required := ["kind", "level", "depth", "round", "verdict", "hero", "economy", "board", "enemies", "buffs", "round_facts", "spawn_count"] var missing := [] for k in required: if not s1.has(k): missing.append(k) Kit.check("快照字段齐备(缺 %s)" % str(missing), missing.is_empty()) Kit.check("快照含英雄/经济/板序号(板 seq=%d)" % int(s1.board.seq), int(s1.hero.max_hp) == int(_db.hero.max_hp) and s1.economy.has("phase") and s1.board.has("seq")) # spawn_count 回归(playtest-2026-09-12 报告 Bug#1):停手两轮零击杀(主角攻击 6×2 < 24 HP 确定)→ # unit_count 必须 == spawn_count(全灭才允许 <);快照级区分「首轮全灭」vs「空场」的数据面在位 Kit.check("快照 spawn_count 在位且 == unit_count(无击杀轮:%d/%d——全灭/空场可判别)" % [int(s1.get("spawn_count", -1)), int(s1.get("unit_count", -1))], int(s1.get("spawn_count", -1)) >= 1 and int(s1.get("spawn_count", -1)) == int(s1.get("unit_count", -2))) logger.log_action("dig", Vector2i(1, 1), true) var diag: String = logger.dump_diagnosis({"test": true}) Kit.check("诊断转储文件生成(%s)" % diag, diag.contains("diag_")) # JSONL 文件内容验证(重新读取) var f := FileAccess.open(path, FileAccess.READ) var lines := 0 var parse_ok := true while not f.eof_reached(): var line := f.get_line() if line.strip_edges() == "": continue lines += 1 var parsed = JSON.parse_string(line) if parsed == null: parse_ok = false Kit.check("JSONL 全部行可解析(%d 行 = session + 2 结算)" % lines, lines >= 3 and parse_ok) logger.close_session()