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
@@ -1055,6 +1055,12 @@ static func _snapshot_package_format(snapshot: Dictionary) -> int:
|
|||||||
# Keep PurchaseQuotePlansV2's exact original allowlist and all format rules.
|
# Keep PurchaseQuotePlansV2's exact original allowlist and all format rules.
|
||||||
runtime_data = runtime_data.duplicate()
|
runtime_data = runtime_data.duplicate()
|
||||||
runtime_data.erase(&"special_peeping_rooms")
|
runtime_data.erase(&"special_peeping_rooms")
|
||||||
|
if runtime_data.has(&"mana_reserve"):
|
||||||
|
# A06 finite reserve: validate the section, then keep the exact original
|
||||||
|
# v15 purchase allowlist unchanged, as for the peeping-room extension.
|
||||||
|
if not preload("res://scripts/mana/mana_reserve_cycle.gd").new().restore_snapshot(runtime_data[&"mana_reserve"]): return -1
|
||||||
|
runtime_data = runtime_data.duplicate()
|
||||||
|
runtime_data.erase(&"mana_reserve")
|
||||||
if version >= 1 and version <= 14:
|
if version >= 1 and version <= 14:
|
||||||
return -1 if PurchaseQuoteV2.has_history(runtime_data) else FORMAT_VERSION
|
return -1 if PurchaseQuoteV2.has_history(runtime_data) else FORMAT_VERSION
|
||||||
if version != 15: return -1
|
if version != 15: return -1
|
||||||
|
|||||||
@@ -274,6 +274,42 @@ func _legacy_controls(writer: Dictionary, reader: Dictionary) -> void:
|
|||||||
_legacy_control(writer, reader, "byte-identical deep copy", "copy")
|
_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})
|
_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:
|
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 reader: Dictionary = runtime.create_snapshot()
|
||||||
var old: Dictionary = writer[&"systems"][&"runtime"][&"domain_events"]["recent_events"].back().duplicate(true)
|
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)
|
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
|
event["event_type"] = actual["event_type"]; event["payload"] = actual["payload"].duplicate(true); replaced += 1
|
||||||
var migration_proof := {}
|
var migration_proof := {}
|
||||||
|
var runtime_migrations := _approved_runtime_migrations(expected, reader)
|
||||||
|
valid = valid and runtime_migrations["ok"]
|
||||||
if migrate_relationships_v1:
|
if migrate_relationships_v1:
|
||||||
_legacy_controls(expected, reader)
|
_legacy_controls(expected, reader)
|
||||||
var migration := _legacy_checkpoint(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:
|
if migrate_relationships_v1:
|
||||||
binding["mapped_fields"].append("relationships_v1_to_v2")
|
binding["mapped_fields"].append("relationships_v1_to_v2")
|
||||||
binding["relationships_v1_to_v2"] = migration_proof
|
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)
|
_proof(label + "-io1-binding", binding)
|
||||||
expect(valid, "whole typed loaded state unchanged except complete identity/saveID-bound IO1: " + label)
|
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")
|
_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 before: Dictionary = _runtime.defense_service.create_snapshot()
|
||||||
var rng_before: Dictionary = _runtime.get_rng_stream(&"defense").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)
|
_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
|
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(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")
|
_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")
|
expect(palette != null, "production facility palette exists")
|
||||||
if palette == null:
|
if palette == null:
|
||||||
return
|
return
|
||||||
expect(palette.get_child_count() == 23 and palette.columns == 23,
|
expect(palette.get_child_count() == 25 and palette.columns == 25,
|
||||||
"palette has exactly 23 facility slots")
|
"palette has exactly 25 facility slots (A07/A08 warps added)")
|
||||||
expect(main.build_buttons.size() == 23, "all 23 registered facility buttons exist")
|
expect(main.build_buttons.size() == 25, "all 25 registered facility buttons exist")
|
||||||
for child: Node in palette.get_children():
|
for child: Node in palette.get_children():
|
||||||
expect(child is Button and main.build_buttons.values().has(child),
|
expect(child is Button and main.build_buttons.values().has(child),
|
||||||
"every palette child is a registered facility button")
|
"every palette child is a registered facility button")
|
||||||
|
|||||||
@@ -407,7 +407,7 @@
|
|||||||
"new_audio_QA": false
|
"new_audio_QA": false
|
||||||
},
|
},
|
||||||
"tests/ui/test_p08_normal_placement_ops.gd": {
|
"tests/ui/test_p08_normal_placement_ops.gd": {
|
||||||
"source_sha256": "d3c246d34f8fd66f6ed237e42a86df89ebaf481bc9dee143b4de8d654ce0eb2e",
|
"source_sha256": "a5c5abdc1348e7dadafd70f73072ac98afcc4c918211c3389e88da5e903af803",
|
||||||
"ordinary_cleanup_prepared": true,
|
"ordinary_cleanup_prepared": true,
|
||||||
"ordinary_control_labels": [
|
"ordinary_control_labels": [
|
||||||
"single"
|
"single"
|
||||||
|
|||||||
Reference in New Issue
Block a user