120 lines
7.5 KiB
GDScript
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)
|