128 lines
6.0 KiB
GDScript
128 lines
6.0 KiB
GDScript
extends SceneTree
|
|
|
|
const CharterService = preload("res://p9_9/charter_service.gd")
|
|
const ProfileState = preload("res://p9_9/profile_state.gd")
|
|
const SaveService = preload("res://p9_9/save_service.gd")
|
|
const Fixture = preload("res://tests/p9_9/runtime_fixture.gd")
|
|
|
|
var failures: Array[String] = []
|
|
var save_root := ""
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("run_test")
|
|
|
|
|
|
func run_test() -> void:
|
|
save_root = OS.get_environment("ABG_P9_9_SAVE_TEST_ROOT")
|
|
if save_root.is_empty():
|
|
save_root = "/private/tmp/abg-p99-persistence-default"
|
|
DirAccess.make_dir_recursive_absolute(save_root)
|
|
var closed := Fixture.closed_p98()
|
|
verify_v3_migration(closed)
|
|
verify_v4_round_trip(closed)
|
|
verify_time_corruption_and_backup(closed)
|
|
if failures.is_empty():
|
|
print("ABG_P9_9_PERSISTENCE_OK version=4 migration=3_to_4 p98_closed=exact renown=14 charter=preserved duplicate_after_reload=0 corruption=true backup=true rollback=true offline_cap=86400 offline_rewards=0 temp_clean=true")
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func verify_v3_migration(closed: Dictionary) -> void:
|
|
var saves = fresh_saves("v3")
|
|
write_json(saves.save_path, saves.envelope_for_snapshot(closed, 3, 1000))
|
|
var loaded: Dictionary = saves.load_snapshot(1060)
|
|
expect(loaded.ok and loaded.migrated and loaded.source_version == 3, "v3 closed save migrates")
|
|
expect(loaded.snapshot.persistent_renown == 14 and loaded.snapshot.renown_reconciliation_receipts.size() == 1, "migration reconciles Renown once")
|
|
expect(loaded.snapshot.contract_clear_ids == closed.contract_clear_ids and loaded.snapshot.company_records == closed.company_records, "migration preserves clear and Record")
|
|
expect(loaded.elapsed_seconds == 60 and loaded.offline_renown == 0, "migration grants no offline value")
|
|
|
|
|
|
func verify_v4_round_trip(closed: Dictionary) -> void:
|
|
var service = CharterService.new(closed)
|
|
var operation_id := "p99-persist-charter"
|
|
expect(service.claim(operation_id, "CH-RG01-THREAT-01", service.state.revision).ok, "Charter fixture commits")
|
|
var saves = fresh_saves("v4")
|
|
var original: Dictionary = service.state.snapshot()
|
|
var saved: Dictionary = saves.save_snapshot(original, 2000)
|
|
expect(saved.ok and saved.version == 4, "v4 profile saves")
|
|
if not saved.ok:
|
|
return
|
|
expect(not FileAccess.file_exists(saves.save_path + ".tmp"), "atomic write leaves no temp")
|
|
var loaded: Dictionary = saves.load_snapshot(2010)
|
|
expect(loaded.ok and not loaded.migrated and loaded.snapshot == original, "v4 round trip is exact")
|
|
var restored = CharterService.new()
|
|
expect(restored.state.restore_snapshot(loaded.snapshot), "loaded profile restores")
|
|
var before: Dictionary = restored.state.snapshot()
|
|
var duplicate: Dictionary = restored.claim(operation_id, "CH-RG01-THREAT-01")
|
|
expect(duplicate.ok and duplicate.duplicate and restored.state.snapshot() == before, "reloaded duplicate is idempotent")
|
|
|
|
|
|
func verify_time_corruption_and_backup(closed: Dictionary) -> void:
|
|
var service = CharterService.new(closed)
|
|
var rollback = fresh_saves("rollback")
|
|
var rollback_saved: Dictionary = rollback.save_snapshot(service.state.snapshot(), 3000)
|
|
expect(rollback_saved.ok, "rollback fixture writes")
|
|
if not rollback_saved.ok:
|
|
return
|
|
var rolled: Dictionary = rollback.load_snapshot(2900)
|
|
expect(rolled.ok and rolled.clock_rollback and rolled.elapsed_seconds == 0, "clock rollback is inert")
|
|
var capped: Dictionary = rollback.load_snapshot(999999)
|
|
expect(capped.ok and capped.elapsed_seconds == SaveService.OFFLINE_CAP_SECONDS, "offline duration is capped")
|
|
expect(capped.offline_coin == 0 and capped.offline_xp == 0 and capped.offline_renown == 0 and capped.offline_battle_ticks == 0, "offline rewards are zero")
|
|
|
|
var malformed = fresh_saves("malformed")
|
|
write_text(malformed.save_path, "{")
|
|
expect(malformed.load_snapshot(4000).code == "malformed_json", "malformed JSON rejects")
|
|
var checksum = fresh_saves("checksum")
|
|
var bad_checksum: Dictionary = checksum.envelope_for_snapshot(service.state.snapshot(), 4, 4000)
|
|
bad_checksum.checksum = "0".repeat(64)
|
|
write_json(checksum.save_path, bad_checksum)
|
|
expect(checksum.load_snapshot(4001).code == "checksum_mismatch", "checksum drift rejects")
|
|
var unknown = fresh_saves("unknown")
|
|
write_json(unknown.save_path, unknown.envelope_for_snapshot(service.state.snapshot(), 99, 4000))
|
|
expect(unknown.load_snapshot(4001).code == "unsupported_version", "unknown version rejects")
|
|
var invalid = fresh_saves("invalid")
|
|
var invalid_state: Dictionary = service.state.snapshot()
|
|
invalid_state.persistent_renown = -1
|
|
write_json(invalid.save_path, invalid.envelope_for_snapshot(invalid_state, 4, 4000))
|
|
expect(invalid.load_snapshot(4001).code == "invalid_state", "valid-checksum invalid profile rejects")
|
|
|
|
var backup = fresh_saves("backup")
|
|
var first: Dictionary = service.state.snapshot()
|
|
expect(backup.save_snapshot(first, 5000).ok, "backup first save writes")
|
|
service.claim("p99-backup-claim", "CH-RG01-CATALOG-01", service.state.revision)
|
|
var second: Dictionary = service.state.snapshot()
|
|
expect(backup.save_snapshot(second, 5010).ok and FileAccess.file_exists(backup.save_path + ".bak"), "second save retains backup")
|
|
write_text(backup.save_path, "broken")
|
|
var recovered: Dictionary = backup.load_snapshot(5020)
|
|
expect(recovered.ok and recovered.code == "backup_recovered" and recovered.primary_error == "malformed_json", "corrupt primary recovers backup")
|
|
expect(recovered.snapshot == first and recovered.snapshot != second, "backup is prior complete state")
|
|
|
|
|
|
func fresh_saves(name: String):
|
|
var saves = SaveService.new()
|
|
saves.save_path = save_root.path_join(name + ".json")
|
|
saves.clear()
|
|
return saves
|
|
|
|
|
|
func write_json(path: String, value: Dictionary) -> void:
|
|
write_text(path, JSON.stringify(value))
|
|
|
|
|
|
func write_text(path: String, value: String) -> void:
|
|
var file := FileAccess.open(path, FileAccess.WRITE)
|
|
expect(file != null, "fixture opens %s" % path)
|
|
if file != null:
|
|
file.store_string(value)
|
|
file.flush()
|
|
|
|
|
|
func expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append("P9.9 persistence: %s" % message)
|