Files
aetherbound-guild/runtime/tests/fg4/save_v11_test.gd
T

122 lines
6.9 KiB
GDScript

extends SceneTree
const FG3Fixture = preload("res://tests/fg3/contract1_fixture.gd")
const SaveV10 = preload("res://fg3/save_v10_service.gd")
const SaveV11 = preload("res://fg4/save_v11_service.gd")
const Fixture = preload("res://tests/fg4/contract1_fixture.gd")
const SLOT_PATH := "user://abg_fg4_save_v11_test.json"
var failures: Array[String] = []
var migrated_snapshot: Dictionary = {}
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
cleanup_files()
verify_exact_migration_and_roundtrip()
verify_checksum_backup_and_rollback()
verify_atomic_file_rotation_and_interruption()
verify_migration_boundaries()
cleanup_files()
if failures.is_empty():
print("ABG_FG4_SAVE_V11_OK migration=v10_rg02_closed sequence=+1 guild,company,growth,equipment,history=exact board=rg03_market1 roundtrip=true rollback=true backup=true interruption=true corruption=true offline_coin,credit,xp,renown,healing,battle_ticks=0")
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func verify_exact_migration_and_roundtrip() -> void:
var fixture := Fixture.new()
var source := fixture.fg3_region_closed_snapshot()
var encoded_v10 := SaveV10.new().encode_snapshot(source, 100)
var migrated := SaveV11.new().migrate_v10_text(String(encoded_v10.text), 100)
expect(encoded_v10.ok and migrated.ok, "exact Save-v10 RG-02 close migrates: %s" % str(migrated))
if not encoded_v10.ok or not migrated.ok:
return
migrated_snapshot = migrated.snapshot
var after: Dictionary = migrated.snapshot
expect(migrated.sequence == 101 and migrated.previous_commit_hash == encoded_v10.envelope.commit_hash, "migration advances and chains the Save commit")
expect(after.guild_profile == source.guild_profile, "Guild profile is byte-equivalent")
expect(after.current_run.run_id == source.current_run.run_id and after.current_run.run_seed == source.current_run.run_seed, "Run identity is exact")
expect(after.current_run.company == source.current_run.company and after.current_run.recruit_records == source.current_run.recruit_records, "Company growth and HP are exact")
expect(after.current_run.inventory == source.current_run.inventory and after.current_run.equipped_by_recruit == source.current_run.equipped_by_recruit, "inventory and one-slot equipment are exact")
expect(after.current_run.coin == source.current_run.coin and after.current_run.replacement_credit == source.current_run.replacement_credit, "Coin and Credit are exact")
expect(after.current_run.route_history == source.current_run.route_history and after.current_run.cleared_rounds == 24, "24 settled RG-02 routes remain exact")
expect(after.current_run.active_artifact_ids == source.current_run.active_artifact_ids, "active Artifact survives exactly")
expect(after.current_run.phase == "market" and after.current_run.region_id == "RG-03" and after.current_run.market_index == 1, "migration opens RG-03 Market 1")
expect(after.current_run.market_board.recruit_slots.size() == 3 and after.current_run.market_board.equipment_offers.size() == 4 and after.current_run.market_board.battle_offers.size() == 3, "Market 1 has exact 3/4/3 board")
expect(after.operation_receipts.is_empty() and after.legacy_operation_receipts.size() >= source.operation_receipts.size(), "prior operations become immutable legacy authority")
expect(String(after.migration_source_hash).length() == 64 and after.migration_source_revision == source.revision, "migration binds exact source state")
expect(migrated.offline_delta == SaveV11.OFFLINE_ZERO, "migration invents no offline value")
var decoded := SaveV11.new().decode_text(String(migrated.text), 101)
expect(decoded.ok and decoded.snapshot == after, "Save-v11 roundtrips byte-equivalent")
func verify_checksum_backup_and_rollback() -> void:
if migrated_snapshot.is_empty():
return
var saves := SaveV11.new()
var first := saves.encode_snapshot(migrated_snapshot, 110)
var second := saves.encode_snapshot(migrated_snapshot, 111, String(first.envelope.commit_hash))
expect(first.ok and second.ok, "adjacent Save-v11 commits encode")
var corrupt = JSON.parse_string(String(second.text))
corrupt.state.current_run.coin += 999
var fallback := saves.load_with_backup(JSON.stringify(corrupt), String(first.text), 110)
expect(fallback.ok and fallback.source == "backup" and fallback.active_failure == "checksum_mismatch", "corrupt active falls back to backup")
var rollback := saves.load_with_backup(String(first.text), "", 111)
expect(not rollback.ok and rollback.active_failure == "rollback_rejected", "minimum sequence rejects rollback")
var future = JSON.parse_string(String(second.text))
future.save_version = 12
expect(not saves.decode_text(JSON.stringify(future)).ok, "future Save blocks instead of resetting")
func verify_atomic_file_rotation_and_interruption() -> void:
if migrated_snapshot.is_empty():
return
var saves := SaveV11.new()
var first := saves.write_slot(SLOT_PATH, migrated_snapshot, 1)
expect(first.ok and FileAccess.file_exists(SLOT_PATH), "first write promotes verified candidate")
var second := saves.write_slot(SLOT_PATH, migrated_snapshot, 2, String(first.commit_hash))
expect(second.ok and FileAccess.file_exists("%s.bak" % SLOT_PATH), "second write rotates active to backup")
var candidate := FileAccess.open("%s.candidate" % SLOT_PATH, FileAccess.WRITE)
candidate.store_string("interrupted partial fg4 bytes")
candidate.close()
var after_interruption := saves.load_slot(SLOT_PATH, 2)
expect(after_interruption.ok and after_interruption.sequence == 2, "orphan candidate never overrides active")
var active := FileAccess.open(SLOT_PATH, FileAccess.WRITE)
active.store_string("corrupt active")
active.close()
var backup := saves.load_slot(SLOT_PATH, 1)
expect(backup.ok and backup.source == "backup" and backup.sequence == 1, "corrupt active falls back to committed backup")
func verify_migration_boundaries() -> void:
var before_close := FG3Fixture.new().contract2_boss_muster_snapshot()
var encoded := SaveV10.new().encode_snapshot(before_close, 90)
var rejected := SaveV11.new().migrate_v10_text(String(encoded.text))
expect(encoded.ok and not rejected.ok and rejected.code == "v10_rg02_closed_required", "pre-close Save cannot skip into RG-03")
var malformed := SaveV11.new().migrate_v10_text("not-json")
expect(not malformed.ok and malformed.code == "v10_save_malformed", "malformed Save-v10 rejects explicitly")
var source := Fixture.new().encoded_v10(91)
var corrupt = JSON.parse_string(String(source.text))
corrupt.state.current_run.coin += 1
var tampered := SaveV11.new().migrate_v10_text(JSON.stringify(corrupt))
expect(not tampered.ok and tampered.code == "v10_checksum_mismatch", "tampered handoff cannot migrate")
func cleanup_files() -> void:
for file_path in [SLOT_PATH, "%s.bak" % SLOT_PATH, "%s.candidate" % SLOT_PATH]:
if FileAccess.file_exists(file_path):
DirAccess.remove_absolute(file_path)
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("FG-4 Save v11: %s" % message)