Files
TheTree_mock/tests/s5/test_runtime_defense_autonomy.gd
TakeshiandClaude Opus 5.5 83b170a4c7 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>
2026-10-20 17:08:40 +09:00

355 lines
27 KiB
GDScript

extends "res://tests/s5/test_runtime_defense_cycle.gd"
## All threat strengths, skills,
## costs and wave limits below are disclosed fixtures, not production values.
## Autonomy is enabled in every Runtime, with no test-only opt-out switch.
func _run() -> void:
print("Defense autonomy: ordinary clock, command precedence and restart")
_test_ordinary_due_pause_two_waves_and_terminal()
_test_multiple_raids_stable_order()
_test_automatic_failure_pause_retry_and_atomicity()
_test_manual_failure_is_not_hidden_by_same_tick_auto()
_test_history_replay_does_not_skip_due_automatic_round()
_test_history_replay_preserves_prior_manual_failure_guard()
_test_resolution_history_is_scoped_per_raid()
_test_generated_ids_skip_existing_saved_history()
_test_exhausted_generated_ids_fail_atomically()
_test_restart_mid_interval_and_speed_equivalence()
_test_auto_breach_projects_facility_suspension()
_cleanup()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_ordinary_due_pause_two_waves_and_terminal() -> void:
_reset_runtime(91_001)
_create_staff_and_facility(&"guard.auto", &"objective.auto", 100, 0, 9_900)
_prepare_started_raid(&"raid.auto", &"guard.auto", &"objective.auto", [
_wave(&"wave.auto.1", 1_000, 1, [&"gate.north"]),
_wave(&"wave.auto.2", 1_000, 1, [&"gate.south"]),
])
# A second incident stays preparing even while the first battle advances.
_create_staff_and_facility(&"guard.prep", &"objective.prep")
_expect(_submit(&"create_defense_raid", {
&"raid_id": &"raid.prep", &"definition": _raid_definition(
&"objective.prep", [_wave(&"wave.prep", 100, 1, [&"gate.north"])]
),
}).is_success(), "a separate preparing incident exists without automatic kickoff")
var start := int(_clock.absolute_minute)
var rng_before: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
_expect(_clock.process_real_delta(10.0) == 0 and _clock.absolute_minute == start, "real paused play advances no game minute or defense")
_advance(4)
_expect(_runtime.defense_service.get_raid(&"raid.auto")[&"current_round"] == 0 and _runtime.get_rng_stream(&"defense").snapshot() == rng_before, "deadline-minus-one neither attacks nor consumes combat RNG")
var due: Array[Dictionary] = _runtime.defense_service.get_due_rounds(start + 5)
_expect(due.size() == 1 and due[0][&"raid_id"] == &"raid.auto", "compact due projection includes only active due battles")
due[0][&"round_number"] = 999
_expect(_runtime.defense_service.get_due_rounds(start + 5)[0][&"round_number"] == 1, "due projection cannot mutate its raid")
var processed_before := int(_runtime.command_queue.processed_count())
_advance(1)
var raid: Dictionary = _runtime.defense_service.get_raid(&"raid.auto")
_expect(raid[&"current_round"] == 1 and raid[&"state"] == DefenseService.RaidState.BETWEEN_WAVES, "exact ordinary deadline resolves one first wave")
_expect(_runtime.command_queue.processed_count() == processed_before, "automatic progression creates no fake player command")
_expect(_runtime.staff_service.get_staff(&"guard.auto").state == StaffRuntime.State.UNAVAILABLE and _runtime.facility_service.get_instance(&"objective.auto").durability == 9_900, "between-wave damage stays in Defense and employee remains reserved")
var between_rng: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
_advance(20)
_expect(_runtime.defense_service.get_raid(&"raid.auto")[&"current_round"] == 1 and _runtime.get_rng_stream(&"defense").snapshot() == between_rng, "between-wave waiting invents no cooldown or automatic next wave")
_expect(_runtime.defense_service.get_raid(&"raid.prep")[&"state"] == DefenseService.RaidState.PREPARING, "ordinary time never starts preparation automatically")
_expect(_submit(&"begin_next_defense_wave", {&"raid_id": &"raid.auto"}).is_success(), "existing player command explicitly begins the second wave")
_advance(5)
raid = _runtime.defense_service.get_raid(&"raid.auto")
_expect(raid[&"state"] == DefenseService.RaidState.DEFENDED and raid[&"current_round"] == 2, "second wave reaches terminal state from ordinary clock only")
var guard: StaffRuntime = _runtime.staff_service.get_staff(&"guard.auto")
_expect(guard.health == raid[&"combatants"][&"guard.auto"][&"health"] and guard.fatigue == raid[&"combatants"][&"guard.auto"][&"fatigue"] and guard.state == StaffRuntime.State.AVAILABLE, "automatic terminal boundary reconciles employee health, fatigue and availability")
_expect(_runtime.facility_service.get_instance(&"objective.auto").durability == raid[&"assets"][&"objective.auto"][&"integrity"], "automatic terminal boundary reconciles facility durability")
var terminal_rng: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
var terminal_events := _count_domain_events(&"DEFENSE_RAID_RESOLVED", &"raid.auto")
_advance(10)
_expect(_runtime.get_rng_stream(&"defense").snapshot() == terminal_rng and _count_domain_events(&"DEFENSE_RAID_RESOLVED", &"raid.auto") == terminal_events, "terminal raids stop consuming combat RNG and emit no repeated resolution")
func _test_multiple_raids_stable_order() -> void:
_reset_runtime(91_002)
# Deliberately register in reverse order; tick order must use stable raid IDs.
for suffix: String in ["z", "a"]:
var guard := StringName("guard.order." + suffix)
var objective := StringName("objective.order." + suffix)
var raid_id := StringName("raid.order." + suffix)
_create_staff_and_facility(guard, objective)
_prepare_started_raid(raid_id, guard, objective, [_wave(StringName("wave.order." + suffix), 1_000, 1, [&"gate.north"])])
var expected_rng := DeterministicRng.from_root_seed(0, &"defense") as DeterministicRng
expected_rng.restore(_runtime.get_rng_stream(&"defense").snapshot())
var expected_rolls: Array[int] = []
for _draw: int in 4:
expected_rolls.append(expected_rng.randi_range(0, 100))
_advance(5)
var order: Array[StringName] = []
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if event.event_type == &"DEFENSE_ROUND_RESOLVED":
order.append(StringName(event.payload[&"raid_id"]))
_expect(order == [&"raid.order.a", &"raid.order.z"], "simultaneous raids resolve in deterministic ID order regardless of creation order")
var roll_cursor := 0
for raid_id: StringName in [&"raid.order.a", &"raid.order.z"]:
var raid: Dictionary = _runtime.defense_service.get_raid(raid_id)
var payload: Dictionary = (raid[&"resolved_rounds"] as Dictionary).values()[0]
_expect(payload[&"defense_roll"] == expected_rolls[roll_cursor] and payload[&"attack_roll"] == expected_rolls[roll_cursor + 1], "each simultaneous raid receives the exact stable-order RNG pair")
_expect(String(payload[&"resolution_id"]).begins_with("defense_round_resolution."), "automatic resolution uses the existing saved ID namespace")
roll_cursor += 2
_expect(_runtime.get_rng_stream(&"defense").snapshot() == expected_rng.snapshot(), "multi-raid tick consumes exactly two combat draws per round")
func _test_automatic_failure_pause_retry_and_atomicity() -> void:
_reset_runtime(91_003)
_create_staff_and_facility(&"guard.failure", &"objective.failure", 100, 0, 9_800)
_prepare_started_raid(&"raid.failure", &"guard.failure", &"objective.failure", [_wave(&"wave.failure", 500, 1, [&"gate.north"])])
_expect(bool(_runtime.inventory_service.add_stack(&"auto.inventory", &"mana_bomb", 5, 100, -1, _clock.absolute_minute, 1, &"auto.stack").get(&"ok", false)), "disclosed skill-cost inventory fixture exists")
_expect(_submit(&"queue_defense_skill", {
&"raid_id": &"raid.failure", &"actor_id": &"guard.failure", &"kind": &"POWER_STRIKE",
&"options": {&"potency": 10, &"metadata": {&"cost": {&"gold": 125, &"mana": 9, &"items": [{&"inventory_id": &"auto.inventory", &"item_id": &"mana_bomb", &"quantity": 2}]}}},
}).is_success(), "disclosed active skill queues before automatic resolution")
var before: Dictionary = _runtime.defense_service.create_snapshot()
var rng_before: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
var id_before := int(_runtime.id_generator.get_counter(&"defense_round_resolution"))
var failure := {&"armed": true, &"calls": 0}
var normal_outcome := Callable(_runtime, "_apply_defense_outcome_boundary")
var injected := func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
var report: Dictionary = normal_outcome.call(payload, phase, token)
if phase == &"apply":
failure[&"calls"] += 1
if bool(failure[&"armed"]) and bool(report.get(&"ok", false)):
failure[&"armed"] = false
report[&"ok"] = false
report[&"error"] = "Disclosed automatic terminal post-apply failure."
return report
_runtime.defense_service.configure_callbacks(Callable(_runtime, "_roll_defense_round"), Callable(_runtime, "_apply_defense_skill_boundary"), injected, Callable(_runtime, "_apply_defense_rng_boundary"))
_clock.set_paused(false)
_clock.advance_minutes(20)
var failed_minute := int(_clock.absolute_minute)
before[&"current_minute"] = failed_minute
_expect(_clock.paused and failed_minute == 365 and failure[&"calls"] == 1, "automatic failure pauses the long catch-up batch after one due attempt")
_expect(_runtime.defense_service.create_snapshot() == before and _runtime.get_rng_stream(&"defense").snapshot() == rng_before, "failed automatic transaction retains round, queued skill and RNG cursor")
_expect(_state.get_resource(&"gold") == 5_000 and _state.get_resource(&"mana") == 100 and _runtime.inventory_service.available_quantity(&"auto.inventory", &"mana_bomb", failed_minute) == 5, "failed automatic terminal outcome rolls all skill costs back")
_expect(_runtime.staff_service.get_staff(&"guard.failure").health == 100 and _runtime.staff_service.get_staff(&"guard.failure").fatigue == 0 and _runtime.facility_service.get_instance(&"objective.failure").durability == 9_800, "failed automatic terminal outcome rolls staff and facility changes back")
_expect(_runtime.id_generator.get_counter(&"defense_round_resolution") == id_before and _count_domain_events(&"DEFENSE_AUTOMATIC_ROUND_FAILED", &"raid.failure") == 1, "failed generated round ID rolls back and one diagnostic remains")
_runtime._advance_defense(failed_minute)
_expect(failure[&"calls"] == 1 and _clock.process_real_delta(5.0) == 0, "same-minute deferred reentry and paused real frames never retry the failure")
_runtime._configure_defense_boundaries()
_clock.set_paused(false)
_clock.advance_minutes(1)
_clock.set_paused(true)
var raid: Dictionary = _runtime.defense_service.get_raid(&"raid.failure")
_expect(raid[&"state"] == DefenseService.RaidState.DEFENDED and raid[&"current_round"] == 1 and raid[&"elapsed_minutes"] == 5 and raid[&"state_changed_at"] == failed_minute + 1, "resuming retries once at the current minute without fabricating offline combat")
_expect(_state.get_resource(&"gold") == 4_875 and _state.get_resource(&"mana") == 91 and _runtime.inventory_service.available_quantity(&"auto.inventory", &"mana_bomb", _clock.absolute_minute) == 3, "automatic retry commits skill costs exactly once")
var expected_rng := DeterministicRng.from_root_seed(0, &"defense") as DeterministicRng
expected_rng.restore(rng_before)
var expected_defense := expected_rng.randi_range(0, 100)
var expected_attack := expected_rng.randi_range(0, 100)
var round_payload: Dictionary = (raid[&"resolved_rounds"] as Dictionary).values()[0]
_expect(round_payload[&"defense_roll"] == expected_defense and round_payload[&"attack_roll"] == expected_attack and _runtime.get_rng_stream(&"defense").snapshot() == expected_rng.snapshot(), "retry consumes exactly the combat pair restored after failure")
func _test_manual_failure_is_not_hidden_by_same_tick_auto() -> void:
_reset_runtime(91_004)
_create_staff_and_facility(&"guard.manual", &"objective.manual")
_prepare_started_raid(&"raid.manual", &"guard.manual", &"objective.manual", [_wave(&"wave.manual", 500, 1, [&"gate.north"])])
var failure := {&"armed": true, &"calls": 0}
var normal_outcome := Callable(_runtime, "_apply_defense_outcome_boundary")
var injected := func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
var report: Dictionary = normal_outcome.call(payload, phase, token)
if phase == &"apply":
failure[&"calls"] += 1
if bool(failure[&"armed"]):
failure[&"armed"] = false
report[&"ok"] = false
report[&"error"] = "Disclosed one-shot manual failure."
return report
_runtime.defense_service.configure_callbacks(Callable(_runtime, "_roll_defense_round"), Callable(_runtime, "_apply_defense_skill_boundary"), injected, Callable(_runtime, "_apply_defense_rng_boundary"))
var attempt := _submit_round_at_due({&"raid_id": &"raid.manual"})
var failed: CommandResult = attempt[&"result"]
_expect(not failed.is_success() and failure[&"calls"] == 1 and attempt[&"before"] == attempt[&"after"], "manual due failure remains a failed command with all domain boundaries rolled back")
_expect(_runtime.defense_service.get_raid(&"raid.manual")[&"current_round"] == 0 and _count_domain_events(&"DEFENSE_ROUND_RESOLVED", &"raid.manual") == 0, "automatic phase never hides that failure by immediately retrying the same raid")
# A successful restore at the very same clock minute must discard the old
# execution guard, including when this Runtime already attempted that minute.
var retryable: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(retryable), "failed but atomic manual round remains a valid save")
_runtime._advance_defense(_clock.absolute_minute)
_expect(_runtime.defense_service.get_raid(&"raid.manual")[&"current_round"] == 1, "successful same-minute restore clears ephemeral attempt state")
func _test_restart_mid_interval_and_speed_equivalence() -> void:
_reset_runtime(91_005)
_create_staff_and_facility(&"guard.restart", &"objective.restart")
_prepare_started_raid(&"raid.restart", &"guard.restart", &"objective.restart", [_wave(&"wave.restart", 10_000, 100, [&"gate.north"])])
_advance(2)
_expect(_submit(&"queue_defense_skill", {&"raid_id": &"raid.restart", &"actor_id": &"guard.restart", &"kind": &"FORTIFY", &"options": {&"potency": 8, &"target_id": &"objective.restart"}}).is_success(), "mid-interval queued skill is an explicit disclosed fixture")
var origin: Dictionary = _runtime.create_snapshot()
_advance(8)
var expected: Dictionary = _runtime.create_snapshot()
_reset_runtime(91_005)
_expect(_runtime.restore_snapshot(origin), "independently initialized Runtime restores the mid-interval save")
_advance(8)
_expect(_runtime.create_snapshot() == expected, "restart reproduces full combat, RNG, queued skill, ID and event state without extra serialized timers")
_expect(_runtime.restore_snapshot(origin), "restore speed comparison origin")
_clock.process_real_delta(0.0)
_clock.set_speed(1)
_clock.set_paused(false)
var x1_ticks := 0
for frame: int in 9:
x1_ticks += _clock.process_real_delta(SimulationKernel.REAL_SECONDS_PER_GAME_MINUTE * 1.01)
var x1: Dictionary = _capture_defense_views()
_expect(_runtime.restore_snapshot(origin), "restore identical x3 origin")
_clock.process_real_delta(0.0)
_clock.set_speed(3)
_clock.set_paused(false)
var x3_ticks := 0
for frame: int in 3:
x3_ticks += _clock.process_real_delta(SimulationKernel.REAL_SECONDS_PER_GAME_MINUTE * 1.01)
_expect(x1_ticks == 9 and x3_ticks == 9 and _capture_defense_views() == x1, "x1 and x3 real-frame execution agree after the same nine game minutes")
_clock.set_paused(true)
func _test_history_replay_does_not_skip_due_automatic_round() -> void:
_reset_runtime(91_007)
_create_staff_and_facility(&"guard.replay.auto", &"objective.replay.auto")
_prepare_started_raid(&"raid.replay.auto", &"guard.replay.auto", &"objective.replay.auto", [_wave(&"wave.replay.auto", 10_000, 100, [&"gate.north"])])
var first_attempt := _submit_round_at_due({&"raid_id": &"raid.replay.auto", &"resolution_id": &"round.replay.first"})
var first: CommandResult = first_attempt[&"result"]
_expect(first.is_success() and not first.is_replay, "a genuine first round establishes canonical history")
var rng_before: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
# A new command key deliberately reads that old result at the second round's
# actual deadline; its command is processed before the automatic phase.
var replay_attempt := _submit_round_at_due({&"raid_id": &"raid.replay.auto", &"resolution_id": &"round.replay.first"}, &"autonomy.replay.new.command.key")
var replay: CommandResult = replay_attempt[&"result"]
_expect(replay.is_success() and replay.is_replay and replay.command_id > first.command_id and replay.details == first.details, "new command key returns the unchanged canonical historical result")
_expect(replay_attempt[&"before"] == replay_attempt[&"after"], "history replay alone consumes no RNG, costs or combat state")
var raid: Dictionary = _runtime.defense_service.get_raid(&"raid.replay.auto")
_expect(raid[&"current_round"] == 2 and _count_domain_events(&"DEFENSE_ROUND_RESOLVED", &"raid.replay.auto") == 2, "historical replay never skips the legitimate automatic round due in the same tick")
var expected_rng := DeterministicRng.from_root_seed(0, &"defense") as DeterministicRng
expected_rng.restore(rng_before)
var expected_defense := expected_rng.randi_range(0, 100)
var expected_attack := expected_rng.randi_range(0, 100)
var latest: Dictionary = {}
for payload: Dictionary in (raid[&"resolved_rounds"] as Dictionary).values():
if payload[&"round_number"] == 2:
latest = payload
_expect(not latest.is_empty() and latest[&"defense_roll"] == expected_defense and latest[&"attack_roll"] == expected_attack and _runtime.get_rng_stream(&"defense").snapshot() == expected_rng.snapshot(), "replay plus due automatic work consumes exactly one new round RNG pair")
func _test_history_replay_preserves_prior_manual_failure_guard() -> void:
_reset_runtime(91_008)
_create_staff_and_facility(&"guard.replay.failure", &"objective.replay.failure")
_prepare_started_raid(&"raid.replay.failure", &"guard.replay.failure", &"objective.replay.failure", [_wave(&"wave.replay.failure", 10_000, 2, [&"gate.north"])])
var first_attempt := _submit_round_at_due({&"raid_id": &"raid.replay.failure", &"resolution_id": &"round.replay.failure.first"})
_expect((first_attempt[&"result"] as CommandResult).is_success(), "first round precedes the disclosed next-round failure")
var rng_before: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
var failure := {&"armed": true, &"calls": 0}
var normal_outcome := Callable(_runtime, "_apply_defense_outcome_boundary")
var injected := func(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
var report: Dictionary = normal_outcome.call(payload, phase, token)
if phase == &"apply":
failure[&"calls"] += 1
if bool(failure[&"armed"]):
failure[&"armed"] = false
report[&"ok"] = false
report[&"error"] = "Disclosed failure before historical replay."
return report
_runtime.defense_service.configure_callbacks(Callable(_runtime, "_roll_defense_round"), Callable(_runtime, "_apply_defense_skill_boundary"), injected, Callable(_runtime, "_apply_defense_rng_boundary"))
var due := int(_clock.absolute_minute) + int(_runtime.defense_service.get_raid(&"raid.replay.failure")[&"round_minutes"])
# Stable command-ID order executes this real failing transaction before the
# helper's subsequent historical replay, at the very same canonical minute.
var accepted: CommandResult = _runtime.submit_command(&"resolve_defense_round", {&"raid_id": &"raid.replay.failure", &"resolution_id": &"round.replay.failure.next"}, &"defense_controller", &"raid.replay.failure", 80, due, &"autonomy.real.failure.first")
var replay_attempt := _submit_round_at_due({&"raid_id": &"raid.replay.failure", &"resolution_id": &"round.replay.failure.first"}, &"autonomy.old.replay.after.failure")
var failed: CommandResult = _runtime.command_queue.get_processed_result(accepted.command_id)
var replay: CommandResult = replay_attempt[&"result"]
_expect(accepted.is_success() and failed != null and not failed.is_success() and replay.is_success() and replay.is_replay and replay.command_id > accepted.command_id, "real failure and following old replay preserve their distinct actual command results")
_expect(failure[&"calls"] == 1 and _runtime.defense_service.get_raid(&"raid.replay.failure")[&"current_round"] == 1 and _runtime.get_rng_stream(&"defense").snapshot() == rng_before, "old replay cannot erase a prior failed real-round guard or hide it with same-tick automatic success")
_runtime._configure_defense_boundaries()
_advance(1)
_expect(_runtime.defense_service.get_raid(&"raid.replay.failure")[&"current_round"] == 2, "the genuine failed round can progress automatically on a later game minute")
func _test_resolution_history_is_scoped_per_raid() -> void:
_reset_runtime(91_009)
var accepted: Array[CommandResult] = []
for suffix: String in ["a", "b"]:
var guard := StringName("guard.shared.id." + suffix)
var objective := StringName("objective.shared.id." + suffix)
var raid_id := StringName("raid.shared.id." + suffix)
_create_staff_and_facility(guard, objective)
_prepare_started_raid(raid_id, guard, objective, [_wave(StringName("wave.shared.id." + suffix), 10_000, 100, [&"gate.north"])])
accepted.append(_runtime.submit_command(&"resolve_defense_round", {&"raid_id": raid_id, &"resolution_id": &"round.shared.history.id"}, &"defense_controller", raid_id, 80, int(_clock.absolute_minute) + 5, StringName("autonomy.shared.history." + suffix)))
_advance(5)
for pending: CommandResult in accepted:
var completed: CommandResult = _runtime.command_queue.get_processed_result(pending.command_id)
_expect(pending.is_success() and completed != null and completed.is_success() and not completed.is_replay and completed.details[&"resolution_id"] == &"round.shared.history.id", "same explicit resolution ID belongs independently to each raid in the same minute")
_expect(_runtime.defense_service.get_raid(&"raid.shared.id.a")[&"current_round"] == 1 and _runtime.defense_service.get_raid(&"raid.shared.id.b")[&"current_round"] == 1, "per-raid manual guards prevent double resolution without suppressing the other raid")
func _test_generated_ids_skip_existing_saved_history() -> void:
_reset_runtime(91_010)
_create_staff_and_facility(&"guard.id.collision", &"objective.id.collision")
_prepare_started_raid(&"raid.id.collision", &"guard.id.collision", &"objective.id.collision", [_wave(&"wave.id.collision", 10_000, 100, [&"gate.north"])])
for number: int in [1, 2]:
var explicit_id := StringName("defense_round_resolution.%012d" % number)
var attempt := _submit_round_at_due({&"raid_id": &"raid.id.collision", &"resolution_id": explicit_id})
_expect((attempt[&"result"] as CommandResult).is_success(), "valid explicit resolution establishes saved namespace collision")
_expect(_runtime.id_generator.get_counter(&"defense_round_resolution") == 0, "existing explicit IDs legitimately leave the generated counter unchanged")
var origin: Dictionary = _runtime.create_snapshot()
_reset_runtime(91_010)
_expect(_runtime.restore_snapshot(origin), "valid mid-battle history with explicit namespace IDs remains loadable")
var rng_before: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
var due := int(_clock.absolute_minute) + 5
_advance(5)
var raid: Dictionary = _runtime.defense_service.get_raid(&"raid.id.collision")
_expect(raid[&"current_round"] == 3 and raid[&"state_changed_at"] == due, "namespace collision never delays the attack beyond its saved deadline")
_expect(_runtime.defense_service.has_resolution(&"raid.id.collision", &"defense_round_resolution.000000000003") and _runtime.id_generator.get_counter(&"defense_round_resolution") == 3, "generated resolution skips both existing IDs and commits one unused ID")
var expected_rng := DeterministicRng.from_root_seed(0, &"defense") as DeterministicRng
expected_rng.restore(rng_before)
var expected_defense := expected_rng.randi_range(0, 100)
var expected_attack := expected_rng.randi_range(0, 100)
var resolved: Dictionary = raid[&"resolved_rounds"][&"defense_round_resolution.000000000003"]
_expect(resolved[&"defense_roll"] == expected_defense and resolved[&"attack_roll"] == expected_attack and _runtime.get_rng_stream(&"defense").snapshot() == expected_rng.snapshot(), "skipping history IDs consumes exactly one attack's combat RNG pair")
_expect(raid[&"resolved_rounds"][&"defense_round_resolution.000000000001"][&"round_number"] == 1 and raid[&"resolved_rounds"][&"defense_round_resolution.000000000002"][&"round_number"] == 2, "skipping IDs preserves both canonical old results")
func _test_exhausted_generated_ids_fail_atomically() -> void:
_reset_runtime(91_011)
_create_staff_and_facility(&"guard.id.exhausted", &"objective.id.exhausted")
_prepare_started_raid(&"raid.id.exhausted", &"guard.id.exhausted", &"objective.id.exhausted", [_wave(&"wave.id.exhausted", 10_000, 100, [&"gate.north"])])
var last_id := StringName("defense_round_resolution.%012d" % StableIdGenerator.MAX_COUNTER)
var attempt := _submit_round_at_due({&"raid_id": &"raid.id.exhausted", &"resolution_id": last_id})
_expect((attempt[&"result"] as CommandResult).is_success(), "valid explicit history can occupy the last generated ID")
var allocator: Dictionary = _runtime.id_generator.create_snapshot()
allocator["counters"]["defense_round_resolution"] = StableIdGenerator.MAX_COUNTER - 1
_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") == 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")
func _test_auto_breach_projects_facility_suspension() -> void:
_reset_runtime(91_006)
_create_staff_and_facility(&"guard.breach.auto", &"objective.breach.auto", 100, 0, 1)
_prepare_started_raid(&"raid.breach.auto", &"guard.breach.auto", &"objective.breach.auto", [_wave(&"wave.breach.auto", 1_000, 1, [&"gate.north"])], 0, 0, {&"target_order": 1}, 1_000)
_advance(5)
var facility: FacilityInstance = _runtime.facility_service.get_instance(&"objective.breach.auto")
_expect(_runtime.defense_service.get_raid(&"raid.breach.auto")[&"state"] == DefenseService.RaidState.BREACHED and facility.durability == 0 and facility.state == FacilityInstance.State.BROKEN, "ordinary automatic combat destroys the objective through the actual facility boundary")
_expect(facility.suspension_reason == &"DESTROYED", "automatic destruction keeps the user-specified suspension state without inventing recovery")
var terminal: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(terminal), "automatically breached terminal state round-trips through ordinary Runtime save restoration")