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:
Takeshi
2026-10-20 17:08:40 +09:00
co-authored by Claude Opus 5.5
parent 40364345ab
commit 83b170a4c7
5 changed files with 58 additions and 5 deletions
+40
View File
@@ -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)
+8 -1
View File
@@ -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")
+3 -3
View File
@@ -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")