Files
aetherbound-guild/runtime/tests/p10_0/save_v6_test.gd
T

127 lines
6.6 KiB
GDScript

extends SceneTree
const MarketService = preload("res://p10_0/market_service.gd")
const SaveService = preload("res://p10_0/save_service.gd")
const SLOT_PATH := "/private/tmp/abg-p100-save-v6-test.json"
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
cleanup_files()
verify_roundtrip_and_operation_retry()
verify_checksum_backup_and_rollback()
verify_atomic_file_rotation_and_interruption()
verify_p9_profile_only_migration()
cleanup_files()
if failures.is_empty():
print("ABG_P10_0_SAVE_V6_OK version=6 checksum=true active_backup=true rollback=true candidate_interruption=true phases=draft,market,encounter,reward,boss,close receipts=idempotent migration=p9_guild_only offline=coin0,credit0,xp0,renown0,healing0,ticks0")
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func verify_roundtrip_and_operation_retry() -> void:
var market := MarketService.new()
var begin := market.begin_run_creation("save-begin", 0, "CTR-R1-01", "standard")
expect(begin.ok, "draft fixture begins")
var saves := SaveService.new()
var encoded := saves.encode_snapshot(market.state.snapshot(), 1)
expect(encoded.ok, "run-creation draft encodes")
var loaded := saves.decode_text(String(encoded.text), 1)
expect(loaded.ok and loaded.snapshot == market.state.snapshot(), "draft snapshot roundtrips byte-equivalent")
var resumed := MarketService.new(loaded.snapshot)
var duplicate := resumed.begin_run_creation("save-begin", 0, "CTR-R1-01", "standard")
expect(duplicate.ok and duplicate.duplicate and resumed.state.next_run_ordinal == 1, "operation receipt survives reload and prevents duplicate seed")
expect(loaded.offline_delta == {"coin": 0, "credit": 0, "xp": 0, "renown": 0, "healing": 0, "battle_ticks": 0}, "load grants zero offline value")
func verify_checksum_backup_and_rollback() -> void:
var market = created_market()
var saves := SaveService.new()
var first := saves.encode_snapshot(market.state.snapshot(), 4)
var raw = market.state.snapshot()
raw.current_run.coin = 41
expect(market.restore_snapshot(raw), "newer sequence fixture validates")
var second := saves.encode_snapshot(market.state.snapshot(), 5, String(first.envelope.commit_hash))
expect(first.ok and second.ok, "adjacent commit hashes encode")
var corrupt_value = JSON.parse_string(String(second.text))
corrupt_value.state.current_run.coin = 999
var fallback := saves.load_with_backup(JSON.stringify(corrupt_value), 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", "sequence floor rejects rollback")
var future_value = JSON.parse_string(String(second.text))
future_value.save_version = 7
var future := saves.decode_text(JSON.stringify(future_value))
expect(not future.ok and future.code == "save_version_unknown", "future save version blocks instead of resetting")
func verify_atomic_file_rotation_and_interruption() -> void:
var market = created_market()
var saves := SaveService.new()
var first := saves.write_slot(SLOT_PATH, market.state.snapshot(), 1)
expect(first.ok and FileAccess.file_exists(SLOT_PATH), "first atomic slot write promotes candidate")
var raw = market.state.snapshot()
raw.current_run.coin = 39
expect(market.restore_snapshot(raw), "second file fixture validates")
var second := saves.write_slot(SLOT_PATH, market.state.snapshot(), 2, String(first.commit_hash))
expect(second.ok and FileAccess.file_exists("%s.bak" % SLOT_PATH), "second write rotates verified active to backup")
var loaded := saves.load_slot(SLOT_PATH, 2)
expect(loaded.ok and loaded.sequence == 2 and loaded.snapshot.current_run.coin == 39, "active file loads newest verified sequence")
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.snapshot == loaded.snapshot, "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, "file load falls back to prior committed backup")
func verify_p9_profile_only_migration() -> void:
var p9 := {
"persistent_renown": 32,
"contract_clear_ids": ["CLEAR-RG01-C01", "CLEAR-RG01-C02"],
"discovery_ids": ["BOSS-BO-R1-01"],
"company_records": [{"id": "P9-RECORD-01", "clear_id": "CLEAR-RG01-C01"}],
"current_run": {"run_id": "forbidden-p9-run", "company": ["forbidden-recruit"]},
}
var migrated := SaveService.new().migrate_p9_guild_profile(p9, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
expect(migrated.ok and migrated.snapshot.current_run.is_empty() and migrated.snapshot.run_creation_draft.is_empty(), "migration imports no P9 Run or draft")
expect(migrated.snapshot.guild_profile.persistent_renown == 32 and migrated.snapshot.guild_profile.company_records.size() == 1, "migration preserves explicit Guild facts and Record")
expect("RG-02" in migrated.snapshot.guild_profile.unlocked_region_ids and migrated.imported_recruit_ids == 0, "two clear facts expose RG-02 without importing Recruit identity")
func created_market():
var market := MarketService.new()
expect(market.begin_run_creation("fixture-begin", 0, "CTR-R1-01", "standard").ok, "fixture begin")
var draft_id := String(market.state.run_creation_draft.draft_id)
expect(market.materialize_opening_offers("fixture-materialize", market.state.revision, draft_id).ok, "fixture materialize")
var ids: Array[String] = []
for offer in market.state.run_creation_draft.opening_offers:
ids.append(String(offer.source_offer_id))
expect(market.set_opening_selection("fixture-select", market.state.revision, [ids[0], ids[1]]).ok, "fixture select")
expect(market.commit_run_creation("fixture-commit", market.state.revision, draft_id, int(market.state.run_creation_draft.selection_revision), Array(market.state.run_creation_draft.selected_offer_ids).duplicate(), String(market.state.run_creation_draft.opening_offer_set_hash)).ok, "fixture commit")
return market
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.0 save v6: %s" % message)