Files
aetherbound-guild/runtime/tests/p10_2/save_v8_test.gd
T

120 lines
7.5 KiB
GDScript

extends SceneTree
const P101Save = preload("res://p10_1/save_v7_service.gd")
const SaveV8 = preload("res://p10_2/save_v8_service.gd")
const Fixture = preload("res://tests/p10_2/mid_contract_fixture.gd")
const SLOT_PATH := "/private/tmp/abg-p102-save-v8-test.json"
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
cleanup_files()
verify_exact_v7_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_P10_2_SAVE_V8_OK version=8 migration=v7_market2_exact board=3,4,3 cleared=1 capacity=4 company=true route_history=1 checksum=true active_backup=true rollback=true candidate_interruption=true legacy_receipts=true offline=coin0,credit0,xp0,renown0,healing0,ticks0")
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func verify_exact_v7_migration_and_roundtrip() -> void:
var fixture = Fixture.new()
var source_snapshot: Dictionary = fixture.p101_market_2_snapshot()
var v7 := P101Save.new().encode_snapshot(source_snapshot, 7)
expect(v7.ok, "saved Market 2 fixture encodes as Save v7")
var saves := SaveV8.new()
var migrated := saves.migrate_v7_text(String(v7.text), 7)
expect(migrated.ok and migrated.sequence == 8 and migrated.previous_commit_hash == v7.envelope.commit_hash, "migration advances sequence and binds the v7 commit")
expect(migrated.snapshot.current_run.run_id == source_snapshot.current_run.run_id and migrated.snapshot.current_run.run_seed == source_snapshot.current_run.run_seed, "migration preserves exact Run identity")
expect(migrated.snapshot.current_run.company == source_snapshot.current_run.company and migrated.snapshot.current_run.coin == source_snapshot.current_run.coin, "migration preserves Company and Coin")
expect(migrated.snapshot.current_run.inventory == source_snapshot.current_run.inventory and migrated.snapshot.current_run.equipped_by_recruit == source_snapshot.current_run.equipped_by_recruit, "migration preserves equipment ownership")
expect(migrated.snapshot.current_run.market_index == 2 and migrated.snapshot.current_run.phase == "market" and migrated.snapshot.current_run.cleared_rounds == 1, "migration opens exact repeated Market 2")
expect(migrated.snapshot.current_run.line_capacity == 4 and migrated.snapshot.current_run.market_board.recruit_offers.size() == 3 and migrated.snapshot.current_run.market_board.equipment_offers.size() == 4 and migrated.snapshot.current_run.market_board.battle_offers.size() == 3, "migration binds exact capacity and 3/4/3 board")
expect(migrated.snapshot.current_run.route_history.size() == 1 and migrated.snapshot.current_run.route_history[0].battle_hash == source_snapshot.current_run.market_2_handoff.source_battle_hash, "first crossing becomes one exact route-history row")
expect(migrated.snapshot.legacy_operation_receipts == source_snapshot.operation_receipts and migrated.snapshot.legacy_entry_commits == source_snapshot.entry_commits, "legacy receipts and entry lineage are preserved")
expect(migrated.offline_delta == SaveV8.OFFLINE_ZERO, "migration invents no offline value")
var decoded := saves.decode_text(String(migrated.text), 8)
expect(decoded.ok and decoded.snapshot == migrated.snapshot, "migrated Save v8 roundtrips byte-equivalent")
func verify_checksum_backup_and_rollback() -> void:
var saves := SaveV8.new()
var migrated := saves.migrate_v7_text(String(Fixture.new().encoded_v7(3).text))
var first := saves.encode_snapshot(migrated.snapshot, 4, String(migrated.envelope.previous_commit_hash))
var service = Fixture.new().market_service()
var recruit: Dictionary = service.state.current_run.market_board.recruit_offers[0]
expect(service.hire("save-hire", service.state.revision, String(recruit.source_offer_id), int(recruit.list_price)).ok, "newer fixture applies one Market operation")
var second := saves.encode_snapshot(service.state.snapshot(), 5, String(first.envelope.commit_hash))
expect(first.ok and second.ok, "adjacent v8 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), 4)
expect(fallback.ok and fallback.source == "backup" and fallback.active_failure == "checksum_mismatch", "corrupt active falls back to valid backup")
var rollback := saves.load_with_backup(String(first.text), "", 5)
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 = 9
var rejected := saves.decode_text(JSON.stringify(future))
expect(not rejected.ok and rejected.code == "save_version_unknown", "future Save blocks instead of resetting")
func verify_atomic_file_rotation_and_interruption() -> void:
var saves := SaveV8.new()
var snapshot_value := Fixture.new().migrated_snapshot()
var first := saves.write_slot(SLOT_PATH, snapshot_value, 1)
expect(first.ok and FileAccess.file_exists(SLOT_PATH), "first write promotes verified candidate")
var service = Fixture.new().market_service()
var offer_id := String(service.state.current_run.available_recruit_offer_ids[0])
expect(service.lock_offer("file-lock", service.state.revision, offer_id, 2).ok, "file fixture applies a lock")
var second := saves.write_slot(SLOT_PATH, service.state.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 bytes")
candidate.close()
var after_interruption := saves.load_slot(SLOT_PATH, 2)
expect(after_interruption.ok and after_interruption.sequence == 2 and offer_id in after_interruption.snapshot.current_run.locked_offer_ids, "orphan candidate never overrides committed 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 fixture = Fixture.new()
var prepared: Dictionary = preload("res://tests/p10_1/first_crossing_fixture.gd").new().prepared_snapshot(0)
var prepared_v7 := P101Save.new().encode_snapshot(prepared, 1)
expect(prepared_v7.ok, "prepared P10.1 fixture is a valid v7 state")
var active := SaveV8.new().migrate_v7_text(String(prepared_v7.text))
expect(not active.ok and active.code == "v7_saved_market_2_required", "active first crossing cannot skip into mid-Contract")
var malformed := SaveV8.new().migrate_v7_text("not-json")
expect(not malformed.ok and malformed.code == "v7_save_malformed", "malformed v7 save rejects explicitly")
var source := fixture.encoded_v7(2)
var corrupt = JSON.parse_string(String(source.text))
corrupt.state.current_run.market_2_handoff = {}
var rejected := SaveV8.new().migrate_v7_text(JSON.stringify(corrupt))
expect(not rejected.ok and rejected.code == "v7_checksum_mismatch", "tampered Market-2 handoff cannot migrate")
func cleanup_files() -> void:
for path in [SLOT_PATH, "%s.bak" % SLOT_PATH, "%s.candidate" % SLOT_PATH]:
if FileAccess.file_exists(path):
DirAccess.remove_absolute(path)
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("P10.2 Save v8: %s" % message)