172 lines
8.9 KiB
GDScript
172 lines
8.9 KiB
GDScript
extends SceneTree
|
|
|
|
const State = preload("res://p9_10/contract_state.gd")
|
|
const Service = preload("res://p9_10/contract_service.gd")
|
|
const BossService = preload("res://p9_10/boss_service.gd")
|
|
const BossBattleService = preload("res://p9_10/boss_battle_service.gd")
|
|
const SaveService = preload("res://p9_10/save_service.gd")
|
|
const P99SaveService = preload("res://p9_9/save_service.gd")
|
|
const Fixture = preload("res://tests/p9_10/runtime_fixture.gd")
|
|
|
|
var failures: Array[String] = []
|
|
var root_path := ""
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("run_test")
|
|
|
|
|
|
func run_test() -> void:
|
|
root_path = OS.get_environment("ABG_P9_10_SAVE_ROOT")
|
|
if root_path.is_empty():
|
|
root_path = "/private/tmp/abg-p910-save-test"
|
|
DirAccess.make_dir_recursive_absolute(root_path)
|
|
verify_v4_migration_and_round_trip()
|
|
verify_interruption_and_rollback()
|
|
verify_corruption_and_backup()
|
|
verify_boss_pending_and_close()
|
|
if failures.is_empty():
|
|
print("ABG_P9_10_PERSISTENCE_OK version=5 migration=4to5 market4=true roundtrip=true interruption=market,boss pending_close=true atomic_close=prior_or_result closed_reload=true duplicate_close=true rollback=0 backup=true corrupt=true offline_coin=0 offline_xp=0 offline_renown=0 offline_healing=0 offline_battle_ticks=0")
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func verify_v4_migration_and_round_trip() -> void:
|
|
var path := root_path.path_join("migration.json")
|
|
remove_set(path)
|
|
var p99 := Fixture.p99_market_four()
|
|
var old_save = P99SaveService.new()
|
|
write_json(path, old_save.envelope_for_snapshot(p99, 4, 1000))
|
|
var saves = SaveService.new()
|
|
saves.save_path = path
|
|
var loaded: Dictionary = saves.load_snapshot(1010)
|
|
expect(loaded.ok and loaded.migrated and loaded.source_version == 4 and loaded.version == 5, "exact v4 P9.9 save migrates")
|
|
expect(loaded.snapshot.state_kind == "p9_10_contract" and loaded.snapshot.current_market == 4 and loaded.snapshot.line_capacity == 5, "migration opens exact P9.10 Market 4")
|
|
expect(loaded.offline_coin == 0 and loaded.offline_xp == 0 and loaded.offline_renown == 0 and loaded.offline_healing == 0 and loaded.offline_battle_ticks == 0, "migration grants no offline value")
|
|
expect(saves.save_snapshot(loaded.snapshot, 1020).ok, "migrated state saves as v5")
|
|
var roundtrip: Dictionary = saves.load_snapshot(1021)
|
|
expect(roundtrip.ok and not roundtrip.migrated and roundtrip.snapshot == loaded.snapshot, "v5 round trip preserves exact state")
|
|
|
|
|
|
func verify_interruption_and_rollback() -> void:
|
|
var path := root_path.path_join("interrupt.json")
|
|
remove_set(path)
|
|
var service = Service.new(Fixture.p99_market_four())
|
|
expect(service.commit_route("interrupt-route", "P910-R4-Q0").ok, "Encounter checkpoint commits")
|
|
var saves = SaveService.new()
|
|
saves.save_path = path
|
|
expect(saves.save_snapshot(service.state.snapshot(), 2000).ok, "Encounter saves")
|
|
var loaded: Dictionary = saves.load_snapshot(2015)
|
|
expect(loaded.ok and loaded.battle_interrupted and loaded.snapshot.phase == State.Phase.MARKET and loaded.snapshot.current_market == 4, "interrupted Encounter returns to Market checkpoint")
|
|
expect(loaded.snapshot.selected_route.is_empty() and loaded.offline_battle_ticks == 0, "interruption advances no hidden battle")
|
|
var rollback: Dictionary = saves.load_snapshot(1990)
|
|
expect(rollback.ok and rollback.clock_rollback and rollback.elapsed_seconds == 0, "clock rollback clamps elapsed time to zero")
|
|
|
|
|
|
func verify_corruption_and_backup() -> void:
|
|
var path := root_path.path_join("backup.json")
|
|
remove_set(path)
|
|
var saves = SaveService.new()
|
|
saves.save_path = path
|
|
var first = Service.new(Fixture.p99_market_four())
|
|
expect(saves.save_snapshot(first.state.snapshot(), 3000).ok, "first valid save writes")
|
|
expect(first.hire("backup-hire", "P910-M4-H1").ok, "second snapshot changes")
|
|
expect(saves.save_snapshot(first.state.snapshot(), 3010).ok, "second valid save creates backup")
|
|
var file := FileAccess.open(path, FileAccess.WRITE)
|
|
file.store_string("{broken")
|
|
file = null
|
|
var recovered: Dictionary = saves.load_snapshot(3020)
|
|
expect(recovered.ok and recovered.code == "backup_recovered" and recovered.snapshot.company.size() == 4, "malformed primary restores prior complete backup")
|
|
var invalid_path := root_path.path_join("invalid.json")
|
|
remove_set(invalid_path)
|
|
var invalid = SaveService.new()
|
|
invalid.save_path = invalid_path
|
|
write_json(invalid_path, {"version": 5, "saved_at_unix": 1, "state": {}, "checksum": "bad"})
|
|
var rejected: Dictionary = invalid.load_snapshot(2)
|
|
expect(not rejected.ok and rejected.code == "checksum_mismatch", "forged checksum rejects")
|
|
|
|
|
|
func verify_boss_pending_and_close() -> void:
|
|
var muster := Fixture.p910_boss_muster()
|
|
var service = BossService.new()
|
|
expect(service.restore_snapshot(muster), "Boss Muster restores")
|
|
expect(service.commit_boss_plan("save-boss-plan", "break_spindle").ok, "Boss plan commits")
|
|
var encounter_path := root_path.path_join("boss-interrupt.json")
|
|
remove_set(encounter_path)
|
|
var encounter_saves = SaveService.new()
|
|
encounter_saves.save_path = encounter_path
|
|
expect(encounter_saves.save_snapshot(service.state.snapshot(), 4000).ok, "Boss encounter saves")
|
|
var interrupted: Dictionary = encounter_saves.load_snapshot(4005)
|
|
expect(interrupted.ok and interrupted.battle_interrupted and interrupted.boss_interrupted, "Boss interruption is classified explicitly")
|
|
expect(interrupted.snapshot == muster, "Boss interruption restores the exact unchanged Muster")
|
|
|
|
var resumed = BossService.new()
|
|
expect(resumed.restore_snapshot(interrupted.snapshot), "interrupted Muster restores")
|
|
expect(resumed.commit_boss_plan("save-boss-plan", "break_spindle").ok, "same Boss plan recommits after rollback")
|
|
var battle = BossBattleService.new()
|
|
expect(battle.start_from_contract(resumed.state).ok, "resumed Boss battle starts")
|
|
var guard := 0
|
|
while not battle.state.terminal and guard < 1900:
|
|
battle.advance_ticks(1)
|
|
guard += 1
|
|
expect(battle.state.outcome == "victory", "persistence Boss fixture wins")
|
|
expect(resumed.settle_boss("save-boss-settle", battle.state.snapshot()).ok, "Boss victory creates pending close")
|
|
var pending: Dictionary = resumed.state.snapshot()
|
|
|
|
var close_path := root_path.path_join("atomic-close.json")
|
|
remove_set(close_path)
|
|
var close_saves = SaveService.new()
|
|
close_saves.save_path = close_path
|
|
expect(close_saves.save_snapshot(pending, 4100).ok, "pending close saves before apply")
|
|
var pending_loaded: Dictionary = close_saves.load_snapshot(4101)
|
|
expect(pending_loaded.ok and pending_loaded.snapshot == pending and pending_loaded.snapshot.phase == State.Phase.CONTRACT_RESULT, "pending close reloads exactly")
|
|
var closer = BossService.new()
|
|
expect(closer.restore_snapshot(pending_loaded.snapshot), "pending close restores")
|
|
expect(closer.close_contract("save-close-second").ok, "pending close applies")
|
|
var closed: Dictionary = closer.state.snapshot()
|
|
expect(close_saves.save_snapshot(closed, 4110).ok, "closed result atomically replaces pending state")
|
|
var closed_loaded: Dictionary = close_saves.load_snapshot(4111)
|
|
expect(closed_loaded.ok and closed_loaded.snapshot == closed and closed_loaded.snapshot.run_closed, "closed persistent result reloads exactly")
|
|
var duplicate = BossService.new()
|
|
expect(duplicate.restore_snapshot(closed_loaded.snapshot), "closed result restores")
|
|
expect(duplicate.close_contract("save-close-second").duplicate and duplicate.state.snapshot() == closed, "close retry after reload grants nothing")
|
|
|
|
var file := FileAccess.open(close_path, FileAccess.WRITE)
|
|
file.store_string("{interrupted-atomic-promote")
|
|
file = null
|
|
var prior: Dictionary = close_saves.load_snapshot(4120)
|
|
expect(prior.ok and prior.code == "backup_recovered" and prior.snapshot == pending, "interrupted atomic promote restores the complete prior pending state")
|
|
var retry = BossService.new()
|
|
expect(retry.restore_snapshot(prior.snapshot) and retry.close_contract("save-close-second").ok, "prior pending state reapplies deterministically")
|
|
expect(retry.state.snapshot() == closed, "atomic recovery yields the same complete closed result")
|
|
|
|
var invalid_path := root_path.path_join("invalid-pending.json")
|
|
remove_set(invalid_path)
|
|
var invalid_pending := pending.duplicate(true)
|
|
invalid_pending.pending_close.renown = 19
|
|
var invalid_saves = SaveService.new()
|
|
invalid_saves.save_path = invalid_path
|
|
write_json(invalid_path, invalid_saves.envelope_for_snapshot(invalid_pending, 5, 4200))
|
|
var invalid_loaded: Dictionary = invalid_saves.load_snapshot(4201)
|
|
expect(not invalid_loaded.ok and invalid_loaded.code == "invalid_state", "checksum-valid pending-close authority drift rejects")
|
|
|
|
|
|
func write_json(path: String, value: Dictionary) -> void:
|
|
var file := FileAccess.open(path, FileAccess.WRITE)
|
|
file.store_string(JSON.stringify(value))
|
|
file = null
|
|
|
|
|
|
func remove_set(path: String) -> void:
|
|
for candidate in [path, path + ".tmp", path + ".bak"]:
|
|
if FileAccess.file_exists(candidate):
|
|
DirAccess.remove_absolute(candidate)
|
|
|
|
|
|
func expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append("P9.10 persistence: %s" % message)
|