Files
aetherbound-guild/runtime/tests/p10_1/save_v7_test.gd
T

132 lines
6.8 KiB
GDScript

extends SceneTree
const P100Market = preload("res://p10_0/market_service.gd")
const P100Save = preload("res://p10_0/save_service.gd")
const RegionService = preload("res://p10_1/region_market_service.gd")
const SaveV7 = preload("res://p10_1/save_v7_service.gd")
const SLOT_PATH := "/private/tmp/abg-p101-save-v7-test.json"
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
cleanup_files()
verify_v6_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_1_SAVE_V7_OK version=7 migration=v6_exact region_entry=true checksum=true active_backup=true rollback=true candidate_interruption=true receipts=idempotent offline=coin0,credit0,xp0,renown0,healing0,ticks0")
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func verify_v6_migration_and_roundtrip() -> void:
var v6 := encoded_v6(unlocked_p100_snapshot(), 6)
var saves := SaveV7.new()
var migrated := saves.migrate_v6_text(String(v6.text), 6)
expect(migrated.ok and migrated.sequence == 7 and migrated.previous_commit_hash == v6.envelope.commit_hash, "migration advances sequence and binds the v6 commit")
expect(migrated.snapshot.guild_profile.persistent_renown == 32 and "RG-02" in migrated.snapshot.guild_profile.unlocked_region_ids, "migration preserves exact Guild facts")
expect(migrated.snapshot.current_run.is_empty() and migrated.offline_delta == SaveV7.OFFLINE_ZERO, "migration invents no Run or offline value")
var decoded := saves.decode_text(String(migrated.text), 7)
expect(decoded.ok and decoded.snapshot == migrated.snapshot, "migrated Save v7 roundtrips byte-equivalent")
var region := RegionService.new(decoded.snapshot)
var entered := region.begin_region_run("save-entry", region.state.revision, "standard")
expect(entered.ok, "loaded Save enters Siltwake")
var encoded := saves.encode_snapshot(region.state.snapshot(), 8, String(decoded.commit_hash))
expect(encoded.ok, "active Market 1 encodes")
var loaded := saves.decode_text(String(encoded.text), 8)
var resumed := RegionService.new(loaded.snapshot)
var duplicate := resumed.begin_region_run("save-entry", 0, "standard")
expect(duplicate.ok and duplicate.duplicate and resumed.state.snapshot() == loaded.snapshot, "entry receipt survives reload and prevents a duplicate seed")
func verify_checksum_backup_and_rollback() -> void:
var saves := SaveV7.new()
var migrated := saves.migrate_v6_text(String(encoded_v6(unlocked_p100_snapshot(), 3).text))
var first := saves.encode_snapshot(migrated.snapshot, 4, String(migrated.envelope.previous_commit_hash))
var region := RegionService.new(migrated.snapshot)
expect(region.begin_region_run("newer-entry", region.state.revision, "wayfinder").ok, "newer fixture enters")
var second := saves.encode_snapshot(region.state.snapshot(), 5, String(first.envelope.commit_hash))
expect(first.ok and second.ok, "adjacent v7 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 = 8
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 := SaveV7.new()
var migrated := saves.migrate_v6_text(String(encoded_v6(unlocked_p100_snapshot(), 0).text))
var first := saves.write_slot(SLOT_PATH, migrated.snapshot, 1)
expect(first.ok and FileAccess.file_exists(SLOT_PATH), "first write promotes verified candidate")
var region := RegionService.new(migrated.snapshot)
expect(region.begin_region_run("file-entry", 0, "stormbound").ok, "file fixture enters")
var second := saves.write_slot(SLOT_PATH, region.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 after_interruption.snapshot.current_run.region_id == "RG-02", "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 saves := SaveV7.new()
var locked := saves.migrate_v6_text(String(encoded_v6(P100Market.new().state.snapshot(), 1).text))
expect(not locked.ok and locked.code == "v6_region_locked", "locked v6 Guild cannot enter RG-02")
var active_market := P100Market.new()
expect(active_market.begin_run_creation("active-draft", 0, "CTR-R1-01", "standard").ok, "active v6 fixture creates a draft")
var active := saves.migrate_v6_text(String(encoded_v6(active_market.state.snapshot(), 1).text))
expect(not active.ok and active.code == "v6_active_run_not_migratable", "active v6 Run cannot be silently discarded")
var malformed := saves.migrate_v6_text("not-json")
expect(not malformed.ok and malformed.code == "v6_save_malformed", "malformed v6 save rejects explicitly")
func unlocked_p100_snapshot() -> Dictionary:
var base := P100Market.new()
var raw := base.state.snapshot()
raw.guild_profile.persistent_renown = 32
raw.guild_profile.contract_clear_ids = ["CLEAR-RG01-C01", "CLEAR-RG01-C02"]
raw.guild_profile.unlocked_region_ids = ["RG-01", "RG-02"]
expect(base.restore_snapshot(raw), "P10.0 unlocked Guild fixture validates")
return base.state.snapshot()
func encoded_v6(snapshot_value: Dictionary, sequence: int) -> Dictionary:
var encoded := P100Save.new().encode_snapshot(snapshot_value, sequence)
expect(encoded.ok, "v6 fixture encodes")
return encoded
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.1 Save v7: %s" % message)