extends SceneTree const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd") var _checks := 0 var _failures := 0 var _runtime: Variant var _clock: Variant var _state: Variant var _main: Control var _path := "user://tests/selection_lifecycle_%d/slot" % OS.get_process_id() func _initialize() -> void: call_deferred("_run") func _expect(ok: bool, message: String) -> void: _checks += 1 if not ok: _failures += 1 push_error(message) func _mount() -> void: _main = load("res://scenes/main/main.tscn").instantiate() _main.set("manual_save_path", _path) root.add_child(_main) _runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false}) await process_frame await process_frame func _build(id: StringName, cell: Vector3i) -> StringName: _main.call("_set_layer", cell.z) _main.call("_select_build_definition", id) var before: int = _runtime.construction_service.get_jobs().size() _main.call("_on_map_cell_clicked", cell) var jobs: Array = _runtime.construction_service.get_jobs() _expect(jobs.size() == before + 1, "normal palette creates %s construction" % id) return jobs.back().job_id if jobs.size() == before + 1 else &"" func _round_trip(kind: StringName, id: StringName, heading: String) -> void: _main.call("_save_manual") var package := SaveService.load_snapshot(_path) _expect(package.get(&"ok", false), "%s selection has a valid package" % kind) _main.set("_selected_entity_id", &"") _main.set("_selected_entity_kind", &"") _main.call("_load_manual", true) _expect(_main.get("_selected_entity_id") == id and _main.get("_selected_entity_kind") == kind, "%s selection restores its actual target" % kind) _expect(heading in _main.get("selected_heading_label").text, "%s inspector restores the target details" % kind) var expected_highlight := &"" if kind == &"excavation" else id _expect(_main.get("world_preview").selected_entity_id == expected_highlight, "%s map selection uses the appropriate highlight" % kind) func _run() -> void: _runtime = root.get_node("SimulationRuntime") _clock = root.get_node("GameClock") _state = root.get_node("GameState") _clock.reset(1, 6, 0) _state.reset() _runtime.initialize(_clock, _state, 42530) _clock.set_paused(true) root.size = Vector2i(1440, 810) await _mount() var reception_job := _build(&"reception", Vector3i(-8, 0, 0)) if reception_job.is_empty(): _finish() return _main.call("_clear_build_selection") _main.call("_on_construction_selected", reception_job) _round_trip(&"construction", reception_job, "工事") _clock.advance_minutes(120) var reception_id: StringName = _runtime.construction_service.get_facilities()[0][&"instance_id"] _main.call("_on_facility_selected", reception_id) _round_trip(&"facility", reception_id, "受付") var stair_job := _build(&"guest_stairs", Vector3i(0, 1, 0)) _clock.advance_minutes(60) _main.call("_clear_build_selection") var stair_id := &"" for row: Dictionary in _runtime.construction_service.get_facilities(): if row[&"definition_id"] == &"guest_stairs": stair_id = row[&"instance_id"] _expect(not stair_job.is_empty() and not stair_id.is_empty(), "paid stair construction finishes without a FacilityService instance") if not stair_id.is_empty(): _main.call("_on_facility_selected", stair_id) _round_trip(&"facility", stair_id, "客用階段") # Rank is an explicit boundary fixture; this suite does not claim natural rank progression. _runtime.submit_command(&"update_progression_metrics", {&"metrics": {&"reputation": 25}}, &"player", &"", 100, -1, &"selection.rank.reputation") _runtime.submit_command(&"change_resources", {&"deltas": {&"gold": 10000}}, &"player", &"", 100, -1, &"selection.rank.capital") _expect(_runtime.submit_command(&"set_world_tree_rank", {&"rank": 2}, &"player", &"", 100, -1, &"selection.rank.promote").is_success(), "boundary fixture unlocks excavation") _main.call("_set_excavation_mode", true) _main.call("_on_map_cell_clicked", Vector3i(0, -1, 0)) var excavations: Array = _runtime.excavation_service.get_jobs() _expect(not excavations.is_empty(), "normal excavation UI creates a saved job") if not excavations.is_empty(): _round_trip(&"excavation", excavations.back()[&"job_id"], "地下掘削") _main.call("_on_facility_selected", reception_id) _expect(_runtime.save_to_path(_path).get(&"ok", false), "save selection through Runtime without explicit metadata") var _audio_cleanup_transition: Dictionary = await _cleanup_fixture_owner(_main, "transition") if not bool(_audio_cleanup_transition.get(&"passed", false)): push_error("Ordinary fixture audio cleanup failed: %s" % str(_audio_cleanup_transition)) quit(96) return await process_frame _expect(_runtime.get_signal_connection_list(&"checkpoint_presentation_requested").is_empty(), "destroyed UI leaves no presentation callback") _expect(_runtime.save_to_path(_path + "_headless").get(&"ok", false), "Runtime still saves with no main scene") var headless := SaveService.load_snapshot(_path + "_headless") _expect(not headless.get(&"manifest", {}).has("interface_selection"), "UI-free save has no stale presentation") await _mount() _expect(_runtime.get_signal_connection_list(&"checkpoint_presentation_requested").size() == 1, "new main scene has exactly one presentation callback") _expect(_runtime.load_from_path(_path).get(&"ok", false) and _main.get("_selected_entity_id") == reception_id, "new main scene receives direct-load selection") # Rotation must preserve each candidate's own selection, rather than the newest UI state. for selected: StringName in [reception_id, stair_id]: _main.call("_on_facility_selected", selected) _expect(_runtime.queue_checkpoint(_path + "_generations", {}, true).get(&"ok", false), "queue an autosave generation") _clock.advance_minutes(1) _expect(_runtime.load_recovery_candidate(_path + "_generations", &"autosave", 1).get(&"ok", false) and _main.get("_selected_entity_id") == reception_id, "older generation restores its reception selection") _expect(_runtime.load_recovery_candidate(_path + "_generations", &"autosave", 0).get(&"ok", false) and _main.get("_selected_entity_id") == stair_id, "newest generation restores its staircase selection") await _test_legacy() _finish() func _test_legacy() -> void: # Materialize a genuine pre-policy world connection through the normal migration contract. _clock.reset(1, 6, 0) _state.reset() _runtime.initialize(_clock, _state, 42531) _clock.set_paused(true) _runtime.world_service.set_walkable(Vector3i(0, 1, 0), true) _expect(_runtime.rebuild_path_graph_from_world(), "old-world boundary fixture has two adjacent floors") var old: Dictionary = _runtime.create_snapshot() var data: Dictionary = old[&"systems"][&"runtime"] data[&"runtime_schema_version"] = 7 data.erase(&"elevators") data.erase(&"legacy_stairs") preload("res://tests/fixtures/pre_upkeep_runtime.gd").strip_empty_upkeep(data) data[&"path"][&"schema_version"] = 3 for field: StringName in [&"vertical_walk_blockers", &"vertical_walk_requires_connector", &"transit_actor_ids"]: data[&"path"].erase(field) _expect(_runtime.restore_snapshot(old), "genuine old world migrates into owned legacy stairs") _main.get("world_preview").set_runtime(_runtime) var records: Array = _main.get("world_preview").get_legacy_stair_records() _expect(not records.is_empty(), "migrated legacy connection is selectable") if not records.is_empty(): var id: StringName = records[0][&"id"] _main.call("_on_legacy_stair_selected", id) _round_trip(&"legacy_stairs", id, "既設の階段") var capture := OS.get_environment("THE_TREE_SELECTION_LIFECYCLE_CAPTURE") if not capture.is_empty(): for frame: int in 5: await process_frame await RenderingServer.frame_post_draw _expect(root.get_texture().get_image().save_png(capture) == OK, "capture migrated staircase selection after actual load") func _finish() -> void: if is_instance_valid(_main): var _audio_cleanup_final: Dictionary = await _cleanup_fixture_owner(_main, "finish") if not bool(_audio_cleanup_final.get(&"passed", false)): push_error("Ordinary fixture audio cleanup failed: %s" % str(_audio_cleanup_final)) quit(96) return for suffix: String in ["", "_headless", "_generations"]: SaveService.delete_save(_path + suffix) print("Selection lifecycle: %d checks, %d failures" % [_checks, _failures]) quit(1 if _failures else 0) ## QA-only control proof routing for sequential ordinary Main owners. ## The old02cd/6be99 helpers and original Clock/Runtime values stay exact. func _cleanup_fixture_owner(owner: Node, phase: String) -> Dictionary: var original_control: String = OS.get_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF") if original_control.is_empty() or not original_control.is_absolute_path(): return {&"passed": false, &"reason": "fresh absolute owned control proof required"} var phase_control: String = original_control.get_basename() + "-" + phase + ".json" if FileAccess.file_exists(phase_control): return {&"passed": false, &"reason": "ordinary owner control proof overwrite forbidden"} OS.set_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF", phase_control) var report: Dictionary = await FixtureCleanup.free_owner_and_wait(self, owner) OS.set_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF", original_control) return report