Files
TheTree_mock/tests/s6/test_runtime_save_lifecycle.gd
T
Takeshi 5dde0c5ec2 Record original-alignment implementation and traceability checkpoint
Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
2026-09-30 23:52:24 +09:00

698 lines
30 KiB
GDScript

extends SceneTree
const TEST_PREFIX := "user://saves/s6_runtime_lifecycle"
const PRODUCTION_AUTOSAVE_PATH := "user://saves/autosave"
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S6: runtime save lifecycle and recovery safety")
await _test_autoload_isolation_and_main_opt_in()
_test_disabled_policy_and_path_timing()
_test_queued_checkpoint_at_tick_boundary()
_test_daily_checkpoint_dedup_generations_and_failure_preservation()
_test_exact_round_trip_recovery_pause_and_performance()
_test_domain_invalid_formal_falls_back_to_backup()
_test_multiple_domain_invalid_candidates_fall_back_to_autosave()
_test_all_domain_invalid_candidates_preserve_complete_live_state()
_test_content_filter_cannot_be_weakened()
_test_whitelisted_recovery_repairs()
_test_non_whitelisted_corruption_is_rejected_atomically()
_cleanup_runtime()
_cleanup_test_saves()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_autoload_isolation_and_main_opt_in() -> void:
var app_clock := root.get_node("GameClock")
var app_state := root.get_node("GameState")
var app_runtime := root.get_node("SimulationRuntime")
app_runtime.configure_save_policy({&"enabled": false})
var default_generation := SaveService.get_autosave_path(
PRODUCTION_AUTOSAVE_PATH,
0
)
var before := _package_fingerprint(default_generation)
app_clock.reset(2, 5, 59)
app_state.reset()
app_runtime.initialize(app_clock, app_state, 60_001)
_expect(not app_runtime.get_save_policy()[&"enabled"], "autoload test runtime starts with disk autosave disabled")
app_clock.advance_one_minute()
_expect(_package_fingerprint(default_generation) == before, "disabled autoload reaches 06:00 without touching the production autosave")
var main := load("res://scenes/main/main.tscn").instantiate() as Control
root.add_child(main)
await process_frame
_expect(
app_runtime.get_save_policy()[&"enabled"]
and app_runtime.get_save_policy()[&"autosave_path"] == PRODUCTION_AUTOSAVE_PATH,
"playable main scene explicitly opts into the production autosave policy"
)
app_runtime.configure_save_policy({&"enabled": false})
main.queue_free()
await process_frame
func _test_disabled_policy_and_path_timing() -> void:
_reset_runtime(60_002, 1, 5, 59)
var path := TEST_PREFIX + "_disabled"
SaveService.delete_save(path)
_expect(
not _runtime.get_save_policy()[&"enabled"]
and _runtime.get_save_policy()[&"autosave_path"] == PRODUCTION_AUTOSAVE_PATH,
"direct Runtime instances remain isolated until explicitly enabled"
)
_clock.advance_one_minute()
_expect(
not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(SaveService.get_autosave_path(path, 0))),
"disabled policy writes no implicit autosave"
)
var before_count := int(
_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"]
)
_runtime.path_service.find_path(
Vector3i(-2, 0, WorldService.LAYER_FRONT),
Vector3i(2, 0, WorldService.LAYER_BACK),
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK],
[&"staff"]
)
var after_count := int(
_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"]
)
_expect(after_count == before_count + 1, "one public path query records one Runtime performance sample")
var snapshot = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(snapshot), "Runtime restores before reconnecting the Path timing boundary")
before_count = int(_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"])
_runtime.path_service.find_path(Vector3i(-1, 0, 0), Vector3i(1, 0, 0))
after_count = int(_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"])
_expect(after_count == before_count + 1, "restore reconnects Path timing without duplicate signal delivery")
func _test_queued_checkpoint_at_tick_boundary() -> void:
_reset_runtime(60_003)
var path := TEST_PREFIX + "_queued"
SaveService.delete_save(path)
var queued = _runtime.queue_manual_checkpoint(path, {&"display_name": "Queued Runtime"})
_expect(queued[&"ok"] and _runtime.get_pending_checkpoint_count() == 1, "manual checkpoint is queued for the next fixed tick boundary")
_expect(not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(path)), "queued checkpoint does not write before its boundary")
_clock.advance_one_minute()
var loaded := SaveService.load_snapshot(path)
_expect(loaded[&"ok"] and _runtime.get_pending_checkpoint_count() == 0, "queued checkpoint writes exactly once at PUBLISH_SNAPSHOT")
_expect(int(loaded[&"snapshot"][&"clock"][&"absolute_minute"]) == 361, "queued package captures the completed minute boundary")
_expect(_has_domain_event(&"SAVE_CHECKPOINT_SUCCEEDED"), "successful manual checkpoint publishes a DomainEvent")
SaveService.delete_save(path)
func _test_daily_checkpoint_dedup_generations_and_failure_preservation() -> void:
_reset_runtime(60_004, 1, 5, 59)
var path := TEST_PREFIX + "_daily"
SaveService.delete_save(path)
var configured = _runtime.configure_save_policy({
&"enabled": true,
&"daily_checkpoint_enabled": true,
&"autosave_path": path,
&"autosave_generations": 3,
})
_expect(configured[&"ok"], "daily Runtime save policy accepts a safe explicit slot")
_clock.advance_one_minute()
_expect(_runtime.get_last_daily_checkpoint_day() == 1, "day 1 checkpoint runs at exactly 06:00")
var generation_zero := SaveService.load_snapshot(SaveService.get_autosave_path(path, 0))
_expect(generation_zero[&"ok"] and int(generation_zero[&"snapshot"][&"clock"][&"absolute_minute"]) == 360, "first daily checkpoint becomes autosave generation zero")
_runtime.call("_process_save_checkpoints", 360)
_expect(not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(SaveService.get_autosave_path(path, 1))), "same-day 06:00 callback is deduplicated")
_clock.advance_minutes(SimulationKernel.MINUTES_PER_DAY)
_clock.advance_minutes(SimulationKernel.MINUTES_PER_DAY)
var expected_minutes := [3240, 1800, 360]
var all_generations_match := true
for generation in range(3):
var loaded := SaveService.load_snapshot(SaveService.get_autosave_path(path, generation))
all_generations_match = all_generations_match \
and loaded[&"ok"] \
and int(loaded[&"snapshot"][&"clock"][&"absolute_minute"]) == expected_minutes[generation]
_expect(all_generations_match, "daily checkpoints rotate three deterministic newest-first generations")
var newest_world := ProjectSettings.globalize_path(
SaveService.get_autosave_path(path, 0)
).path_join(SaveService.WORLD_FILE)
var newest_before := FileAccess.get_md5(newest_world)
var pending_path := ProjectSettings.globalize_path(path + ".autosave.pending")
DirAccess.make_dir_recursive_absolute(pending_path)
var sentinel := FileAccess.open(pending_path.path_join("not_a_package.txt"), FileAccess.WRITE)
sentinel.store_string("block promotion")
sentinel = null
_clock.advance_minutes(SimulationKernel.MINUTES_PER_DAY)
_expect(
not _runtime.latest_save_operation_result[&"ok"]
and _runtime.get_last_daily_checkpoint_day() == 3,
"failed day 4 checkpoint remains eligible for retry instead of being marked complete"
)
_expect(FileAccess.get_md5(newest_world) == newest_before, "failed autosave leaves the previous newest generation byte-identical")
_expect(_has_domain_event(&"AUTOSAVE_CHECKPOINT_FAILED"), "autosave failure publishes a structured DomainEvent")
DirAccess.remove_absolute(pending_path.path_join("not_a_package.txt"))
DirAccess.remove_absolute(pending_path)
SaveService.delete_save(path)
func _test_exact_round_trip_recovery_pause_and_performance() -> void:
_reset_runtime(60_005, 3, 7, 15)
var path := TEST_PREFIX + "_round_trip"
SaveService.delete_save(path)
_clock.set_speed(3)
var expected_snapshot = _runtime.create_snapshot()
var expected_digest := SimulationDigest.compute(expected_snapshot)
var first_save = _runtime.save_to_path(path, {&"display_name": "Runtime Exact"})
_expect(first_save[&"ok"] and not _clock.paused, "save pauses I/O temporarily and restores the running clock")
_state.add_resource(&"faith", 77)
_clock.advance_minutes(5)
var loaded = _runtime.load_from_path(path)
_expect(loaded[&"ok"] and _runtime.latest_digest == expected_digest, "ordinary Runtime load restores the exact persisted snapshot")
_expect(_clock.absolute_minute == int(expected_snapshot[&"clock"][&"absolute_minute"]) and not _clock.paused, "load preserves the saved clock minute and pause state")
var minute_before_process = _clock.absolute_minute
_expect(_clock.process_real_delta(10.0) == 0 and _clock.absolute_minute == minute_before_process, "first process frame after load never advances an extra minute")
var second_save = _runtime.save_to_path(path, {&"display_name": "Runtime New Formal"})
_expect(second_save[&"ok"], "second formal Runtime save creates a verified prior-generation backup")
_corrupt_world(path)
_state.add_resource(&"faith", 5)
var inventory = _runtime.inspect_recovery_candidates(path)
_expect(inventory[&"ok"] and inventory[&"selected"][&"id"] == &"backup", "explicit candidate inventory identifies the verified backup")
var recovered = _runtime.load_with_recovery(path)
_expect(recovered[&"ok"] and recovered[&"selected"][&"id"] == &"backup", "Runtime recovery skips a corrupt formal package")
_expect(_runtime.latest_digest == expected_digest, "recovery loads the exact prior formal snapshot without promotion")
_expect(recovered[&"recovery_report"][&"restore_committed"] and _has_domain_event(&"SAVE_RECOVERY_LOADED"), "successful recovery returns a structured report and publishes a DomainEvent")
var explicit = _runtime.load_recovery_candidate(path, &"backup")
_expect(explicit[&"ok"] and explicit[&"read_only"], "caller can explicitly load one named recovery candidate")
_expect(int(_clock.performance_budget.get_report(&"save")[&"lifetime_sample_count"]) >= 2, "Runtime records actual save operation timing")
_expect(int(_clock.performance_budget.get_report(&"load")[&"lifetime_sample_count"]) >= 3, "Runtime records load, inventory and recovery timing")
SaveService.delete_save(path)
func _test_domain_invalid_formal_falls_back_to_backup() -> void:
_reset_runtime(60_009, 2, 8, 0)
var path := TEST_PREFIX + "_domain_backup"
SaveService.delete_save(path)
var backup_snapshot: Dictionary = _runtime.create_snapshot()
var backup_digest := SimulationDigest.compute(backup_snapshot)
_expect(
SaveService.save_snapshot(path, backup_snapshot)[&"ok"],
"domain fallback fixture writes a valid prior formal package"
)
var invalid_formal: Dictionary = backup_snapshot.duplicate(true)
invalid_formal[&"game_state"][&"resources"][&"gold"] = int(
invalid_formal[&"game_state"][&"resources"][&"gold"]
) + 1
_expect(
SaveService.save_snapshot(path, invalid_formal)[&"ok"],
"checksum-valid but ledger-invalid formal package is preserved as a package"
)
_state.add_resource(&"faith", 17)
var strict_guard := _authoritative_live_guard()
var strict_load: Dictionary = _runtime.load_from_path(path)
_expect(
not strict_load[&"ok"] and strict_load[&"code"] == &"RESTORE_FAILED",
"ordinary load remains strict and does not fall through to a valid backup"
)
_expect(
_authoritative_live_guard() == strict_guard,
"strict ordinary-load rejection preserves authoritative gameplay state"
)
var explicit_formal: Dictionary = _runtime.load_recovery_candidate(path, &"formal")
_expect(
not explicit_formal[&"ok"] and explicit_formal[&"code"] == &"RESTORE_FAILED",
"explicit candidate load remains strict to the requested domain-invalid package"
)
var recovered: Dictionary = _runtime.load_with_recovery(path)
_expect(
recovered[&"ok"] and recovered[&"selected"][&"id"] == &"backup",
"Runtime rejects a domain-invalid formal package and automatically selects its valid backup"
)
_expect(
recovered[&"candidates"][0][&"package_status"] == &"VALID"
and recovered[&"candidates"][0][&"domain_status"] == &"REJECTED"
and recovered[&"candidates"][0][&"domain_rejection_code"] == &"DOMAIN_OR_CROSSLINK_VALIDATION_FAILED"
and recovered[&"candidates"][0][&"failed_attempt_live_state_preserved"],
"formal candidate report separates successful package verification from domain rejection"
)
_expect(
recovered[&"candidates"][1][&"domain_status"] == &"VALID"
and recovered[&"candidates"][1][&"selected"],
"backup candidate report identifies the first domain-valid committed candidate"
)
_expect(
_runtime.latest_digest == backup_digest,
"domain fallback commits the exact backup snapshot"
)
_expect(
recovered[&"package_candidate_report"][&"selected"][&"id"] == &"formal"
and recovered[&"package_recovery_report"][&"source"][&"candidate_id"] == &"backup"
and recovered[&"restore_safety_report"][&"recovery_mode"],
"result preserves package-level selection evidence and final Runtime restore-safety evidence"
)
SaveService.delete_save(path)
func _test_multiple_domain_invalid_candidates_fall_back_to_autosave() -> void:
_reset_runtime(60_010, 2, 9, 0)
var path := TEST_PREFIX + "_domain_autosave"
SaveService.delete_save(path)
var valid_autosave: Dictionary = _runtime.create_snapshot()
var valid_digest := SimulationDigest.compute(valid_autosave)
var invalid_backup: Dictionary = valid_autosave.duplicate(true)
invalid_backup[&"game_state"][&"resources"][&"gold"] = int(
invalid_backup[&"game_state"][&"resources"][&"gold"]
) + 1
var invalid_formal: Dictionary = valid_autosave.duplicate(true)
invalid_formal[&"game_state"][&"resources"][&"gold"] = int(
invalid_formal[&"game_state"][&"resources"][&"gold"]
) + 2
_expect(
SaveService.save_snapshot(path, invalid_backup)[&"ok"]
and SaveService.save_snapshot(path, invalid_formal)[&"ok"],
"multiple domain-invalid formal generations remain checksum-valid recovery candidates"
)
_expect(
SaveService.save_autosave(path, valid_autosave, {}, 3)[&"ok"],
"valid autosave is written behind both invalid formal generations"
)
var package_guard := {
&"formal": _package_fingerprint(path),
&"backup": _package_fingerprint(path + ".bak"),
&"autosave": _package_fingerprint(SaveService.get_autosave_path(path, 0)),
}
_state.add_resource(&"faith", 23)
var recovered: Dictionary = _runtime.load_with_recovery(path)
_expect(
recovered[&"ok"] and recovered[&"selected"][&"id"] == &"autosave_0",
"Runtime walks past two domain-invalid candidates to the newest valid autosave"
)
_expect(
recovered[&"candidates"][0][&"domain_status"] == &"REJECTED"
and recovered[&"candidates"][1][&"domain_status"] == &"REJECTED"
and recovered[&"candidates"][0][&"failed_attempt_live_state_preserved"]
and recovered[&"candidates"][1][&"failed_attempt_live_state_preserved"]
and recovered[&"candidates"][4][&"domain_status"] == &"VALID",
"candidate attempts retain both ordered domain rejections and the autosave success"
)
_expect(
recovered[&"recovery_report"][&"domain_rejections"].size() == 2
and recovered[&"recovery_report"][&"selected_candidate_id"] == &"autosave_0",
"combined recovery report records every rejected domain candidate and final selection"
)
_expect(_runtime.latest_digest == valid_digest, "autosave fallback restores the exact valid snapshot")
_expect(
package_guard == {
&"formal": _package_fingerprint(path),
&"backup": _package_fingerprint(path + ".bak"),
&"autosave": _package_fingerprint(SaveService.get_autosave_path(path, 0)),
},
"domain fallback remains read-only and never promotes or rewrites any package"
)
SaveService.delete_save(path)
func _test_all_domain_invalid_candidates_preserve_complete_live_state() -> void:
_reset_runtime(60_011, 2, 10, 0)
var path := TEST_PREFIX + "_all_domain_invalid"
SaveService.delete_save(path)
var base: Dictionary = _runtime.create_snapshot()
var invalid_backup: Dictionary = base.duplicate(true)
invalid_backup[&"game_state"][&"resources"][&"gold"] = int(
invalid_backup[&"game_state"][&"resources"][&"gold"]
) + 1
var invalid_formal: Dictionary = base.duplicate(true)
invalid_formal[&"game_state"][&"resources"][&"gold"] = int(
invalid_formal[&"game_state"][&"resources"][&"gold"]
) + 2
var invalid_autosave: Dictionary = base.duplicate(true)
invalid_autosave[&"game_state"][&"resources"][&"gold"] = int(
invalid_autosave[&"game_state"][&"resources"][&"gold"]
) + 3
_expect(
SaveService.save_snapshot(path, invalid_backup)[&"ok"]
and SaveService.save_snapshot(path, invalid_formal)[&"ok"]
and SaveService.save_autosave(path, invalid_autosave, {}, 3)[&"ok"],
"all-invalid fixture exposes three checksum-valid but domain-invalid candidates"
)
_state.add_resource(&"faith", 31)
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.DISABLE,
EventDefinition.ContentRating.GENERAL,
[&"local_atomic_filter"]
)
_clock.set_speed(3)
_clock.set_paused(true)
_expect(
_runtime.queue_manual_checkpoint(TEST_PREFIX + "_never_written")[&"ok"],
"all-invalid atomicity fixture includes a pending Runtime checkpoint"
)
var live_guard := _complete_live_guard()
var rejected: Dictionary = _runtime.load_with_recovery(path)
_expect(
not rejected[&"ok"]
and rejected[&"code"] == &"RESTORE_FAILED"
and rejected[&"recovery_report"][&"rejection_reason"] == &"NO_DOMAIN_VALID_RECOVERY_CANDIDATE",
"all domain-invalid candidates return one explicit terminal recovery failure"
)
_expect(
rejected[&"recovery_report"][&"domain_rejections"].size() == 3
and rejected[&"selected"].is_empty(),
"terminal failure reports all three domain rejections and no selected candidate"
)
_expect(
_complete_live_guard() == live_guard and rejected[&"live_state_preserved"],
"all-candidate failure preserves the complete live snapshot, filter, pause, policy, and pending checkpoint"
)
_expect(
rejected[&"restore_safety_report"] == rejected[&"recovery_report"]
and rejected[&"package_candidate_report"].has(&"candidates"),
"terminal failure preserves both package inventory and Runtime restore-safety reports"
)
SaveService.delete_save(path)
func _test_content_filter_cannot_be_weakened() -> void:
_reset_runtime(60_006)
var weak_path := TEST_PREFIX + "_weak_filter"
var exact_path := TEST_PREFIX + "_exact_filter"
SaveService.delete_save(weak_path)
SaveService.delete_save(exact_path)
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.NONE,
EventDefinition.ContentRating.RESTRICTED,
[&"saved_tag"]
)
_expect(_runtime.save_to_path(weak_path)[&"ok"], "weaker saved content profile fixture is valid")
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.DISABLE,
EventDefinition.ContentRating.GENERAL,
[&"local_tag"]
)
var loaded = _runtime.load_from_path(weak_path)
var profile = _runtime.event_service.get_content_filter_profile()
_expect(
loaded[&"ok"]
and profile[&"filter_mode"] == EventDefinition.FilterBehavior.DISABLE
and profile[&"maximum_content_rating"] == EventDefinition.ContentRating.GENERAL,
"loading a weaker save cannot reduce behavior severity or raise the rating ceiling"
)
_expect(
profile[&"disabled_content_tags"] == [&"local_tag", &"saved_tag"],
"load safety unions local and saved disabled content tags"
)
var filter_report: Dictionary = loaded[&"restore_safety_report"][&"content_filter"]
_expect(filter_report[&"saved_profile_was_weaker"] and filter_report[&"strict_profile_preserved"], "load result explains that the stricter local filter won")
var exact_snapshot = _runtime.create_snapshot()
var exact_digest := SimulationDigest.compute(exact_snapshot)
_expect(_runtime.save_to_path(exact_path)[&"ok"], "effective strict profile saves normally")
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.NONE,
EventDefinition.ContentRating.RESTRICTED,
[]
)
var exact_loaded = _runtime.load_from_path(exact_path)
_expect(exact_loaded[&"ok"] and _runtime.latest_digest == exact_digest, "ordinary content-profile round trip remains snapshot-exact")
SaveService.delete_save(weak_path)
SaveService.delete_save(exact_path)
func _test_whitelisted_recovery_repairs() -> void:
_reset_runtime(60_007)
var path := TEST_PREFIX + "_allowed_repairs"
SaveService.delete_save(path)
var snapshot = _runtime.create_snapshot()
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
var staff := StaffRuntime.new().configure(&"repair_staff", &"worker", {
&"state": StaffRuntime.State.MOVING_TO_TASK,
&"current_task_id": &"repair_task",
})
var task := StaffTask.new().configure(&"repair_task", &"cleanup", 50, _clock.absolute_minute, {
&"state": StaffTask.State.RESERVED,
&"assigned_staff_id": &"missing_staff",
})
runtime_data[&"staff"][&"staff"].append(staff.create_snapshot())
runtime_data[&"staff"][&"tasks"].append(task.create_snapshot())
_append_staff_path_crosslink(runtime_data, &"repair_staff", Vector3i.ZERO)
var terminal := EventInstance.new().configure(
&"terminal_foreground",
&"relationship.threshold",
&"relationship.threshold",
[&"player"],
100,
_clock.absolute_minute,
-1,
true,
&""
)
terminal.state = EventInstance.State.COMPLETED
runtime_data[&"events"][&"instances"].append(terminal.create_snapshot())
runtime_data[&"events"][&"foreground_instance_id"] = "terminal_foreground"
runtime_data[&"events"][&"actor_locks"] = {"player": "terminal_foreground"}
_expect(SaveService.save_snapshot(path, snapshot)[&"ok"], "checksum-valid orphan repair fixture writes as a package")
var recovered = _runtime.load_with_recovery(path)
_expect(recovered[&"ok"], "recovery accepts only the whitelisted Task and Event Lock damage")
var repaired_task = _runtime.staff_service.get_task(&"repair_task")
var repaired_staff = _runtime.staff_service.get_staff(&"repair_staff")
_expect(
repaired_task != null and repaired_task.state == StaffTask.State.OPEN and repaired_task.assigned_staff_id.is_empty()
and repaired_staff != null and repaired_staff.current_task_id.is_empty(),
"StaffService audit reopens the orphan Task and releases its stale Staff pointer"
)
_expect(_runtime.event_service.get_actor_locks().is_empty() and _runtime.event_service.get_foreground_instance() == null, "terminal foreground recovery releases only its stale Event Locks")
var report: Dictionary = recovered[&"recovery_report"]
_expect(
report[&"repairs"][&"orphan_tasks"][&"reopened_task_ids"] == [&"repair_task"]
and report[&"repairs"][&"orphan_tasks"][&"released_staff_ids"] == [&"repair_staff"],
"repair report names every repaired Task and Staff exactly"
)
_expect(
report[&"repairs"][&"orphan_event_locks"][&"released_actor_ids"] == [&"player"]
and report[&"repairs"][&"orphan_event_locks"][&"released_foreground_instance_id"] == &"terminal_foreground",
"repair report names the released Actor Lock and terminal foreground exactly"
)
_expect(report[&"automatic_repairs_applied"] and not report[&"forbidden_repairs_applied"], "recovery report confirms its authoritative whitelist boundary")
SaveService.delete_save(path)
func _test_non_whitelisted_corruption_is_rejected_atomically() -> void:
_reset_runtime(60_008)
var built = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"reception", &"origin": Vector3i(-6, 0, 0), &"rotation": 0},
&"player", &"", 100, -1, &"s6.invalid.fixture.facility"
)
_expect(built.is_success(), "invalid recovery fixtures start from a valid constructed facility")
_clock.advance_minutes(120)
var base = _runtime.create_snapshot()
var missing_facility = base.duplicate(true)
missing_facility[&"systems"][&"runtime"][&"facilities"][&"instances"] = []
_test_rejected_candidate(
TEST_PREFIX + "_reject_missing_facility",
missing_facility,
"recovery never deletes or invents a missing operational facility"
)
var ledger_mutation = base.duplicate(true)
ledger_mutation[&"game_state"][&"resources"][&"gold"] = int(
ledger_mutation[&"game_state"][&"resources"][&"gold"]
) + 1
_test_rejected_candidate(
TEST_PREFIX + "_reject_ledger",
ledger_mutation,
"recovery never synthesizes Gold or rewrites the ledger projection"
)
var route_mismatch = base.duplicate(true)
var invalid_relationship := RelationshipState.new().configure(
&"invalid_route",
&"actor_a",
&"actor_b",
{
&"route_lock": RelationshipState.RouteLock.EXCLUSIVE_LOCKED,
&"route_id": &"",
}
)
route_mismatch[&"systems"][&"runtime"][&"relationships"][&"relationships"].append(
invalid_relationship.create_snapshot()
)
_test_rejected_candidate(
TEST_PREFIX + "_reject_route",
route_mismatch,
"recovery never guesses an exclusive Relationship route lock"
)
var unreleasable = base.duplicate(true)
var bad_staff := StaffRuntime.new().configure(&"blocked_staff", &"worker", {
&"state": StaffRuntime.State.MOVING_TO_TASK,
&"current_task_id": &"blocked_task",
})
var bad_task := StaffTask.new().configure(&"blocked_task", &"cleanup", 50, _clock.absolute_minute, {
&"state": StaffTask.State.RESERVED,
&"assigned_staff_id": &"missing_staff",
&"reservation_ids": [&"missing_reservation"],
})
unreleasable[&"systems"][&"runtime"][&"staff"][&"staff"].append(bad_staff.create_snapshot())
unreleasable[&"systems"][&"runtime"][&"staff"][&"tasks"].append(bad_task.create_snapshot())
_append_staff_path_crosslink(
unreleasable[&"systems"][&"runtime"],
&"blocked_staff",
Vector3i.ZERO
)
_test_rejected_candidate(
TEST_PREFIX + "_reject_reservation",
unreleasable,
"recovery rejects an orphan Task whose inventory Reservation cannot be released"
)
func _test_rejected_candidate(path: String, snapshot: Dictionary, label: String) -> void:
SaveService.delete_save(path)
_expect(SaveService.save_snapshot(path, snapshot)[&"ok"], "%s fixture is checksum-valid" % label)
var guard := _authoritative_live_guard()
var result = _runtime.load_with_recovery(path)
_expect(not result[&"ok"] and result[&"code"] == &"RESTORE_FAILED", label)
_expect(_authoritative_live_guard() == guard, "%s and leaves authoritative live state unchanged" % label)
_expect(
result[&"recovery_report"][&"restore_committed"] == false
and not result[&"recovery_report"][&"forbidden_repairs_applied"],
"%s reports atomic rejection without forbidden repair" % label
)
SaveService.delete_save(path)
func _append_staff_path_crosslink(
runtime_data: Dictionary,
staff_id: StringName,
cell: Vector3i
) -> void:
var actor_id := StringName("staff:%s" % staff_id)
runtime_data[&"path"][&"actor_cells"].append({
&"actor_id": actor_id,
&"cell": cell,
})
runtime_data[&"path"][&"actor_progress"].append({
&"actor_id": actor_id,
&"last_cell": cell,
&"last_progress_at": _clock.absolute_minute,
&"last_observed_at": _clock.absolute_minute,
&"stuck_reported_at": -1,
})
runtime_data[&"movement_credits"].append({
&"actor_id": String(actor_id),
&"credit": 0,
&"updated_at": _clock.absolute_minute,
})
for traffic: Dictionary in runtime_data[&"path"][&"cell_traffic"]:
if traffic[&"cell"] == cell:
traffic[&"current_density"] = int(traffic[&"current_density"]) + 1
break
func _authoritative_live_guard() -> Dictionary:
return {
&"clock": _clock.clock_snapshot(),
&"game_state": _state.create_snapshot(),
&"economy": _runtime.economy_service.create_snapshot(),
&"inventory": _runtime.inventory_service.create_snapshot(),
&"facilities": _runtime.facility_service.create_snapshot(),
&"construction": _runtime.construction_service.create_snapshot(),
&"staff": _runtime.staff_service.create_snapshot(),
&"relationships": _runtime.relationship_service.create_snapshot(),
&"events": _runtime.event_service.create_snapshot(),
}
func _complete_live_guard() -> Dictionary:
return {
&"snapshot": _runtime.create_snapshot(),
&"clock_paused": _clock.paused,
&"save_policy": _runtime.get_save_policy(),
&"pending_checkpoint_count": _runtime.get_pending_checkpoint_count(),
&"last_daily_checkpoint_day": _runtime.get_last_daily_checkpoint_day(),
&"content_filter": _runtime.event_service.get_content_filter_profile(),
}
func _reset_runtime(seed: int, day := 1, hour := 6, minute := 0) -> void:
_cleanup_runtime()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(day, hour, minute)
_state.reset()
var report = _runtime.initialize(_clock, _state, seed)
_expect(report[&"valid"], "Runtime fixture initializes deterministic definitions")
func _cleanup_runtime() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _cleanup_test_saves() -> void:
for suffix: String in [
"_disabled", "_queued", "_daily", "_round_trip", "_weak_filter",
"_exact_filter", "_allowed_repairs", "_reject_missing_facility",
"_reject_ledger", "_reject_route", "_reject_reservation",
"_domain_backup", "_domain_autosave", "_all_domain_invalid",
"_never_written",
]:
SaveService.delete_save(TEST_PREFIX + suffix)
func _has_domain_event(event_type: StringName) -> bool:
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if event.event_type == event_type:
return true
return false
func _package_fingerprint(save_path: String) -> Dictionary:
var absolute_path := ProjectSettings.globalize_path(save_path)
var result := {&"directory_exists": DirAccess.dir_exists_absolute(absolute_path)}
for file_name: String in [SaveService.MANIFEST_FILE, SaveService.WORLD_FILE, SaveService.THUMBNAIL_FILE]:
var file_path := absolute_path.path_join(file_name)
result[file_name] = FileAccess.get_md5(file_path) if FileAccess.file_exists(file_path) else ""
return result
func _corrupt_world(save_path: String) -> void:
var world_path := ProjectSettings.globalize_path(save_path).path_join(SaveService.WORLD_FILE)
var bytes := FileAccess.get_file_as_bytes(world_path)
if bytes.is_empty():
return
bytes[bytes.size() - 1] = bytes[bytes.size() - 1] ^ 0x5a
var file := FileAccess.open(world_path, FileAccess.WRITE)
file.store_buffer(bytes)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)