Record original-alignment implementation and traceability checkpoint

Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
This commit is contained in:
Takeshi
2026-09-30 23:52:24 +09:00
parent 6077e02ebe
commit 5dde0c5ec2
490 changed files with 54051 additions and 809 deletions
+46
View File
@@ -12,6 +12,7 @@ func _initialize() -> void:
func _run() -> void:
print("S5: deterministic wave-by-wave Raid defense")
_test_threat_summary()
_test_definition_and_registration_validation()
_test_scores_penalties_and_stable_order()
_test_layered_assets_and_breach()
@@ -22,6 +23,7 @@ func _run() -> void:
_test_rng_transaction_retry_stability()
_test_retreat_policies_and_manual_retreat_replay()
_test_cancel_transaction_and_reconciliation()
_test_cancel_incomplete_preparation()
_test_resolution_operation_namespace_and_legacy_v1()
_test_multiple_waves_require_explicit_transition()
_test_raid_duration_and_wave_round_limits()
@@ -39,6 +41,22 @@ func _run() -> void:
quit(_failures)
func _test_threat_summary() -> void:
var service := DefenseServiceType.new()
_expect(service.get_threat_summary()[&"total"] == 0, "empty defense has no HUD threat")
_must(service.create_raid(&"preparing", 0, _definition()), "HUD preparing raid created")
_prepare_basic(service, &"active")
var before := service.create_snapshot()
var summary := service.get_threat_summary()
_expect(summary == {&"preparing": 1, &"active": 1, &"between_waves": 0, &"total": 2}, "HUD counts preparing and active raids separately")
summary[&"total"] = 99
_expect(service.get_threat_summary()[&"total"] == 2 and service.create_snapshot() == before, "HUD projection is owned and read only")
_must(service.cancel_raid(&"preparing", 0), "HUD incomplete preparation can be cancelled")
_expect(service.get_threat_summary()[&"preparing"] == 0 and service.get_threat_summary()[&"total"] == 1, "cancelled history is not an ongoing threat")
var restored := DefenseServiceType.new()
_expect(restored.restore_snapshot(service.create_snapshot()) and restored.get_threat_summary() == service.get_threat_summary(), "HUD projection follows restored authoritative state")
func _test_definition_and_registration_validation() -> void:
var service := DefenseServiceType.new()
_expect(not service.create_raid(&"", 0, _definition()).is_success(), "Raid ID is required")
@@ -505,6 +523,7 @@ func _test_multiple_waves_require_explicit_transition() -> void:
_must(service.start_raid(&"multi", 0), "multi-wave Raid starts")
var wave_one = service.resolve_next_round(&"multi", &"wave_one_round", 5)
_expect(wave_one.is_success() and wave_one.details[&"state"] == &"BETWEEN_WAVES", "first max-round wave ends BETWEEN_WAVES")
_expect(service.get_threat_summary()[&"between_waves"] == 1 and service.get_threat_summary()[&"active"] == 0, "HUD distinguishes waiting for next wave from combat")
var between_snapshot := service.create_snapshot()
_expect(not service.resolve_next_round(&"multi", &"illegal_between_round", 6).is_success(), "round cannot resolve while waiting between waves")
_expect(service.create_snapshot() == between_snapshot, "failed between-wave round is atomic")
@@ -515,6 +534,7 @@ func _test_multiple_waves_require_explicit_transition() -> void:
_expect(not service.begin_next_wave(&"multi", 7).is_success(), "active wave cannot be skipped")
var wave_two = service.resolve_next_round(&"multi", &"wave_two_round", 12)
_expect(wave_two.is_success() and wave_two.details[&"state"] == &"DEFENDED", "last wave completion defends the Raid")
_expect(service.get_threat_summary()[&"total"] == 0, "defended raid is removed from ongoing HUD threats")
var raid := service.get_raid(&"multi")
_expect(raid[&"current_round"] == 2 and raid[&"elapsed_minutes"] == 10, "round and elapsed counters span all waves")
_expect(raid[&"waves"][0][&"state"] == &"RESOLVED" and raid[&"waves"][1][&"state"] == &"RESOLVED", "each completed wave retains its own state")
@@ -968,3 +988,29 @@ func _expect(condition: bool, message: String) -> bool:
_failures += 1
push_error(" CHECK FAILED: %s" % message)
return false
func _test_cancel_incomplete_preparation() -> void:
for partial: bool in [false, true]:
var service := DefenseServiceType.new()
_must(service.create_raid(&"unprepared", 0, _definition({&"objective_ids": [&"core", &"annex"]})), "create raid with incomplete objective registration")
if partial:
_must(service.register_asset(&"unprepared", &"core", &"OBJECTIVE", 1000, 0), "register only one of two objectives")
var before := service.create_snapshot()
_expect(not service.start_raid(&"unprepared", 0).is_success() and service.create_snapshot() == before, "incomplete preparation still cannot start combat")
var cancelled = service.cancel_raid(&"unprepared", 1, &"PLAYER_ABORT", &"cancel_unprepared")
_expect(cancelled.is_success(), "incomplete preparation can be cancelled")
if not cancelled.is_success():
continue
var raid := service.get_raid(&"unprepared")
_expect(raid[&"state"] == DefenseServiceType.RaidState.CANCELLED and raid[&"started_at"] == -1 and raid[&"current_round"] == 0, "preparation cancellation does not invent combat")
_expect(raid[&"assets"].size() == (1 if partial else 0), "cancellation does not invent unregistered assets")
var saved := service.create_snapshot()
var restored := DefenseServiceType.new()
_expect(restored.restore_snapshot(JSON.parse_string(JSON.stringify(saved))), "cancelled preparation survives JSON save transport")
var replay = restored.cancel_raid(&"unprepared", 99, &"DIFFERENT", &"cancel_unprepared")
_expect(replay.is_success() and replay.is_replay and restored.create_snapshot() == saved, "cancel replay after restore preserves state")
var corrupt := saved.duplicate(true)
corrupt[&"raids"][0][&"started_at"] = 0
_expect(not restored.restore_snapshot(corrupt) and restored.create_snapshot() == saved, "started cancelled raid still requires all objective assets and rejects corruption atomically")
_expect(restored.validate_invariants()[&"ok"], "cancelled incomplete preparation remains valid")
+347
View File
@@ -0,0 +1,347 @@
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()
_advance(5)
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(_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")
@@ -0,0 +1 @@
uid://dnoi1lvhxm72
+120 -44
View File
@@ -140,10 +140,10 @@ func _test_all_commands_multiwave_and_terminal_replay() -> void:
&"raid_id": &"raid.multi", &"resolution_id": &"round.multi.early",
})
_expect(not early.is_success(), "a round before its configured interval is rejected")
_advance(5)
var first_round := _submit(&"resolve_defense_round", {
var first_round_attempt := _submit_round_at_due({
&"raid_id": &"raid.multi", &"resolution_id": &"round.multi.1",
})
var first_round: CommandResult = first_round_attempt[&"result"]
var after_first: Dictionary = _runtime.defense_service.get_raid(&"raid.multi")
_expect(
first_round.is_success()
@@ -165,10 +165,10 @@ func _test_all_commands_multiwave_and_terminal_replay() -> void:
&"raid_id": &"raid.multi", &"resolution_id": &"round.multi.same-minute",
})
_expect(not same_minute.is_success(), "a different round ID cannot resolve again in the same minute")
_advance(5)
var terminal := _submit(&"resolve_defense_round", {
var terminal_attempt := _submit_round_at_due({
&"raid_id": &"raid.multi", &"resolution_id": &"round.multi.2",
})
var terminal: CommandResult = terminal_attempt[&"result"]
var terminal_raid: Dictionary = _runtime.defense_service.get_raid(&"raid.multi")
var linked_staff: StaffRuntime = _runtime.staff_service.get_staff(&"guard.multi")
var linked_facility: FacilityInstance = _runtime.facility_service.get_instance(&"objective.multi")
@@ -243,10 +243,10 @@ func _test_manual_retreat_cancel_and_long_horizon() -> void:
0,
{&"target_order": 1}
)
_advance(5)
var partial := _submit(&"resolve_defense_round", {
var partial_attempt := _submit_round_at_due({
&"raid_id": &"raid.cancel", &"resolution_id": &"round.cancel.partial",
})
var partial: CommandResult = partial_attempt[&"result"]
var partial_raid: Dictionary = _runtime.defense_service.get_raid(&"raid.cancel")
_expect(
partial.is_success()
@@ -289,23 +289,20 @@ func _test_manual_retreat_cancel_and_long_horizon() -> void:
var long_raid: Dictionary = _runtime.defense_service.get_raid(&"raid.long")
_expect(
_runtime.defense_service.current_minute == _clock.absolute_minute
and int(long_raid[&"current_round"]) == 0
and int(long_raid[&"state"]) == DefenseService.RaidState.ACTIVE,
"24-hour ticking advances Defense time without automatically resolving rounds"
and int(long_raid[&"current_round"]) == 1
and int(long_raid[&"state"]) == DefenseService.RaidState.DEFENDED
and int(long_raid[&"elapsed_minutes"]) == 5,
"ordinary ticking automatically reaches the existing max-duration boundary once"
)
_expect(
_runtime.get_rng_stream(&"defense").snapshot() == long_rng_before,
"Defense ticking alone consumes no round RNG"
_runtime.get_rng_stream(&"defense").snapshot() != long_rng_before
and int(long_raid[&"finished_at"]) == int(long_raid[&"started_at"]) + 5,
"long horizon consumes combat RNG only for the configured due round"
)
var duration_terminal := _submit(&"resolve_defense_round", {
&"raid_id": &"raid.long", &"resolution_id": &"round.long.duration",
})
var duration_payload: Dictionary = (long_raid[&"resolved_rounds"] as Dictionary).values()[0]
_expect(
duration_terminal.is_success()
and int(_runtime.defense_service.get_raid(&"raid.long")[&"state"])
== DefenseService.RaidState.DEFENDED
and duration_terminal.details.get(&"reason", &"") == &"RAID_DURATION_EXHAUSTED",
"the next explicit round applies max-duration terminal behavior"
duration_payload.get(&"reason", &"") == &"RAID_DURATION_EXHAUSTED",
"the automatic round applies the same max-duration outcome as the command API"
)
_create_staff_and_facility(&"guard.breach", &"objective.breach", 100, 0, 1)
@@ -317,10 +314,10 @@ func _test_manual_retreat_cancel_and_long_horizon() -> void:
{&"target_order": 1},
1_000
)
_advance(5)
var breached := _submit(&"resolve_defense_round", {
var breached_attempt := _submit_round_at_due({
&"raid_id": &"raid.breach", &"resolution_id": &"round.breach.terminal",
})
var breached: CommandResult = breached_attempt[&"result"]
_expect(
breached.is_success()
and int(_runtime.defense_service.get_raid(&"raid.breach")[&"state"])
@@ -427,7 +424,6 @@ func _test_rng_skill_and_outcome_atomicity() -> void:
and String(queued.details.get(&"action_id", "")).begins_with("defense_skill_action."),
"queued skill receives a stable generated action namespace"
)
_advance(5)
var defense_before: Dictionary = _runtime.defense_service.create_snapshot()
var defense_rng_before: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
var authority_rng_before: Dictionary = _runtime.get_rng_stream(&"authority").snapshot()
@@ -456,25 +452,36 @@ func _test_rng_skill_and_outcome_atomicity() -> void:
failing_outcome,
Callable(_runtime, "_apply_defense_rng_boundary")
)
var failed := _submit(&"resolve_defense_round", {
var failed_attempt := _submit_round_at_due({
&"raid_id": &"raid.atomic", &"resolution_id": &"round.atomic.retry",
})
var failed: CommandResult = failed_attempt[&"result"]
var atomic_before: Dictionary = failed_attempt[&"before"]
var atomic_after: Dictionary = failed_attempt[&"after"]
defense_before = atomic_before[&"defense"]
defense_rng_before = atomic_before[&"defense_rng"]
authority_rng_before = atomic_before[&"authority_rng"]
economy_before = atomic_before[&"economy"]
state_before = atomic_before[&"state"]
inventory_before = atomic_before[&"inventory"]
staff_before = atomic_before[&"staff"]
facilities_before = atomic_before[&"facilities"]
_expect(not failed.is_success(), "an injected post-apply terminal failure rejects the round")
_expect(
_runtime.defense_service.create_snapshot() == defense_before
and _runtime.get_rng_stream(&"defense").snapshot() == defense_rng_before
and _runtime.get_rng_stream(&"authority").snapshot() == authority_rng_before,
atomic_after[&"defense"] == defense_before
and atomic_after[&"defense_rng"] == defense_rng_before
and atomic_after[&"authority_rng"] == authority_rng_before,
"failed outcome restores Defense state and its RNG without touching other streams"
)
_expect(
_runtime.economy_service.create_snapshot() == economy_before
and _state.create_snapshot() == state_before
and _runtime.inventory_service.create_snapshot() == inventory_before,
atomic_after[&"economy"] == economy_before
and atomic_after[&"state"] == state_before
and atomic_after[&"inventory"] == inventory_before,
"failed outcome restores Gold, Mana, item reservations and consumption"
)
_expect(
_runtime.staff_service.create_snapshot() == staff_before
and _runtime.facility_service.create_snapshot() == facilities_before,
atomic_after[&"staff"] == staff_before
and atomic_after[&"facilities"] == facilities_before,
"failed outcome restores Staff and Facility changes atomically"
)
_expect(
@@ -531,25 +538,32 @@ func _test_rng_skill_and_outcome_atomicity() -> void:
]}}},
})
_expect(reservation_skill.is_success(), "multi-item skill cost queues before boundary validation")
_advance(5)
var reserve_defense_before: Dictionary = _runtime.defense_service.create_snapshot()
var reserve_rng_before: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
var reserve_inventory_before: Dictionary = _runtime.inventory_service.create_snapshot()
var reserve_economy_before: Dictionary = _runtime.economy_service.create_snapshot()
var reserve_state_before: Dictionary = _state.create_snapshot()
var reservation_failed := _submit(&"resolve_defense_round", {
var reservation_attempt := _submit_round_at_due({
&"raid_id": &"raid.reserve", &"resolution_id": &"round.reserve.failed",
})
var reservation_failed: CommandResult = reservation_attempt[&"result"]
var reserve_before: Dictionary = reservation_attempt[&"before"]
var reserve_after: Dictionary = reservation_attempt[&"after"]
reserve_defense_before = reserve_before[&"defense"]
reserve_rng_before = reserve_before[&"defense_rng"]
reserve_inventory_before = reserve_before[&"inventory"]
reserve_economy_before = reserve_before[&"economy"]
reserve_state_before = reserve_before[&"state"]
_expect(not reservation_failed.is_success(), "a later unavailable item rejects the whole skill-cost batch")
_expect(
_runtime.inventory_service.create_snapshot() == reserve_inventory_before
and _runtime.economy_service.create_snapshot() == reserve_economy_before
and _state.create_snapshot() == reserve_state_before,
reserve_after[&"inventory"] == reserve_inventory_before
and reserve_after[&"economy"] == reserve_economy_before
and reserve_after[&"state"] == reserve_state_before,
"partial item reservation failure rolls every external resource back"
)
_expect(
_runtime.defense_service.create_snapshot() == reserve_defense_before
and _runtime.get_rng_stream(&"defense").snapshot() == reserve_rng_before,
reserve_after[&"defense"] == reserve_defense_before
and reserve_after[&"defense_rng"] == reserve_rng_before,
"skill validation failure leaves Defense and round RNG untouched"
)
@@ -676,10 +690,10 @@ func _test_snapshot_replay_corruption_and_legacy_migration() -> void:
0,
{&"target_order": 1}
)
_advance(5)
var first := _submit(&"resolve_defense_round", {
var first_attempt := _submit_round_at_due({
&"raid_id": &"raid.save", &"resolution_id": &"round.save.1",
})
var first: CommandResult = first_attempt[&"result"]
_expect(
first.is_success()
and int(_runtime.defense_service.get_raid(&"raid.save")[&"state"])
@@ -699,17 +713,17 @@ func _test_snapshot_replay_corruption_and_legacy_migration() -> void:
})
_expect(queued.is_success(), "queued next-round skill is included in the mid-Raid snapshot")
var origin: Dictionary = _runtime.create_snapshot()
_advance(5)
var first_branch := _submit_with_key(&"resolve_defense_round", {
var first_branch_attempt := _submit_round_at_due({
&"raid_id": &"raid.save", &"resolution_id": &"round.save.2",
}, &"runtime.defense.save.resolve")
var first_branch: CommandResult = first_branch_attempt[&"result"]
var branch_snapshot: Dictionary = _runtime.create_snapshot()
_expect(first_branch.is_success(), "first post-save Defense branch resolves")
_expect(_runtime.restore_snapshot(origin), "mid-round Runtime snapshot restores atomically")
_advance(5)
var replay_branch := _submit_with_key(&"resolve_defense_round", {
var replay_branch_attempt := _submit_round_at_due({
&"raid_id": &"raid.save", &"resolution_id": &"round.save.2",
}, &"runtime.defense.save.resolve")
var replay_branch: CommandResult = replay_branch_attempt[&"result"]
_expect(
replay_branch.is_success() and replay_branch.details == first_branch.details,
"restored mid-round branch reproduces the exact Defense resolution"
@@ -796,6 +810,7 @@ func _test_snapshot_replay_corruption_and_legacy_migration() -> void:
legacy[&"systems"][&"runtime"].erase(&"kitchen_production")
legacy[&"systems"][&"runtime"].erase(&"elevators")
legacy[&"systems"][&"runtime"].erase(&"legacy_stairs")
preload("res://tests/fixtures/pre_upkeep_runtime.gd").strip_empty_upkeep(legacy[&"systems"][&"runtime"])
legacy[&"systems"][&"runtime"][&"path"][&"schema_version"] = 3
legacy[&"systems"][&"runtime"][&"path"].erase(&"vertical_walk_blockers")
legacy[&"systems"][&"runtime"][&"path"].erase(&"vertical_walk_requires_connector")
@@ -1029,3 +1044,64 @@ func _expect(condition: bool, label: String) -> void:
return
_failures += 1
push_error(" ✗ %s" % label)
## Explicit API coverage schedules the command at its actual deadline. The
## normal kernel executes APPLY_COMMANDS before automatic defense progression.
## Captures bracket that actual handler, excluding later unrelated tick phases.
func _submit_round_at_due(
payload: Dictionary,
idempotency_key: StringName = &""
) -> Dictionary:
var raid_id := StringName(payload[&"raid_id"])
var raid: Dictionary = _runtime.defense_service.get_raid(raid_id)
var due := int(raid[&"state_changed_at"]) + int(raid[&"round_minutes"])
_expect(due > _clock.absolute_minute, "manual deadline helper receives a future configured deadline")
if due <= _clock.absolute_minute:
return {&"result": _submit(&"resolve_defense_round", payload), &"before": {}, &"after": {}}
if idempotency_key.is_empty():
_command_sequence += 1
idempotency_key = StringName("runtime.defense.%04d" % _command_sequence)
var captures := {&"before": {}, &"after": {}, &"signals": []}
var executors: Dictionary = _runtime.get("_executors")
var original: Callable = executors[&"resolve_defense_round"]
executors[&"resolve_defense_round"] = func(command: SimulationCommand) -> CommandResult:
captures[&"before"] = _capture_defense_views()
var result: CommandResult = original.call(command)
captures[&"after"] = _capture_defense_views()
return result
var accepted: CommandResult = _runtime.submit_command(
&"resolve_defense_round", payload, &"defense_controller", raid_id,
80, due, idempotency_key
)
var capture_completed := func(result: CommandResult) -> void:
if result.command_id == accepted.command_id:
(captures[&"signals"] as Array).append(result.copy())
_runtime.command_completed.connect(capture_completed)
_clock.advance_minutes(due - _clock.absolute_minute)
_clock.set_paused(true)
_runtime.command_completed.disconnect(capture_completed)
executors[&"resolve_defense_round"] = original
var completed: CommandResult = _runtime.command_queue.get_processed_result(accepted.command_id)
_expect(accepted.is_success() and completed != null, "manual defense command is genuinely processed at its scheduled tick")
_expect(
(captures[&"signals"] as Array).size() == 1
and completed != null
and (captures[&"signals"][0] as CommandResult).to_dictionary() == completed.to_dictionary(),
"command_completed emits the actual persisted result, including failures"
)
captures[&"result"] = completed
return captures
func _capture_defense_views() -> Dictionary:
return {
&"defense": _runtime.defense_service.create_snapshot(),
&"defense_rng": _runtime.get_rng_stream(&"defense").snapshot(),
&"authority_rng": _runtime.get_rng_stream(&"authority").snapshot(),
&"economy": _runtime.economy_service.create_snapshot(),
&"state": _state.create_snapshot(),
&"inventory": _runtime.inventory_service.create_snapshot(),
&"staff": _runtime.staff_service.create_snapshot(),
&"facilities": _runtime.facility_service.create_snapshot(),
}
+321
View File
@@ -0,0 +1,321 @@
extends "res://tests/s5/test_runtime_defense_cycle.gd"
## Acceptance tests for the configured Event -> Defense bridge. Source causes, facilities and every
## combat value below are disclosed fixtures. This does not prove normal thief,
## male bankruptcy or bank delinquency generation or an approved balance.
func _run() -> void:
print("Configured Event -> Defense: identity, rollback and save")
_test_failure_policy_and_expiry()
_test_event_rollback_preserves_failed_round_guard()
_test_serialized_action_enum_rejection()
_test_source_families_from_ordinary_event_phase()
_test_replay_conflict_and_manual_provenance_rejection()
_test_rollback_of_later_failed_action()
_test_choice_replay_and_real_cost_commit_failure()
_test_save_branch_provenance_corruption_and_legacy()
_cleanup()
if _failures == 0: print("PASS: %d checks" % _checks)
else: push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_source_families_from_ordinary_event_phase() -> void:
_reset_runtime(92_001)
for kind: String in ["THIEF", "BANKRUPTCY_RIOT", "BANK_DELINQUENCY", "AUTHORITY_ESCALATION"]:
var objective := StringName("objective.event." + kind)
_create_staff_and_facility(StringName("guard.event." + kind), objective)
var action := _event_raid_action(objective, kind)
var instance := StringName("event.instance.fixture." + kind)
_expect(_register_threat_event(StringName("event.fixture." + kind), [action]).is_success(), "explicit configured event definition registers for " + kind)
_expect(_enqueue_threat_event(StringName("event.fixture." + kind), instance, StringName("cause.fixture." + kind)).is_success(), "disclosed cause enters the actual Event queue for " + kind)
var before := int(_runtime.defense_service.get_raid_ids().size())
var commands_before := int(_runtime.command_queue.processed_count())
_advance(1)
var raid_id := _event_raid_id(instance, action.action_id)
var raid: Dictionary = _runtime.defense_service.get_raid(raid_id)
_expect(_runtime.defense_service.get_raid_ids().size() == before + 1 and raid[&"state"] == DefenseService.RaidState.PREPARING, "ordinary event phase creates an existing preparing threat for " + kind)
_expect(raid[&"created_at"] == _clock.absolute_minute and raid[&"source_id"] == StringName("cause.fixture." + kind) and raid[&"event_source"]["source_kind"] == kind, "creation time and source families are preserved independently")
_expect(_runtime.event_service.get_instance(instance).state == EventInstance.State.COMPLETED and _count_domain_events(&"DEFENSE_RAID_CREATED", raid_id) == 1, "automatic Event action completes once and emits one real incident event")
_expect(_runtime.command_queue.processed_count() == commands_before, "ordinary event sink creates no synthetic player command")
# Incomplete templates fail closed rather than using Defense defaults.
var incomplete := _event_raid_action(&"objective.event.THIEF", "THIEF")
(incomplete.payload[&"definition"] as Dictionary).erase(&"round_minutes")
_expect(_register_threat_event(&"event.fixture.incomplete", [incomplete]).is_success(), "schema accepts an action whose runtime configuration must still be checked")
_expect(_enqueue_threat_event(&"event.fixture.incomplete", &"event.instance.incomplete", &"cause.incomplete").is_success(), "incomplete disclosed template enters the event queue")
var raids_before: Dictionary = _runtime.defense_service.create_snapshot()
_advance(1)
raids_before[&"current_minute"] = _clock.absolute_minute
_expect(_runtime.defense_service.create_snapshot() == raids_before and _runtime.event_service.get_instance(&"event.instance.incomplete").state == EventInstance.State.CANCELLED, "missing interval creates no threat and preserves every existing raid")
func _test_replay_conflict_and_manual_provenance_rejection() -> void:
_reset_runtime(92_002)
_create_staff_and_facility(&"guard.event.replay", &"objective.event.replay")
var action := _event_raid_action(&"objective.event.replay", "BANKRUPTCY_RIOT")
_expect(_register_threat_event(&"event.fixture.replay", [action]).is_success(), "replay fixture definition registers")
_expect(_enqueue_threat_event(&"event.fixture.replay", &"event.instance.replay", &"male.guest.cause.fixture").is_success(), "disclosed cause is queued")
_advance(1)
var raid_id := _event_raid_id(&"event.instance.replay", action.action_id)
_expect(_register_defender(raid_id, &"guard.event.replay", 50).is_success() and _register_objective(raid_id, &"objective.event.replay", 20).is_success(), "ordinary defense assignment connects real employee and facility")
_expect(_submit(&"start_defense_raid", {&"raid_id": raid_id}).is_success(), "ordinary start command begins the configured incident")
_advance(5)
var context := {&"event_instance_id": &"event.instance.replay", &"threat_source_id": &"male.guest.cause.fixture"}
var before: Dictionary = _runtime.create_snapshot()
var replay: Dictionary = _runtime._execute_event_action_boundary(action, &"apply", context, null)
_expect(replay.get(&"ok", false) and replay.get(&"is_replay", false) and _runtime.create_snapshot() == before, "event-action replay preserves active combat, staff locks, resources, RNG, clock and event log")
var conflicting := EventAction.from_variant(action.create_snapshot())
(conflicting.payload[&"definition"][&"waves"] as Array)[0][&"threat_strength"] += 1
var rejected: Dictionary = _runtime._execute_event_action_boundary(conflicting, &"apply", context, null)
_expect(not rejected.get(&"ok", true) and _runtime.create_snapshot() == before, "same cause/action with a conflicting template never replaces its existing battle")
var forged_definition := _threat_definition(&"objective.event.replay")
forged_definition[&"event_source"] = _runtime.defense_service.get_raid(raid_id)[&"event_source"]
var defense_before: Dictionary = _runtime.defense_service.create_snapshot()
_expect(not _submit(&"create_defense_raid", {&"raid_id": &"forged.manual.provenance", &"definition": forged_definition}).is_success() and _runtime.defense_service.create_snapshot() == defense_before, "manual raid API cannot forge event provenance")
func _test_rollback_of_later_failed_action() -> void:
_reset_runtime(92_003)
_create_staff_and_facility(&"guard.event.rollback", &"objective.event.rollback")
_expect(_runtime.staff_service.create_task(&"fixture.work", 20, _clock.absolute_minute, {}, &"task.event.duplicate").is_success(), "disclosed duplicate task is prepared for apply-time failure")
var threat := _event_raid_action(&"objective.event.rollback", "BANK_DELINQUENCY")
var duplicate := EventAction.new().configure(&"fail.after.threat", EventAction.Type.CREATE_TASK, {&"task_id": &"task.event.duplicate", &"task_type": &"fixture.work"})
_expect(_register_threat_event(&"event.fixture.rollback", [threat, duplicate]).is_success(), "rollback-all event includes threat followed by an apply-time failure")
_expect(_enqueue_threat_event(&"event.fixture.rollback", &"event.instance.rollback", &"loan.overdue.cause.fixture").is_success(), "disclosed overdue cause enters the queue without claiming a real loan")
var before: Dictionary = _runtime.defense_service.create_snapshot()
var rng_before: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
_advance(1)
before[&"current_minute"] = _clock.absolute_minute
var raid_id := _event_raid_id(&"event.instance.rollback", threat.action_id)
_expect(_runtime.defense_service.create_snapshot() == before and _runtime.get_rng_stream(&"defense").snapshot() == rng_before, "later action failure rolls the new incident and its provenance back")
_expect(_runtime.defense_service.get_raid(raid_id).is_empty() and _count_domain_events(&"DEFENSE_RAID_CREATED", raid_id) == 0, "rolled-back incident leaves no committed creation event")
_expect(_runtime.event_service.get_instance(&"event.instance.rollback").state == EventInstance.State.CANCELLED, "failed event follows the existing cancellation policy")
func _test_save_branch_provenance_corruption_and_legacy() -> void:
_reset_runtime(92_004)
_create_staff_and_facility(&"guard.event.save", &"objective.event.save")
var action := _event_raid_action(&"objective.event.save", "THIEF")
_expect(_register_threat_event(&"event.fixture.save", [action]).is_success(), "save fixture definition registers")
_expect(_enqueue_threat_event(&"event.fixture.save", &"event.instance.save", &"thief.cause.fixture").is_success(), "save fixture source enters queue")
var queued: Dictionary = _runtime.create_snapshot()
_advance(1)
var expected: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(queued), "queued configured threat restores before execution")
_advance(1)
_expect(_runtime.create_snapshot() == expected, "save/restart branch produces the exact same event-derived raid ID, provenance, event and command state")
var raid_id := _event_raid_id(&"event.instance.save", action.action_id)
for kind: String in ["digest", "type", "event", "source", "interval", "wave_limit", "wave_strength"]:
var corrupt: Dictionary = expected.duplicate(true)
var raids: Array = corrupt[&"systems"][&"runtime"][&"defense"][&"raids"]
var raid: Dictionary = raids[0]
if kind == "digest": raid[&"event_source"]["template_digest"] = "a".repeat(64)
elif kind == "type": raid[&"event_source"] = []
elif kind == "event": raid[&"event_source"]["event_instance_id"] = "missing.event"
elif kind == "interval": raid[&"round_minutes"] = 7
elif kind == "wave_limit": (raid[&"waves"] as Array)[0][&"max_rounds"] += 1
elif kind == "wave_strength":
(raid[&"waves"] as Array)[0][&"threat_strength"] += 1
(raid[&"waves"] as Array)[0][&"remaining_threat"] += 1
else: raid[&"source_id"] = "unrelated.cause"
_expect(not _runtime.restore_snapshot(corrupt) and _runtime.create_snapshot() == expected, "corrupt event provenance is rejected atomically: " + kind)
_expect(_runtime.defense_service.get_raid(raid_id)[&"event_source"]["template_digest"].length() == 64, "valid event provenance remains after all rejected restores")
_expect(_submit(&"set_defense_retreat_policy", {&"raid_id": raid_id, &"policy": &"PROTECT_AND_RETREAT", &"threshold": 35}).is_success(), "normal player policy remains mutable after event creation")
var changed_policy: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(changed_policy) and _runtime.create_snapshot() == changed_policy, "immutable template validation preserves lawful policy changes")
_reset_runtime(92_005)
_create_staff_and_facility(&"guard.event.legacy", &"objective.event.legacy")
_expect(_submit(&"create_defense_raid", {&"raid_id": &"raid.legacy.manual", &"definition": _threat_definition(&"objective.event.legacy")}).is_success(), "real manual raid exists before legacy provenance migration")
var legacy: Dictionary = _runtime.create_snapshot()
legacy[&"systems"][&"runtime"][&"runtime_schema_version"] = 12
(legacy[&"systems"][&"runtime"][&"defense"][&"raids"] as Array)[0].erase(&"event_source")
var clean: Dictionary = legacy[&"systems"][&"runtime"][&"facility_cleanliness"].duplicate(true)
_expect(_runtime.restore_snapshot(legacy) and _runtime.facility_cleanliness.create_snapshot() == clean, "version-12 save keeps its required cleaning state when migrating to event schema 13")
_expect(not clean["facilities"].is_empty() and _runtime.defense_service.get_raid(&"raid.legacy.manual")[&"event_source"].is_empty(), "legacy migration preserves a nonempty cleanliness participant and old manual raid")
var migrated: Dictionary = _runtime.create_snapshot()
_expect(migrated[&"systems"][&"runtime"][&"runtime_schema_version"] == 13, "legacy12 re-emits current13 without losing required12 data")
var missing_clean: Dictionary = legacy.duplicate(true)
missing_clean[&"systems"][&"runtime"].erase(&"facility_cleanliness")
_expect(not _runtime.restore_snapshot(missing_clean) and _runtime.create_snapshot() == migrated, "legacy12 missing mandatory cleanliness is rejected atomically")
var malformed_clean: Dictionary = legacy.duplicate(true)
malformed_clean[&"systems"][&"runtime"][&"facility_cleanliness"]["schema_version"] = 99
_expect(not _runtime.restore_snapshot(malformed_clean) and _runtime.create_snapshot() == migrated, "legacy12 malformed mandatory cleanliness is rejected atomically")
var future: Dictionary = _runtime.create_snapshot()
var current: Dictionary = future.duplicate(true)
future[&"systems"][&"runtime"][&"runtime_schema_version"] = 14
_expect(not _runtime.restore_snapshot(future) and _runtime.create_snapshot() == current, "unknown future runtime schema remains rejected")
func _test_choice_replay_and_real_cost_commit_failure() -> void:
for commit_failure: bool in [false, true]:
_reset_runtime(92_006 if not commit_failure else 92_007)
_create_staff_and_facility(&"guard.event.choice", &"objective.event.choice")
var instance_id := &"event.instance.choice.fixture"
var threat := _event_raid_action(&"objective.event.choice", "THIEF")
var actions: Array[Dictionary] = [threat.create_snapshot()]
if commit_failure:
# This disclosed action spends the actual reserved escrow before the
# real commit. There is no mocked executor, cost callback or clock.
var consume := EventAction.new().configure(&"fixture.consume.escrow", EventAction.Type.ADD_LEDGER_ENTRY, {
&"from_account": StringName("EVENT_ESCROW:%s" % instance_id),
&"to_account": &"OPERATING_EXPENSE", &"amount": 7,
})
actions.append(consume.create_snapshot())
var registered: CommandResult = _runtime.submit_command(&"register_event_definition", {&"definition": {
&"event_id": &"event.fixture.choice", &"category": &"DEFENSE", &"foreground": false,
&"transaction_policy": &"ROLLBACK_ALL", &"choices": [{&"choice_id": &"fixture.choice",
&"text_key": &"fixture.choice.text", &"cost": {&"gold": 7}, &"actions": actions}],
}})
_expect(registered.is_success() and _enqueue_threat_event(&"event.fixture.choice", instance_id, &"choice.cause.fixture").is_success(), "disclosed choice threat registers and enters the actual event queue")
_advance(1)
var instance: EventInstance = _runtime.event_service.get_instance(instance_id)
_expect(instance.state == EventInstance.State.WAITING_CHOICE, "ordinary event phase waits for an actual choice before creating its raid")
var before: Dictionary = _runtime.defense_service.create_snapshot()
var cash_before: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
var expense_before: int = _runtime.economy_service.get_balance(&"OPERATING_EXPENSE")
var payload := {&"event_instance_id": instance_id, &"choice_instance_id": instance.choice_instance_id, &"choice_id": &"fixture.choice"}
var first := _submit(&"submit_event_choice", payload)
var raid_id := _event_raid_id(instance_id, threat.action_id)
if commit_failure:
_expect(not first.is_success() and _runtime.defense_service.create_snapshot() == before, "real cost commit failure rolls back the newly-created raid and provenance")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before and _runtime.economy_service.get_balance(&"OPERATING_EXPENSE") == expense_before, "failed commit rolls back its drain action and releases the actual escrow once")
_expect(_count_domain_events(&"DEFENSE_RAID_CREATED", raid_id) == 0 and _runtime.economy_service.get_balance(StringName("EVENT_ESCROW:%s" % instance_id)) == 0, "failed choice leaves no committed threat notification or stranded escrow")
else:
_expect(first.is_success() and _runtime.defense_service.get_raid(raid_id)[&"state"] == DefenseService.RaidState.PREPARING, "actual successful choice creates one preparing incident")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before - 7 and _runtime.economy_service.get_balance(&"OPERATING_EXPENSE") == expense_before + 7, "successful choice commits its disclosed fixture cost once")
var defense_after: Dictionary = _runtime.defense_service.create_snapshot()
var economy_after: Dictionary = _runtime.economy_service.create_snapshot()
var events_after: Dictionary = _runtime.event_service.create_snapshot()
# Successful choice commands legitimately resume the real clock. Pause
# through its public API to submit and observe the retry immediately.
_clock.set_paused(true)
var replay := _submit(&"submit_event_choice", payload)
_expect(replay.is_replay and replay.is_success() == first.is_success() and _runtime.defense_service.create_snapshot() == defense_after and _runtime.economy_service.create_snapshot() == economy_after and _runtime.event_service.create_snapshot() == events_after, "different command key replays the same real choice result without threat creation or payment retry")
_expect(_runtime.economy_service.validate_ledger()[&"ok"] and _runtime.event_service.validate_invariants()[&"ok"], "configured threat choice preserves actual Event and double-entry invariants")
func _threat_definition(objective: StringName) -> Dictionary:
return {&"entry_points": [&"fixture.event.entrance"], &"objective_ids": [objective],
&"round_minutes": 5, &"max_duration_minutes": 120,
&"retreat_policy": &"HOLD", &"defender_retreat_threshold": 0,
&"terrain_bonus": 0, &"command_bonus": 0, &"surprise_penalty": 0, &"multiple_entry_penalty": 0,
&"waves": [{&"threat_strength": 600, &"max_rounds": 3, &"retreat_threshold": 0}]}
func _event_raid_action(objective: StringName, kind: String) -> EventAction:
return EventAction.new().configure(&"configured.threat", EventAction.Type.CREATE_DEFENSE_RAID, {&"template_id": &"fixture.template.explicit", &"source_kind": kind, &"definition": _threat_definition(objective)})
func _register_threat_event(event_id: StringName, actions: Array) -> CommandResult:
var snapshots: Array[Dictionary] = []
for action: EventAction in actions: snapshots.append(action.create_snapshot())
return _runtime.submit_command(&"register_event_definition", {&"definition": {&"event_id": event_id, &"category": &"DEFENSE", &"foreground": false, &"transaction_policy": &"ROLLBACK_ALL", &"automatic_actions": snapshots}})
func _enqueue_threat_event(event_id: StringName, instance_id: StringName, source_id: StringName) -> CommandResult:
return _runtime.submit_command(&"enqueue_event", {&"event_id": event_id, &"actor_ids": [], &"options": {&"event_instance_id": instance_id, &"context": {&"threat_source_id": source_id}}})
func _event_raid_id(instance_id: StringName, action_id: StringName) -> StringName:
return StringName("event_defense_raid." + JSON.stringify([String(instance_id), String(action_id)], "", true, true).sha256_text())
func _test_serialized_action_enum_rejection() -> void:
_reset_runtime(92_008)
_create_staff_and_facility(&"guard.event.enum", &"objective.event.enum")
var action := _event_raid_action(&"objective.event.enum", "THIEF")
var bad_types: Array = [999, -1, "FUTURE_UNKNOWN_ACTION", 1.5, true, null]
for index: int in range(bad_types.size()):
var raw: Dictionary = action.create_snapshot()
raw[&"type"] = bad_types[index]
var event_id := StringName("event.enum.invalid.%d" % index)
var defense_before: Dictionary = _runtime.defense_service.create_snapshot()
var result: CommandResult = _runtime.submit_command(&"register_event_definition", {&"definition": {&"event_id": event_id, &"category": &"DEFENSE", &"automatic_actions": [raw]}})
_expect(not result.is_success() and _runtime.event_service.get_definition(event_id) == null and _runtime.defense_service.create_snapshot() == defense_before, "actual event registration rejects an unknown serialized action type without dropping or coercing it: %d" % index)
_expect(_register_threat_event(&"event.fixture.enum", [action]).is_success() and _enqueue_threat_event(&"event.fixture.enum", &"event.instance.enum", &"cause.fixture.enum").is_success(), "valid queued event exists for corrupt enum restore tests")
var queued: Dictionary = _runtime.create_snapshot()
for raw_type: Variant in bad_types:
var corrupt: Dictionary = queued.duplicate(true)
for raw_definition: Dictionary in corrupt[&"systems"][&"runtime"][&"event_definitions"]:
if String(raw_definition[&"event_id"]) == "event.fixture.enum":
(raw_definition[&"automatic_actions"] as Array)[0][&"type"] = raw_type
_expect(not _runtime.restore_snapshot(corrupt) and _runtime.create_snapshot() == queued, "queued complete threat template rejects a corrupt action enum atomically")
var integer_enum: Dictionary = queued.duplicate(true)
for raw_definition: Dictionary in integer_enum[&"systems"][&"runtime"][&"event_definitions"]:
if String(raw_definition[&"event_id"]) == "event.fixture.enum":
(raw_definition[&"automatic_actions"] as Array)[0][&"type"] = EventAction.Type.CREATE_DEFENSE_RAID
_expect(_runtime.restore_snapshot(integer_enum) and _runtime.create_snapshot() == queued, "known serialized integer enum remains compatible and canonicalizes to its name")
func _test_failure_policy_and_expiry() -> void:
for policy: String in ["SKIP_FAILED", "BRANCH_FAILURE", "EXPIRED"]:
_reset_runtime(92_020)
_create_staff_and_facility(&"guard.event.policy", &"objective.event.policy")
_expect(_runtime.staff_service.create_task(&"fixture.work", 20, _clock.absolute_minute, {}, &"task.event.policy.duplicate").is_success(), "actual duplicate task exists for policy failure")
var primary := _event_raid_action(&"objective.event.policy", "THIEF")
var duplicate := EventAction.new().configure(&"fail.policy", EventAction.Type.CREATE_TASK, {&"task_id": &"task.event.policy.duplicate", &"task_type": &"fixture.work"})
var fallback := _event_raid_action(&"objective.event.policy", "AUTHORITY_ESCALATION")
fallback.action_id = &"fallback.threat"
var definition := {&"event_id": &"event.fixture.policy", &"category": &"DEFENSE", &"foreground": false, &"automatic_actions": [primary.create_snapshot(), duplicate.create_snapshot()]}
if policy == "EXPIRED":
definition[&"transaction_policy"] = &"ROLLBACK_ALL"
definition[&"fallback_actions"] = [fallback.create_snapshot()]
else:
definition[&"transaction_policy"] = policy
definition[&"failure_actions"] = [fallback.create_snapshot()]
_expect(_runtime.submit_command(&"register_event_definition", {&"definition": definition}).is_success(), "real Event registers policy " + policy)
var options := {&"event_instance_id": &"event.instance.policy", &"context": {&"threat_source_id": &"cause.policy.fixture"}}
if policy == "EXPIRED": options[&"expires_at"] = _clock.absolute_minute
_expect(_runtime.submit_command(&"enqueue_event", {&"event_id": &"event.fixture.policy", &"actor_ids": [], &"options": options}).is_success(), "real Event queue accepts policy cause " + policy)
_advance(1)
var primary_id := _event_raid_id(&"event.instance.policy", primary.action_id)
var fallback_id := _event_raid_id(&"event.instance.policy", fallback.action_id)
var expected_id := primary_id if policy == "SKIP_FAILED" else fallback_id
var missing_id := fallback_id if policy == "SKIP_FAILED" else primary_id
_expect(_runtime.defense_service.get_raid_ids() == [expected_id] and _runtime.defense_service.get_raid(missing_id).is_empty(), "policy commits only the intended threat and discards rolled-back primary: " + policy)
_expect(_count_domain_events(&"DEFENSE_RAID_CREATED", expected_id) == 1 and _count_domain_events(&"DEFENSE_RAID_CREATED", missing_id) == 0, "policy leaves exactly one committed incident notification: " + policy)
var instance: EventInstance = _runtime.event_service.get_instance(&"event.instance.policy")
_expect(instance.state == (EventInstance.State.EXPIRED if policy == "EXPIRED" else EventInstance.State.COMPLETED), "policy preserves its real Event terminal state: " + policy)
var saved: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(saved) and _runtime.create_snapshot() == saved, "non-primary threat provenance restores exactly: " + policy)
func _test_event_rollback_preserves_failed_round_guard() -> void:
_reset_runtime(92_021)
_create_staff_and_facility(&"guard.event.failure", &"objective.event.failure")
_create_staff_and_facility(&"guard.event.uncommitted", &"objective.event.uncommitted")
var prior_definition := _threat_definition(&"objective.event.failure")
prior_definition[&"waves"][0][&"max_rounds"] = 1
_expect(_submit(&"create_defense_raid", {&"raid_id": &"raid.event.prior", &"definition": prior_definition}).is_success(), "real existing incident precedes Event rollback")
_expect(_register_defender(&"raid.event.prior", &"guard.event.failure", 90).is_success() and _register_objective(&"raid.event.prior", &"objective.event.failure", 20).is_success() and _submit(&"start_defense_raid", {&"raid_id": &"raid.event.prior"}).is_success(), "prior real defense has an automatic due round")
var failure := {&"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(report.get(&"ok", false)):
report[&"ok"] = false
report[&"error"] = "Disclosed real post-apply failure before Event rollback."
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(5)
_expect(_clock.paused and failure[&"calls"] == 1 and _runtime.defense_service.get_raid(&"raid.event.prior")[&"current_round"] == 0, "real automatic round fails atomically and pauses once")
_expect(_runtime.staff_service.create_task(&"fixture.work", 20, _clock.absolute_minute, {}, &"task.event.guard.duplicate").is_success(), "Event later apply-time failure uses real duplicate task")
var threat := _event_raid_action(&"objective.event.uncommitted", "THIEF")
var duplicate := EventAction.new().configure(&"fail.guard", EventAction.Type.CREATE_TASK, {&"task_id": &"task.event.guard.duplicate", &"task_type": &"fixture.work"})
_expect(_register_threat_event(&"event.fixture.guard", [threat, duplicate]).is_success() and _enqueue_threat_event(&"event.fixture.guard", &"event.instance.guard", &"cause.guard.fixture").is_success(), "real failed-minute Event registers and queues")
var defense_before: Dictionary = _runtime.defense_service.create_snapshot()
var rng_before: Dictionary = _runtime.get_rng_stream(&"defense").snapshot()
var failed_event: CommandResult = _runtime.event_service.start_next_event(_clock.absolute_minute)
_expect(not failed_event.is_success() and _runtime.defense_service.create_snapshot() == defense_before and _runtime.get_rng_stream(&"defense").snapshot() == rng_before, "Event rollback restores existing Defense and RNG after newly-created incident")
_expect(_count_domain_events(&"DEFENSE_AUTOMATIC_ROUND_FAILED", &"raid.event.prior") == 1 and _count_domain_events(&"DEFENSE_RAID_CREATED", _event_raid_id(&"event.instance.guard", threat.action_id)) == 0, "Event rollback keeps prior failure diagnostic while removing uncommitted creation")
_runtime._advance_defense(_clock.absolute_minute)
_expect(failure[&"calls"] == 1 and _runtime.defense_service.get_raid(&"raid.event.prior")[&"current_round"] == 0, "Event rollback cannot clear the real same-minute automatic attempt guard")
_runtime._configure_defense_boundaries()
_clock.set_paused(false)
_clock.advance_minutes(1)
_clock.set_paused(true)
_expect(_runtime.defense_service.get_raid(&"raid.event.prior")[&"current_round"] == 1, "next real minute resumes prior incident after its failed attempt")
@@ -0,0 +1 @@
uid://dwqqi0hae5h52