Fix regressions in roster-only suites from A06/A07/P14-03
The formal collector's roster includes 46 suites the Makefile never ran; the first formal attempt stopped on test_required_kitchen and a sweep found: - v15 MIMI purchase saves could not be written once Runtime saves carried mana_reserve (P06-01): the format detector now validates that section and excludes it before the unchanged v15 allowlist, like special_peeping_rooms. - test_required_kitchen's legacy14 exact-load comparison now maps the two approved migrations (fresh A06 reserve at the restored boundary, elevator snapshot v1->v2) after checking each against its contract, in Runtime key order; every other byte must still match. - test_runtime_defense_autonomy allowed Mana to change only by the A06 flow. - test_p08_normal_placement_ops expects 25 palette slots (two warps); its suite-policy pin is updated with flags unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
40364345ab
commit
83b170a4c7
@@ -274,6 +274,42 @@ func _legacy_controls(writer: Dictionary, reader: Dictionary) -> void:
|
||||
_legacy_control(writer, reader, "byte-identical deep copy", "copy")
|
||||
_proof("legacy14-migration-controls", {"attempts": 17, "writer_sha256": _typed_sha(writer), "reader_sha256": _typed_sha(reader), "failures": failures})
|
||||
|
||||
## Approved later save migrations applied when an older save is loaded, each
|
||||
## checked against its own contract and then mapped into `expected` in place:
|
||||
## A06 starts a fresh finite Mana reserve at the restored boundary, and P14-03
|
||||
## raises the elevator snapshot from v1 to v2 with every other field unchanged.
|
||||
func _approved_runtime_migrations(expected: Dictionary, reader: Dictionary) -> Dictionary:
|
||||
var old: Dictionary = expected[&"systems"][&"runtime"]
|
||||
var now: Dictionary = reader[&"systems"][&"runtime"]
|
||||
var fields: Array = []
|
||||
var proof := {}
|
||||
var ok := true
|
||||
if not old.has(&"mana_reserve") and now.has(&"mana_reserve"):
|
||||
var minute := int(expected[&"clock"][&"absolute_minute"])
|
||||
var fresh := {&"version": 1, &"policy_id": &"MP-01-r02", &"capacity": 300, &"generation": 20,
|
||||
&"interval_minutes": 10, &"minute": minute - minute % 10, &"last_flow": {}}
|
||||
ok = ok and now[&"mana_reserve"] == fresh
|
||||
old[&"mana_reserve"] = now[&"mana_reserve"].duplicate(true)
|
||||
fields.append("mana_reserve_absent_to_A06_fresh_start")
|
||||
proof["mana_reserve"] = {"fresh_start_minute": minute - minute % 10, "matches_contract": ok}
|
||||
if old.get(&"elevators", {}).get(&"version") == 1 and now.get(&"elevators", {}).get(&"version") == 2:
|
||||
var raised: Dictionary = old[&"elevators"].duplicate(true)
|
||||
raised[&"version"] = 2
|
||||
var same_otherwise: bool = raised == now[&"elevators"]
|
||||
ok = ok and same_otherwise
|
||||
old[&"elevators"] = raised
|
||||
fields.append("elevators_v1_to_v2")
|
||||
proof["elevators"] = {"only_version_changed": same_otherwise}
|
||||
if not fields.is_empty():
|
||||
# Saves keep the Runtime's own key order, so the mapped tree follows it.
|
||||
ok = ok and old.size() == now.size()
|
||||
var ordered := {}
|
||||
for key: Variant in now:
|
||||
ok = ok and old.has(key)
|
||||
ordered[key] = old.get(key)
|
||||
expected[&"systems"][&"runtime"] = ordered
|
||||
return {"ok": ok, "fields": fields, "proof": proof}
|
||||
|
||||
func _compare_loaded(writer: Dictionary, write_path: String, read_path: String, label: String, migrate_relationships_v1: bool = false) -> void:
|
||||
var reader: Dictionary = runtime.create_snapshot()
|
||||
var old: Dictionary = writer[&"systems"][&"runtime"][&"domain_events"]["recent_events"].back().duplicate(true)
|
||||
@@ -286,6 +322,8 @@ func _compare_loaded(writer: Dictionary, write_path: String, read_path: String,
|
||||
valid = valid and var_to_bytes(event) == var_to_bytes(old)
|
||||
event["event_type"] = actual["event_type"]; event["payload"] = actual["payload"].duplicate(true); replaced += 1
|
||||
var migration_proof := {}
|
||||
var runtime_migrations := _approved_runtime_migrations(expected, reader)
|
||||
valid = valid and runtime_migrations["ok"]
|
||||
if migrate_relationships_v1:
|
||||
_legacy_controls(expected, reader)
|
||||
var migration := _legacy_checkpoint(expected, reader)
|
||||
@@ -300,6 +338,8 @@ func _compare_loaded(writer: Dictionary, write_path: String, read_path: String,
|
||||
if migrate_relationships_v1:
|
||||
binding["mapped_fields"].append("relationships_v1_to_v2")
|
||||
binding["relationships_v1_to_v2"] = migration_proof
|
||||
binding["mapped_fields"].append_array(runtime_migrations["fields"])
|
||||
binding["approved_runtime_migrations"] = runtime_migrations["proof"]
|
||||
_proof(label + "-io1-binding", binding)
|
||||
expect(valid, "whole typed loaded state unchanged except complete identity/saveID-bound IO1: " + label)
|
||||
|
||||
|
||||
@@ -328,10 +328,17 @@ func _test_exhausted_generated_ids_fail_atomically() -> void:
|
||||
_expect(_runtime.id_generator.restore_snapshot(allocator), "valid near-exhausted allocator is a disclosed saved-state boundary")
|
||||
var before: Dictionary = _runtime.defense_service.create_snapshot()
|
||||
var rng_before: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
|
||||
var mana_before: int = _state.get_resource(&"mana")
|
||||
var reserve_minute_before: int = _runtime.mana_reserve.get_minute()
|
||||
_advance(5)
|
||||
# A06: crossing a 10-minute boundary settles the finite reserve; that flow is
|
||||
# the only Mana change allowed here.
|
||||
var reserve_mana := mana_before
|
||||
if _runtime.mana_reserve.get_minute() != reserve_minute_before:
|
||||
reserve_mana = int(_runtime.mana_reserve.get_last_flow()[&"after"])
|
||||
before[&"current_minute"] = _clock.absolute_minute
|
||||
_expect(before == _runtime.defense_service.create_snapshot() and rng_before == _runtime.get_rng_stream(&"defense").snapshot() and _clock.paused, "exhausted automatic allocator fails without advancing combat or RNG")
|
||||
_expect(_state.get_resource(&"gold") == 5_000 and _state.get_resource(&"mana") == 100, "allocation failure charges no skill or outcome resources")
|
||||
_expect(_state.get_resource(&"gold") == 5_000 and _state.get_resource(&"mana") == reserve_mana, "allocation failure charges no skill or outcome resources (Mana moves only by the A06 reserve flow)")
|
||||
_expect(_count_domain_events(&"DEFENSE_AUTOMATIC_ROUND_FAILED", &"raid.id.exhausted") == 1 and _runtime.id_generator.get_counter(&"defense_round_resolution") == StableIdGenerator.MAX_COUNTER - 1, "exhaustion records one diagnostic and rolls skipped-ID allocation back")
|
||||
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ class PalettePlacement extends Placement:
|
||||
expect(palette != null, "production facility palette exists")
|
||||
if palette == null:
|
||||
return
|
||||
expect(palette.get_child_count() == 23 and palette.columns == 23,
|
||||
"palette has exactly 23 facility slots")
|
||||
expect(main.build_buttons.size() == 23, "all 23 registered facility buttons exist")
|
||||
expect(palette.get_child_count() == 25 and palette.columns == 25,
|
||||
"palette has exactly 25 facility slots (A07/A08 warps added)")
|
||||
expect(main.build_buttons.size() == 25, "all 25 registered facility buttons exist")
|
||||
for child: Node in palette.get_children():
|
||||
expect(child is Button and main.build_buttons.values().has(child),
|
||||
"every palette child is a registered facility button")
|
||||
|
||||
Reference in New Issue
Block a user