132 lines
7.6 KiB
GDScript
132 lines
7.6 KiB
GDScript
extends SceneTree
|
|
|
|
const Fixture = preload("res://tests/fg3/contract1_fixture.gd")
|
|
const P103Fixture = preload("res://tests/p10_3/deep_channels_fixture.gd")
|
|
const SaveV9 = preload("res://p10_3/save_v9_service.gd")
|
|
const SaveV10 = preload("res://fg3/save_v10_service.gd")
|
|
|
|
const SLOT_PATH := "user://abg_fg3_save_v10_test.json"
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
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_and_state_boundaries()
|
|
cleanup_files()
|
|
if failures.is_empty():
|
|
print("ABG_FG3_SAVE_V10_OK migration=v9_market8 sequence=+1 company,growth,equipment,history=exact board=market8_generated 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.p103_market_8_snapshot()
|
|
var encoded_v9 := SaveV9.new().encode_snapshot(source, 60)
|
|
var migrated := SaveV10.new().migrate_v9_text(String(encoded_v9.text), 60)
|
|
expect(encoded_v9.ok and migrated.ok, "exact Save-v9 Market 8 migrates: %s" % str(migrated))
|
|
if not encoded_v9.ok or not migrated.ok:
|
|
return
|
|
var after: Dictionary = migrated.snapshot
|
|
expect(migrated.sequence == 61 and migrated.previous_commit_hash == encoded_v9.envelope.commit_hash, "migration advances and chains the Save commit")
|
|
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.guild_profile == source.guild_profile, "Guild profile is byte-equivalent")
|
|
expect(after.current_run.company == source.current_run.company and after.current_run.recruit_records == source.current_run.recruit_records, "Company records and growth are byte-equivalent")
|
|
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 ownership are byte-equivalent")
|
|
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 == 7, "seven settled routes remain exact")
|
|
expect(after.current_run.active_artifact_ids == source.current_run.active_artifact_ids and after.current_run.artifact_rule_state == source.current_run.artifact_rule_state, "Artifact decision survives exactly")
|
|
expect(after.current_run.promotion_draft == source.current_run.promotion_draft, "promotion state survives exactly")
|
|
expect(after.current_run.phase == "market" and after.current_run.market_index == 8 and after.current_run.market_board.market_index == 8, "migration opens generated FG-3 Market 8")
|
|
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 8 has exact 3/4/3 board")
|
|
expect(after.operation_receipts.is_empty() and after.legacy_operation_receipts.size() >= source.operation_receipts.size(), "old operation receipts remain 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 == SaveV10.OFFLINE_ZERO, "migration invents no offline value")
|
|
var decoded := SaveV10.new().decode_text(String(migrated.text), 61)
|
|
expect(decoded.ok and decoded.snapshot == after, "Save-v10 roundtrips byte-equivalent")
|
|
|
|
|
|
func verify_checksum_backup_and_rollback() -> void:
|
|
var saves := SaveV10.new()
|
|
var snapshot_value := Fixture.new().fg3_market_8_snapshot()
|
|
expect(not snapshot_value.is_empty(), "checksum fixture migrates")
|
|
if snapshot_value.is_empty():
|
|
return
|
|
var first := saves.encode_snapshot(snapshot_value, 70)
|
|
var second := saves.encode_snapshot(snapshot_value, 71, String(first.envelope.commit_hash))
|
|
expect(first.ok and second.ok, "adjacent Save-v10 commits encode")
|
|
var corrupt = JSON.parse_string(String(second.text))
|
|
corrupt.state.current_run.coin = 999999
|
|
var fallback := saves.load_with_backup(JSON.stringify(corrupt), String(first.text), 70)
|
|
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), "", 71)
|
|
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 = 11
|
|
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 := SaveV10.new()
|
|
var snapshot_value := Fixture.new().fg3_market_8_snapshot()
|
|
expect(not snapshot_value.is_empty(), "file fixture migrates")
|
|
if snapshot_value.is_empty():
|
|
return
|
|
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 second := saves.write_slot(SLOT_PATH, snapshot_value, 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 fg3 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_and_state_boundaries() -> void:
|
|
var market_7 := P103Fixture.new().market_7_snapshot(true)
|
|
var encoded := SaveV9.new().encode_snapshot(market_7, 5)
|
|
var active := SaveV10.new().migrate_v9_text(String(encoded.text))
|
|
expect(not active.ok and active.code == "v9_saved_market_8_required", "Market 7 cannot skip into FG-3")
|
|
var malformed := SaveV10.new().migrate_v9_text("not-json")
|
|
expect(not malformed.ok and malformed.code == "v9_save_malformed", "malformed Save-v9 rejects explicitly")
|
|
var source := Fixture.new().encoded_v9(6)
|
|
var corrupt = JSON.parse_string(String(source.text))
|
|
corrupt.state.current_run.coin += 1
|
|
var rejected := SaveV10.new().migrate_v9_text(JSON.stringify(corrupt))
|
|
expect(not rejected.ok and rejected.code == "v9_checksum_mismatch", "tampered Market-8 handoff cannot migrate")
|
|
var invalid_state := Fixture.new().fg3_market_8_snapshot()
|
|
expect(not invalid_state.is_empty(), "state-boundary fixture migrates")
|
|
if invalid_state.is_empty():
|
|
return
|
|
invalid_state.current_run.market_board.offer_level = 9
|
|
expect(not SaveV10.new().encode_snapshot(invalid_state, 1).ok, "generated Market drift rejects")
|
|
|
|
|
|
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-3 Save v10: %s" % message)
|