383 lines
14 KiB
GDScript
383 lines
14 KiB
GDScript
extends SceneTree
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _clock: Variant
|
|
var _state: Variant
|
|
var _runtime: Variant
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("MVP: Runtime operations integration")
|
|
_reset_runtime()
|
|
_test_disabled_compatibility_and_enable()
|
|
_test_enabled_snapshot_round_trip()
|
|
_test_seed_424242_second_arrival_boundary()
|
|
_test_after_mutation_notifications_are_atomic()
|
|
_test_deferred_failure_backlog_and_exact_retry()
|
|
_test_daily_payroll_financials_and_crosslink_tamper()
|
|
_cleanup()
|
|
if _failures == 0:
|
|
print("PASS: %d checks" % _checks)
|
|
else:
|
|
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
|
|
func _reset_runtime(seed: int = 20260827) -> void:
|
|
_cleanup()
|
|
_clock = load("res://autoload/game_clock.gd").new()
|
|
_state = load("res://autoload/game_state.gd").new()
|
|
_runtime = load("res://autoload/simulation_runtime.gd").new()
|
|
_clock.reset(1, 6, 0)
|
|
_state.reset()
|
|
var report: Dictionary = _runtime.initialize(_clock, _state, seed)
|
|
_expect(bool(report.get(&"valid", false)), "Runtime initializes before Operations opt-in")
|
|
_clock.set_paused(true)
|
|
|
|
|
|
func _cleanup() -> void:
|
|
if _runtime != null:
|
|
_runtime.free()
|
|
if _clock != null:
|
|
_clock.free()
|
|
if _state != null:
|
|
_state.free()
|
|
_runtime = null
|
|
_clock = null
|
|
_state = null
|
|
|
|
|
|
func _test_disabled_compatibility_and_enable() -> void:
|
|
var disabled: Dictionary = _runtime.get_operations_report()
|
|
_expect(not disabled[&"enabled"], "Operations remain disabled for existing Runtime tests")
|
|
var disabled_snapshot: Dictionary = _runtime.create_snapshot()
|
|
var disabled_runtime: Dictionary = disabled_snapshot[&"systems"][&"runtime"]
|
|
_expect(int(disabled_runtime[&"runtime_schema_version"]) == 7, "Runtime snapshot advances to schema v7")
|
|
_expect(
|
|
disabled_runtime[&"operations"] == {
|
|
&"wrapper_version": 1,
|
|
&"enabled": false,
|
|
&"personal_action_points": 0,
|
|
&"runtime_failure_latched": false,
|
|
&"runtime_failure_report": {},
|
|
&"deferred_runtime_minute": -1,
|
|
&"coordinator": {},
|
|
},
|
|
"disabled Operations wrapper has one canonical shape"
|
|
)
|
|
var enabled: Dictionary = _runtime.enable_production_operations()
|
|
var enabled_ok := bool(enabled.get(&"ok", false))
|
|
_expect(enabled_ok, "Production Operations opt in succeeds only at Day 1 06:00")
|
|
if not enabled_ok:
|
|
print(" enable diagnostic: %s" % JSON.stringify(enabled, "", true, true))
|
|
return
|
|
_expect(
|
|
_runtime.operations_coordinator.current_minute == 360,
|
|
"enable processes the 06:00 boundary from minute 359 exactly once"
|
|
)
|
|
_expect(
|
|
int(_runtime.get_operations_report()[&"personal_action_points"]) == 2,
|
|
"first morning boundary restores two personal action points"
|
|
)
|
|
|
|
|
|
func _test_enabled_snapshot_round_trip() -> void:
|
|
if _runtime.operations_coordinator == null:
|
|
return
|
|
var snapshot: Dictionary = _runtime.create_snapshot()
|
|
var expected_digest := SimulationDigest.compute(snapshot)
|
|
_expect(_runtime.restore_snapshot(snapshot), "enabled Runtime v5 snapshot restores")
|
|
_expect(_runtime.latest_digest == expected_digest, "enabled Runtime v5 round trip is exact")
|
|
var operations: Dictionary = _runtime.get_operations_report()
|
|
_expect(
|
|
operations[&"enabled"] and _runtime.operations_coordinator.current_minute == _clock.absolute_minute,
|
|
"restored Coordinator clock cross-link remains exact"
|
|
)
|
|
|
|
|
|
func _test_seed_424242_second_arrival_boundary() -> void:
|
|
_reset_runtime(424242)
|
|
var enabled: Dictionary = _runtime.enable_production_operations()
|
|
_expect(bool(enabled.get(&"ok", false)), "seed 424242 enables production Operations")
|
|
if not bool(enabled.get(&"ok", false)):
|
|
return
|
|
var before_count: int = _runtime.guest_service.guest_count()
|
|
_clock.advance_minutes(30)
|
|
var report: Dictionary = _runtime.get_operations_report()
|
|
_expect(
|
|
_clock.absolute_minute == 390 \
|
|
and int(report.get(&"deferred_runtime_minute", -2)) == -1 \
|
|
and not bool(report.get(&"runtime_failure_latched", true)) \
|
|
and not _runtime.operations_coordinator.is_external_quarantined(),
|
|
"seed 424242 completes the minute 390 arrival boundary without quarantine"
|
|
)
|
|
_expect(
|
|
_runtime.operations_coordinator.get_population_count() \
|
|
== _runtime.guest_service.guest_count() \
|
|
and _runtime.guest_service.guest_count() >= before_count,
|
|
"second arrival keeps Coordinator and Guest population cross-linked"
|
|
)
|
|
|
|
|
|
func _test_after_mutation_notifications_are_atomic() -> void:
|
|
_reset_runtime(424242)
|
|
_expect(
|
|
bool(_runtime.enable_production_operations().get(&"ok", false)),
|
|
"after-mutation rollback fixture enables Operations"
|
|
)
|
|
_clock.advance_minutes(29)
|
|
var before := {
|
|
&"economy": _runtime.economy_service.create_snapshot(),
|
|
&"guests": _runtime.guest_service.create_snapshot(),
|
|
&"path": _runtime.path_service.create_snapshot(),
|
|
&"movement": _runtime._movement_credits.duplicate(true),
|
|
&"progression": _runtime.progression_service.create_snapshot(),
|
|
&"events": _runtime.domain_events.create_snapshot(),
|
|
&"ids": _runtime.id_generator.create_snapshot(),
|
|
}
|
|
var signals := {&"guest": 0, &"economy": 0, &"arrival": 0}
|
|
_runtime.guest_service.guest_spawned.connect(
|
|
func(_guest: GuestRuntime) -> void: signals[&"guest"] += 1
|
|
)
|
|
_runtime.economy_service.transaction_posted.connect(
|
|
func(_transaction_id: StringName, _entries: Array) -> void:
|
|
signals[&"economy"] += 1
|
|
)
|
|
_runtime.operations_coordinator.arrivals_committed.connect(
|
|
func(_report: Dictionary) -> void: signals[&"arrival"] += 1
|
|
)
|
|
_runtime.set_operations_test_boundary_hook(
|
|
&"ARRIVAL_PLAN",
|
|
func(_payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary:
|
|
return {
|
|
&"ok": phase != _runtime.OPERATIONS_TEST_AFTER_FIRST_MUTATION_PHASE,
|
|
&"reason": &"FORCED_AFTER_MUTATION_FAILURE",
|
|
}
|
|
)
|
|
_clock.advance_minutes(1)
|
|
var after := {
|
|
&"economy": _runtime.economy_service.create_snapshot(),
|
|
&"guests": _runtime.guest_service.create_snapshot(),
|
|
&"path": _runtime.path_service.create_snapshot(),
|
|
&"movement": _runtime._movement_credits.duplicate(true),
|
|
&"progression": _runtime.progression_service.create_snapshot(),
|
|
&"events": _runtime.domain_events.create_snapshot(),
|
|
&"ids": _runtime.id_generator.create_snapshot(),
|
|
}
|
|
_expect(
|
|
_clock.absolute_minute == 390 and _clock.paused and before == after,
|
|
"failure after the first arrival mutation restores every external domain exactly"
|
|
)
|
|
_expect(
|
|
int(signals[&"guest"]) == 0 and int(signals[&"economy"]) == 0 \
|
|
and int(signals[&"arrival"]) == 0,
|
|
"rolled-back arrival leaks no Guest, Economy, or Coordinator success signal"
|
|
)
|
|
_runtime.set_operations_test_boundary_hook(&"ARRIVAL_PLAN", Callable())
|
|
var retried: Dictionary = _runtime.acknowledge_operations_pause()
|
|
_expect(
|
|
bool(retried.get(&"ok", false)) and int(signals[&"guest"]) > 0 \
|
|
and int(signals[&"economy"]) > 0 and int(signals[&"arrival"]) == 1,
|
|
"successful retry flushes each staged service notification after commit"
|
|
)
|
|
|
|
|
|
func _test_deferred_failure_backlog_and_exact_retry() -> void:
|
|
_reset_runtime(424242)
|
|
_expect(
|
|
bool(_runtime.enable_production_operations().get(&"ok", false)),
|
|
"deferred retry fixture enables Operations"
|
|
)
|
|
_clock.advance_minutes(29)
|
|
var scheduled: CommandResult = _runtime.submit_command(
|
|
&"spawn_guest",
|
|
{
|
|
&"guest_id": &"scheduled_boundary_guest",
|
|
&"guest_type_id": _runtime.get_mvp_guest_definition_ids()[0],
|
|
&"gold": 25,
|
|
&"options": {
|
|
&"current_cell": Vector3i.ZERO,
|
|
&"planned_exit_at": 2_000,
|
|
},
|
|
},
|
|
&"test", &"scheduled_boundary_guest", 100, 390,
|
|
&"operations.test.scheduled.spawn"
|
|
)
|
|
_expect(scheduled.is_success(), "minute 390 command is queued before the boundary")
|
|
var counts := {&"guest": 0, &"arrival": 0, &"hook": 0}
|
|
_runtime.guest_service.guest_spawned.connect(
|
|
func(_guest: GuestRuntime) -> void: counts[&"guest"] += 1
|
|
)
|
|
_runtime.operations_coordinator.arrivals_committed.connect(
|
|
func(_report: Dictionary) -> void: counts[&"arrival"] += 1
|
|
)
|
|
_runtime.set_operations_test_boundary_hook(
|
|
&"ARRIVAL_PLAN",
|
|
func(_payload: Dictionary, phase: StringName, _token: Variant) -> Dictionary:
|
|
counts[&"hook"] += 1
|
|
return {
|
|
&"ok": false,
|
|
&"reason": &"FORCED_PREPARE_FAILURE",
|
|
&"phase": phase,
|
|
}
|
|
)
|
|
_clock.set_speed(4)
|
|
var processed: int = _clock.process_real_delta(5.0)
|
|
var failed_report: Dictionary = _runtime.get_operations_report()
|
|
_expect(
|
|
processed == 1 and _clock.absolute_minute == 390 and _clock.paused,
|
|
"boundary failure stops an eight-tick backlog at the failed minute"
|
|
)
|
|
_expect(
|
|
int(failed_report[&"deferred_runtime_minute"]) == 390 \
|
|
and failed_report[&"runtime_failure_latched"] \
|
|
and _runtime.operations_coordinator.current_minute == 389,
|
|
"failed boundary persists one canonical deferred shape"
|
|
)
|
|
_expect(
|
|
_runtime.guest_service.has_guest(&"scheduled_boundary_guest") \
|
|
and int(counts[&"guest"]) == 1 \
|
|
and int(counts[&"arrival"]) == 0,
|
|
"pre-boundary command commits once while failed callback emits no arrival notification"
|
|
)
|
|
var interrupted: Dictionary = _runtime.create_snapshot()
|
|
_expect(
|
|
_runtime.restore_snapshot(interrupted),
|
|
"deferred v5 snapshot restores before acknowledgement"
|
|
)
|
|
_runtime.set_operations_test_boundary_hook(&"ARRIVAL_PLAN", Callable())
|
|
var retry_guest_signals := {&"count": 0}
|
|
_runtime.guest_service.guest_spawned.connect(
|
|
func(_guest: GuestRuntime) -> void: retry_guest_signals[&"count"] += 1
|
|
)
|
|
var acknowledged: Dictionary = _runtime.acknowledge_operations_pause()
|
|
_expect(
|
|
bool(acknowledged.get(&"ok", false)) \
|
|
and bool(acknowledged.get(&"suffix_replayed", false)) \
|
|
and _clock.absolute_minute == 390 \
|
|
and _runtime.operations_coordinator.current_minute == 390,
|
|
"acknowledgement retries the same boundary and suffix exactly once"
|
|
)
|
|
var completion_count := 0
|
|
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
|
|
if event.event_type == &"COMMAND_COMPLETED" \
|
|
and event.causation_command_id == scheduled.command_id:
|
|
completion_count += 1
|
|
_expect(
|
|
completion_count == 1 \
|
|
and _runtime.command_queue.processed_count() == 1 \
|
|
and _runtime.guest_service.current_minute == 390 \
|
|
and int(retry_guest_signals[&"count"]) \
|
|
== _runtime.operations_coordinator.get_arrival_reports()[-1][&"total_count"],
|
|
"retry never replays the command and advances the skipped Runtime suffix once"
|
|
)
|
|
var completed: Dictionary = _runtime.create_snapshot()
|
|
_expect(_runtime.restore_snapshot(completed), "completed deferred retry round-trips exactly")
|
|
|
|
|
|
func _test_daily_payroll_financials_and_crosslink_tamper() -> void:
|
|
_reset_runtime(811_004)
|
|
var hired: CommandResult = _runtime.submit_command(
|
|
&"hire_staff_candidate",
|
|
{&"candidate_id": &"staff_akane", &"current_cell": Vector3i.ZERO},
|
|
&"player", &"staff_akane", 100, -1, &"operations.payroll.hire.akane"
|
|
)
|
|
_expect(hired.is_success(), "daily payroll fixture hires one catalog Staff member")
|
|
_expect(
|
|
bool(_runtime.enable_production_operations().get(&"ok", false)),
|
|
"daily payroll fixture enables Operations after hiring"
|
|
)
|
|
_clock.advance_minutes(1_440)
|
|
var summary: Dictionary = _runtime.operations_coordinator.get_day_summary(1)
|
|
var settlement: Dictionary = summary.get(&"settlement", {})
|
|
_expect(
|
|
_clock.absolute_minute == 1_800 \
|
|
and int(summary.get(&"closed_at", -1)) == 1_800 \
|
|
and int(summary[&"category_totals"][&"wages"]) == 120,
|
|
"06:00 closes Day 1 once and accrues only the new daily wage"
|
|
)
|
|
_expect(
|
|
int(settlement.get(&"paid_total", -1)) == 120 \
|
|
and int(settlement.get(&"arrears_total", -1)) == 0 \
|
|
and _runtime.staff_service.get_staff(&"staff_akane").unpaid_wages == 0,
|
|
"sufficient Gold pays the full lexical payroll with no arrears"
|
|
)
|
|
var wage_transaction: StringName = _runtime.economy_service.get_transaction_id_for_idempotency_key(
|
|
&"operations.wage.day.00000001"
|
|
)
|
|
var wage_entries: Array[LedgerEntry] = _runtime.economy_service.get_transaction_entries(wage_transaction)
|
|
_expect(
|
|
wage_entries.size() == 1 \
|
|
and wage_entries[0].category == &"WAGE" \
|
|
and wage_entries[0].absolute_minute == 1_800 \
|
|
and wage_entries[0].amount == 120,
|
|
"paid payroll posts one canonical WAGE ledger line at the closing boundary"
|
|
)
|
|
var canonical: Dictionary = _runtime.create_snapshot()
|
|
_expect(_runtime.restore_snapshot(canonical), "Day 1 payroll/P&L v5 snapshot round-trips")
|
|
|
|
var signing_category: Dictionary = canonical.duplicate(true)
|
|
for raw_entry: Dictionary in signing_category[&"systems"][&"runtime"][&"economy"][&"entries"]:
|
|
if String(raw_entry[&"category"]) == "STAFF_SIGNING_COST":
|
|
raw_entry[&"category"] = "TRANSFER"
|
|
break
|
|
_expect(
|
|
_rejects_atomically(signing_category),
|
|
"closed non-WAGE ledger category tamper is rejected atomically"
|
|
)
|
|
var wage_category: Dictionary = canonical.duplicate(true)
|
|
for raw_entry: Dictionary in wage_category[&"systems"][&"runtime"][&"economy"][&"entries"]:
|
|
if String(raw_entry[&"category"]) == "WAGE":
|
|
raw_entry[&"category"] = "TRANSFER"
|
|
break
|
|
_expect(
|
|
_rejects_atomically(wage_category),
|
|
"Staff settlement cannot outlive its canonical WAGE ledger evidence"
|
|
)
|
|
var lifetime: Dictionary = canonical.duplicate(true)
|
|
lifetime[&"systems"][&"runtime"][&"progression"][&"metrics"][&"guest_lifetime"] += 1
|
|
_expect(
|
|
_rejects_atomically(lifetime),
|
|
"Progression lifetime Guest inflation is rejected against active plus persistent Guests"
|
|
)
|
|
var plan_class: Dictionary = canonical.duplicate(true)
|
|
var arrival_reports: Array = plan_class[&"systems"][&"runtime"][&"operations"][&"coordinator"][&"arrival_reports"]
|
|
var changed_plan := false
|
|
for report: Dictionary in arrival_reports:
|
|
if not (report[&"plans"] as Array).is_empty():
|
|
var plan: Dictionary = report[&"plans"][0]
|
|
plan[&"guest_class"] = "noble" if String(plan[&"guest_class"]) != "noble" else "laborer"
|
|
changed_plan = true
|
|
break
|
|
_expect(
|
|
changed_plan and _rejects_atomically(plan_class),
|
|
"arrival plan class cannot diverge from its persisted Guest definition"
|
|
)
|
|
var downgraded: Dictionary = canonical.duplicate(true)
|
|
downgraded[&"systems"][&"runtime"][&"runtime_schema_version"] = 4
|
|
downgraded[&"systems"][&"runtime"].erase(&"kitchen_production")
|
|
_expect(
|
|
_rejects_atomically(downgraded),
|
|
"relabelled enabled v5 cannot discard Operations history through v4 migration"
|
|
)
|
|
|
|
|
|
func _rejects_atomically(candidate: Dictionary) -> bool:
|
|
var before: Dictionary = _runtime.create_snapshot()
|
|
return not _runtime.restore_snapshot(candidate) \
|
|
and _runtime.create_snapshot() == before
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|