127 lines
6.6 KiB
GDScript
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)
|