Files
aetherbound-guild/runtime/tests/p10_3/save_v9_test.gd
T

140 lines
9.0 KiB
GDScript

extends SceneTree
const SaveV8 = preload("res://p10_2/save_v8_service.gd")
const SaveV9 = preload("res://p10_3/save_v9_service.gd")
const Fixture = preload("res://tests/p10_3/deep_channels_fixture.gd")
const SLOT_PATH := "/private/tmp/abg-p103-save-v9-test.json"
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
cleanup_files()
verify_exact_v8_migration_and_roundtrip()
verify_checksum_backup_and_rollback()
verify_atomic_file_rotation_and_interruption()
verify_migration_boundaries()
verify_state_corruption_boundaries()
cleanup_files()
if failures.is_empty():
print("ABG_P10_3_SAVE_V9_OK version=9 migration=v8_market5_exact board=3,4,3 cleared=4 capacity=6 company=true route_history=4 artifact_state=true promotion_state=true 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_v8_migration_and_roundtrip() -> void:
var fixture = Fixture.new()
var source_snapshot: Dictionary = fixture.p102_market_5_snapshot()
var v8 := SaveV8.new().encode_snapshot(source_snapshot, 20)
expect(v8.ok, "stable Market 5 fixture encodes as Save v8")
var saves := SaveV9.new()
var migrated := saves.migrate_v8_text(String(v8.text), 20)
expect(migrated.ok and migrated.sequence == 21 and migrated.previous_commit_hash == v8.envelope.commit_hash, "migration advances sequence and binds the v8 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 == 5 and migrated.snapshot.current_run.phase == "market" and migrated.snapshot.current_run.cleared_rounds == 4, "migration opens exact deep-channel Market 5")
expect(migrated.snapshot.current_run.line_capacity == 6 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 == source_snapshot.current_run.route_history, "four completed routes remain byte-equivalent")
expect(migrated.snapshot.current_run.active_artifact_ids.is_empty() and migrated.snapshot.current_run.artifact_draft.is_empty() and migrated.snapshot.current_run.promotion_draft.is_empty(), "growth state starts empty before Round 6")
for recruit_id in migrated.snapshot.current_run.company:
var before: Dictionary = source_snapshot.current_run.recruit_records[recruit_id]
var after: Dictionary = migrated.snapshot.current_run.recruit_records[recruit_id]
expect(after.origin_profession_id == before.base_profession_id and after.profession_id == before.profession_id, "%s preserves Profession identity" % recruit_id)
expect(after.level == before.level and after.xp == before.xp and after.current_hp == before.current_hp and after.max_hp == before.max_hp, "%s preserves level, XP and HP" % recruit_id)
expect(after.promotion_state == "base" and after.promotion_history.is_empty(), "%s receives an empty promotion state" % recruit_id)
for operation_id in source_snapshot.operation_receipts:
expect(migrated.snapshot.legacy_operation_receipts.has(operation_id), "P10.2 operation %s is retained" % operation_id)
expect(migrated.offline_delta == SaveV9.OFFLINE_ZERO, "migration invents no offline value")
var decoded := saves.decode_text(String(migrated.text), 21)
expect(decoded.ok and decoded.snapshot == migrated.snapshot, "migrated Save v9 roundtrips byte-equivalent")
func verify_checksum_backup_and_rollback() -> void:
var saves := SaveV9.new()
var snapshot_value := Fixture.new().migrated_snapshot(4)
var first := saves.encode_snapshot(snapshot_value, 5)
var selected_value := Fixture.new().market_7_snapshot()
var second := saves.encode_snapshot(selected_value, 6, String(first.envelope.commit_hash))
expect(first.ok and second.ok, "adjacent v9 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), 5)
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), "", 6)
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 = 10
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 := SaveV9.new()
var first := saves.write_slot(SLOT_PATH, Fixture.new().migrated_snapshot(), 1)
expect(first.ok and FileAccess.file_exists(SLOT_PATH), "first write promotes verified candidate")
var second := saves.write_slot(SLOT_PATH, Fixture.new().market_7_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 growth bytes")
candidate.close()
var after_interruption := saves.load_slot(SLOT_PATH, 2)
expect(after_interruption.ok and after_interruption.sequence == 2 and after_interruption.snapshot.current_run.market_index == 7, "orphan candidate never overrides committed growth state")
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 Market-5 backup")
func verify_migration_boundaries() -> void:
var p102_market_4 = preload("res://tests/p10_2/mid_contract_fixture.gd").new().market_service_at(4)
var active_v8 := SaveV8.new().encode_snapshot(p102_market_4.state.snapshot(), 1)
expect(active_v8.ok, "active P10.2 Market 4 fixture is a valid v8 state")
var active := SaveV9.new().migrate_v8_text(String(active_v8.text))
expect(not active.ok and active.code == "v8_saved_market_5_required", "active Round 4 cannot skip into deep channels")
var malformed := SaveV9.new().migrate_v8_text("not-json")
expect(not malformed.ok and malformed.code == "v8_save_malformed", "malformed v8 save rejects explicitly")
var source := Fixture.new().encoded_v8(2)
var corrupt = JSON.parse_string(String(source.text))
corrupt.state.current_run.reed_ferry_restored = false
var rejected := SaveV9.new().migrate_v8_text(JSON.stringify(corrupt))
expect(not rejected.ok and rejected.code == "v8_checksum_mismatch", "tampered Ferry handoff cannot migrate")
func verify_state_corruption_boundaries() -> void:
var saves := SaveV9.new()
var market_7 := Fixture.new().market_7_snapshot()
var wrong_seed := market_7.duplicate(true)
wrong_seed.current_run.artifact_draft.run_seed = "0".repeat(32)
expect(not saves.encode_snapshot(wrong_seed, 1).ok, "Artifact draft bound to another Run rejects")
var wrong_level := market_7.duplicate(true)
wrong_level.current_run.market_board.offer_level = 2
wrong_level.current_run.market_board.erase("board_hash")
wrong_level.current_run.market_board.board_hash = preload("res://p10_3/market_board_service.gd").hash_value(wrong_level.current_run.market_board)
expect(not saves.encode_snapshot(wrong_level, 1).ok, "Market-7 offer-level drift rejects even with a recomputed board hash")
var wrong_growth := market_7.duplicate(true)
wrong_growth.current_run.market_board.growth_disclosure.milestone = "linked_positions"
wrong_growth.current_run.market_board.erase("board_hash")
wrong_growth.current_run.market_board.board_hash = preload("res://p10_3/market_board_service.gd").hash_value(wrong_growth.current_run.market_board)
expect(not saves.encode_snapshot(wrong_growth, 1).ok, "Market-7 growth disclosure drift rejects even with a recomputed board hash")
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("P10.3 Save v9: %s" % message)