Preserve approved pixel tower prototype as development baseline

This commit is contained in:
Codex
2026-09-09 07:51:16 +09:00
commit 423fd6dd2e
915 changed files with 95193 additions and 0 deletions
+778
View File
@@ -0,0 +1,778 @@
extends SceneTree
const GuestDefinitionType = preload("res://scripts/guest/guest_definition.gd")
const GuestRuntimeType = preload("res://scripts/guest/guest_runtime.gd")
const GuestServiceType = preload("res://scripts/guest/guest_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S3: deterministic Guest AI")
_test_definition_and_runtime_contract()
_test_need_updates_and_single_clamp()
_test_need_context_and_adult_gate()
_test_shared_need_context_fast_paths()
_test_batched_need_updates_match_individual_api()
_test_candidate_filtering()
_test_integer_utility_and_stable_tie_break()
_test_state_machine_and_loop_guard()
_test_atomic_queue_abandonment()
_test_service_interruption_is_idempotent()
_test_service_need_effect_schema()
_test_path_failure_safe_recovery()
_test_exit_persistence_boundary()
_test_snapshot_round_trip_and_atomic_rejection()
_test_v1_need_migration_and_schema_rejection()
_test_deterministic_decision_snapshot()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_definition_and_runtime_contract() -> void:
var definition := GuestDefinitionType.new().configure(&"adventurer", {
&"tags": [&"adventurer", &"general", &"general"],
&"wealth_min": 100,
&"wealth_max": 500,
&"base_patience": 45,
&"risk_tolerance": 80,
&"preference_weights": {&"meal": 8, &"quiet": -2},
&"group_size_weights": {1: 80, 2: 20},
&"legal_sensitivity": 60,
&"quality_sensitivity": 70,
&"adult_verified": false,
&"need_rates": {&"hunger": 3, &"sleep": -2, &"leisure": 1},
&"credit_limit_gold": 100,
&"minimum_quality": 40,
})
_expect(definition.is_valid(), "complete guest definition validates")
_expect(definition.tags == [&"adventurer", &"general"], "definition tags are unique and stable-sorted")
_expect(definition.get_need_rate(&"intimacy") == 0, "omitted canonical Need rates normalize to zero")
_expect(definition.need_rates.size() == 6, "definition always materializes the six canonical Need IDs")
var round_trip := GuestDefinitionType.from_snapshot(definition.create_snapshot())
_expect(round_trip != null, "definition reconstructs from its canonical snapshot")
_expect(round_trip.create_snapshot() == definition.create_snapshot(), "definition snapshot round trip is exact")
var legacy_definition := GuestDefinitionType.new().configure(&"legacy_alias", {
&"need_rates": {&"rest": 1},
})
_expect(not legacy_definition.is_valid(), "legacy Definition Need aliases are invalid outside v1 migration")
var legacy_runtime := GuestRuntimeType.new().configure(&"legacy_guest", &"adventurer", {
&"needs": {&"social": 10},
})
_expect(not legacy_runtime.is_valid(), "legacy Runtime Need aliases are invalid outside v1 migration")
var coerced_adult := GuestDefinitionType.new().configure(&"coerced_adult", {
&"adult_verified": "true",
})
_expect(not coerced_adult.is_valid(), "adult verification cannot be supplied through truthy string coercion")
var service := GuestServiceType.new()
_expect(service.register_definition(definition), "valid definition registers")
_expect(not service.register_definition(definition), "duplicate definition ID is rejected")
var guest := service.spawn_guest(&"guest_contract", &"adventurer", 10, {
&"planned_exit_at": 200,
&"current_cell": Vector3i(2, 3, 0),
})
_expect(guest != null and guest.is_valid(), "guest runtime spawns with required authoritative fields")
_expect(guest.wallet_gold == 100 and guest.credit_limit_gold == 100, "definition supplies deterministic wallet floor and credit limit")
_expect(guest.patience_remaining == 45, "definition supplies base queue patience")
_expect(service.validate_invariants()[&"valid"], "fresh GuestService invariants hold")
func _test_need_updates_and_single_clamp() -> void:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_needs", &"adventurer", 0, {
&"state": GuestRuntimeType.State.MOVING,
&"needs": {
&"hunger": 99,
&"sleep": 1,
&"hygiene": 99,
&"medical": 99,
&"leisure": 99,
&"intimacy": 99,
},
&"planned_exit_at": 500,
})
var report: Dictionary = service.update_needs(guest.guest_id, {
&"dirt": 2,
&"injury": 2,
&"illness": 2,
&"need_effects": {
&"hunger": 5,
&"sleep": -5,
&"hygiene": 3,
&"medical": 3,
&"leisure": 3,
&"intimacy": 3,
},
})
_expect(report[&"ok"], "Need update succeeds for a live guest")
_expect(report[&"deltas"]["hunger"] == 10, "base, movement and external hunger effects add before clamp")
_expect(report[&"deltas"]["sleep"] == -5, "positive and negative sleep effects combine before clamp")
_expect(guest.needs[&"hunger"] == 100 and guest.needs[&"sleep"] == 0, "Need upper and lower bounds clamp once")
_expect(guest.needs[&"hygiene"] == 100 and guest.needs[&"medical"] == 100, "dirt, injury and illness update their distinct Needs")
_expect(guest.needs[&"leisure"] == 100 and guest.needs[&"intimacy"] == 100, "leisure and intimacy effects use canonical IDs")
_expect(service.advance_one_minute(1)[&"ok"], "one-minute GuestService cadence advances")
_expect(not service.advance_one_minute(1)[&"ok"], "the same minute cannot apply Need drift twice")
func _test_need_context_and_adult_gate() -> void:
var service := _service_with_definitions()
var verified := service.spawn_guest(&"verified_context", &"verified_noble", 0, {
&"needs": {&"hunger": 0, &"sleep": 0, &"hygiene": 10, &"medical": 20, &"leisure": 0, &"intimacy": 30},
&"planned_exit_at": 500,
})
var unverified := service.spawn_guest(&"unverified_context", &"adventurer", 0, {
&"needs": {&"hunger": 0, &"sleep": 0, &"hygiene": 10, &"medical": 20, &"leisure": 0, &"intimacy": 30},
&"planned_exit_at": 500,
})
var day := service.update_needs(verified.guest_id, {
&"unstimulated": false,
&"dirt": 5,
&"injury": 4,
&"illness": 3,
&"is_night": false,
&"intimacy_day": 3,
&"intimacy_night": 7,
})
_expect(day[&"deltas"]["hygiene"] == 5, "integer dirt context raises Hygiene only")
_expect(day[&"deltas"]["medical"] == 7, "injury and illness combine into Medical")
_expect(day[&"deltas"]["intimacy"] == 3, "day context uses the adult-verified Intimacy day rate")
var night := service.update_needs(verified.guest_id, {
&"unstimulated": false,
&"is_night": true,
&"intimacy_day": 3,
&"intimacy_night": 7,
})
_expect(night[&"deltas"]["intimacy"] == 7, "night context selects the separate Intimacy night rate")
var blocked := service.update_needs(unverified.guest_id, {
&"unstimulated": false,
&"is_night": true,
&"intimacy_night": 7,
})
_expect(blocked[&"deltas"]["intimacy"] == 0, "unverified guests cannot receive contextual adult Intimacy drift")
var before_invalid := service.create_snapshot()
var wrong_type := service.update_needs(verified.guest_id, {&"injury": "4"})
_expect(not wrong_type[&"ok"], "stringified Need context integers are rejected")
_expect(service.create_snapshot() == before_invalid, "invalid context leaves live Need state untouched")
var legacy_alias := service.update_needs(verified.guest_id, {
&"need_effects": {&"rest": -5},
})
_expect(not legacy_alias[&"ok"], "legacy Need aliases are rejected outside snapshot migration")
_expect(service.create_snapshot() == before_invalid, "legacy context rejection is atomic")
func _test_batched_need_updates_match_individual_api() -> void:
var batched := _service_with_definitions()
batched.spawn_guest(&"z_guest", &"adventurer", 0, {
&"state": GuestRuntimeType.State.MOVING,
&"group_id": &"party",
&"needs": {&"hunger": 10, &"sleep": 20, &"hygiene": 30, &"medical": 40, &"leisure": 50, &"intimacy": 60},
&"planned_exit_at": 500,
})
batched.spawn_guest(&"a_guest", &"verified_noble", 0, {
&"needs": {&"hunger": 5, &"sleep": 15, &"hygiene": 25, &"medical": 35, &"leisure": 45, &"intimacy": 55},
&"planned_exit_at": 500,
})
var individual := GuestServiceType.new()
individual.restore_snapshot(batched.create_snapshot())
var default_context := {
&"unstimulated": false,
&"dirt": 1,
&"need_effects": {&"sleep": 3},
}
var z_context := {
&"solo_guest": true,
&"injury": 2,
&"need_effects": {&"hunger": -1, &"leisure": 4},
}
var expected_reports: Array[Dictionary] = [
individual.update_needs(&"a_guest", default_context),
individual.update_needs(&"z_guest", z_context),
]
individual.current_minute = 1
var batch_report := batched.advance_one_minute(1, {
&"default": default_context,
"z_guest": z_context,
})
_expect(
batch_report[&"guest_reports"] == expected_reports,
"batched Need cadence preserves individual report values and stable guest-id order"
)
_expect(
batched.create_snapshot() == individual.create_snapshot(),
"optimized batched Need mutation is snapshot-equivalent to individual updates"
)
func _test_shared_need_context_fast_paths() -> void:
var source := _service_with_definitions()
for guest_id: StringName in [&"z_guest", &"a_guest"]:
source.spawn_guest(guest_id, &"adventurer", 0, {
&"state": GuestRuntimeType.State.MOVING,
&"needs": {
&"hunger": 10,
&"sleep": 20,
&"hygiene": 30,
&"medical": 40,
&"leisure": 50,
&"intimacy": 60,
},
&"planned_exit_at": 500,
})
var start := source.create_snapshot()
var empty_fast := GuestServiceType.new()
var empty_reference := GuestServiceType.new()
empty_fast.restore_snapshot(start)
empty_reference.restore_snapshot(start)
var empty_report := empty_fast.advance_one_minute(1)
var empty_reference_report := empty_reference.advance_one_minute(1, {
&"default": {},
&"a_guest": {},
&"z_guest": {},
})
_expect(
empty_report == empty_reference_report,
"empty shared Need context preserves the preflight-path report exactly"
)
_expect(
empty_fast.create_snapshot() == empty_reference.create_snapshot(),
"empty shared Need context preserves the preflight-path snapshot exactly"
)
var common_context := {
&"unstimulated": false,
&"dirt": 2,
&"injury": 1,
&"need_effects": {&"sleep": 3, &"leisure": -2},
}
var default_fast := GuestServiceType.new()
var default_reference := GuestServiceType.new()
default_fast.restore_snapshot(start)
default_reference.restore_snapshot(start)
var default_report := default_fast.advance_one_minute(1, {
&"default": common_context,
})
var default_reference_report := default_reference.advance_one_minute(1, {
&"default": common_context,
&"a_guest": common_context,
&"z_guest": common_context,
})
_expect(
default_report == default_reference_report,
"default-only shared Need context preserves the override-path report exactly"
)
_expect(
default_fast.create_snapshot() == default_reference.create_snapshot(),
"default-only shared Need context preserves the override-path snapshot exactly"
)
var invalid_default := GuestServiceType.new()
invalid_default.restore_snapshot(start)
var before_invalid := invalid_default.create_snapshot()
var invalid_report := invalid_default.advance_one_minute(1, {
&"default": {&"injury": "1"},
})
_expect(not invalid_report[&"ok"], "invalid default-only Need context is rejected")
_expect(
invalid_default.create_snapshot() == before_invalid,
"invalid default-only Need context is atomic across the population and minute"
)
var malformed_default := GuestServiceType.new()
var malformed_reference := GuestServiceType.new()
malformed_default.restore_snapshot(start)
malformed_reference.restore_snapshot(start)
var malformed_report := malformed_default.advance_one_minute(1, {
&"default": "legacy malformed default",
})
var malformed_reference_report := malformed_reference.advance_one_minute(1)
_expect(
malformed_report == malformed_reference_report,
"non-Dictionary default retains the legacy empty-context meaning"
)
_expect(
malformed_default.create_snapshot() == malformed_reference.create_snapshot(),
"malformed-default compatibility remains snapshot-exact"
)
func _test_candidate_filtering() -> void:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_filter", &"adventurer", 0, {
&"wallet_gold": 500,
&"credit_limit_gold": 100,
&"planned_exit_at": 1_000,
})
var candidates: Array = [
_candidate(&"cannot_pay", {&"price_gold": 501}),
_candidate(&"adult_room", {&"adult_only": true}),
_candidate(&"unreachable", {&"reachable": false}),
_candidate(&"closed", {&"is_open": false}),
_candidate(&"too_slow", {&"wait_minutes": 120}),
_candidate(&"too_risky", {&"risk_level": 81}),
_candidate(&"too_illegal", {&"illegal_level": 41}),
_candidate(&"too_poor", {&"expected_quality": 39}),
_candidate(&"legacy_relief", {&"need_relief": {&"fun": 40}}),
_candidate(&"legacy_effects", {&"need_effects": {&"safety": -20}}),
_candidate(&"canonical_effects", {&"need_effects": {&"medical": -20}}),
_candidate(&"credit_ok", {&"price_gold": 550, &"credit_allowed": true}),
_candidate(&"normal_ok"),
]
var filtered := service.extract_candidates(guest.guest_id, candidates, {
&"absolute_minute": 10,
&"adult_content_enabled": true,
&"wait_hard_limit": 120,
})
_expect(_candidate_ids(filtered) == [&"canonical_effects", &"credit_ok", &"normal_ok"], "candidate extraction applies every hard filter before Utility")
var reasons := _rejection_map(service.get_last_candidate_rejections(guest.guest_id))
_expect(reasons[&"cannot_pay"] == &"CANNOT_PAY", "SYS-GST-001 rejects an unaffordable facility")
_expect(reasons[&"adult_room"] == &"ADULT_NOT_VERIFIED", "SYS-GST-002 rejects restricted content before Utility")
_expect(reasons[&"unreachable"] == &"UNREACHABLE", "unreachable facility is removed")
_expect(reasons[&"too_slow"] == &"WAIT_HARD_LIMIT", "wait equal to the hard limit is removed")
_expect(reasons[&"too_risky"] == &"RISK_REFUSED", "risk above guest tolerance is removed")
_expect(reasons[&"too_illegal"] == &"ILLEGALITY_REFUSED", "illegal level above sensitivity threshold is removed")
_expect(reasons[&"too_poor"] == &"QUALITY_TOO_LOW", "quality below guest minimum is removed")
_expect(reasons[&"legacy_relief"] == &"INVALID_NEED_MAP", "candidate Need maps cannot use legacy aliases")
_expect(reasons[&"legacy_effects"] == &"INVALID_NEED_MAP", "candidate need_effects also reject migration-only aliases")
guest.wallet_gold = 100
var reserve_filtered := service.extract_candidates(guest.guest_id, [
_candidate(&"meal_with_cash", {&"price_gold": 80}),
_candidate(&"leisure_below_reserve", {
&"price_gold": 80,
&"service_category": &"entertainment",
}),
], {&"absolute_minute": 10})
_expect(_candidate_ids(reserve_filtered) == [&"meal_with_cash"], "discretionary spending preserves the personality-based survival reserve")
guest.wallet_gold = 500
var verified := service.spawn_guest(&"guest_verified", &"verified_noble", 0, {
&"wallet_gold": 1_000,
&"planned_exit_at": 1_000,
})
var adult_candidates := service.extract_candidates(verified.guest_id, [
_candidate(&"adult_room", {&"adult_only": true}),
], {&"adult_content_enabled": true, &"absolute_minute": 1})
_expect(adult_candidates.size() == 1, "verified adult guest can retain a permitted restricted candidate")
var content_blocked := service.extract_candidates(verified.guest_id, [
_candidate(&"adult_room", {&"adult_only": true}),
], {&"adult_content_enabled": false, &"absolute_minute": 1})
_expect(content_blocked.is_empty(), "Content Filter still overrides verified adult status")
var callback_order: Array[StringName] = []
service.extract_candidates(guest.guest_id, [
_candidate(&"z_path"),
_candidate(&"a_path"),
], {
&"absolute_minute": 1,
&"reachability_callback": func(_runtime: GuestRuntime, facility: Dictionary, _context: Dictionary) -> bool:
callback_order.append(facility[&"facility_id"])
return true,
})
_expect(callback_order == [&"a_path", &"z_path"], "reachability callbacks run in stable facility-ID order")
func _test_integer_utility_and_stable_tie_break() -> void:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_utility", &"adventurer", 0, {
&"wallet_gold": 1_000,
&"planned_exit_at": 1_000,
&"state": GuestRuntimeType.State.DECIDING,
&"needs": {&"hunger": 90},
})
var shared := {
&"need_relief": 40,
&"preference": 2,
&"expected_quality": 3,
&"novelty": 0,
&"group_fit": 4,
&"price_burden": 5,
&"travel_minutes": 6,
&"wait_minutes": 7,
&"risk": 8,
&"risk_sensitivity": 2,
&"repetition_penalty": 9,
&"urgency": 90,
&"minimum_quality": 0,
}
var candidate_z := shared.duplicate(true)
candidate_z[&"facility_id"] = &"z_room"
var candidate_a := shared.duplicate(true)
candidate_a[&"facility_id"] = &"a_room"
var evaluated := service.evaluate_candidate(guest.guest_id, candidate_z)
_expect(evaluated[&"utility"] is int, "Utility result is an integer")
_expect(evaluated[&"utility"] == 264, "Utility follows the documented integer term equation exactly")
var ranked := service.rank_candidates(guest.guest_id, [candidate_z, candidate_a])
_expect(ranked[0][&"facility_id"] == &"a_room", "SYS-GST-003 resolves a full Utility tie by facility_id")
var decision := service.decide(guest.guest_id, [candidate_z, candidate_a], 1, {
&"exit_threshold": 15,
})
_expect(decision[&"ok"] and decision[&"candidate"][&"facility_id"] == &"a_room", "decision reserves the stable winning candidate")
_expect(guest.state == GuestRuntimeType.State.MOVING and guest.target_facility_id == &"a_room", "successful decision enters MOVING with one target")
func _test_state_machine_and_loop_guard() -> void:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_states", &"adventurer", 0, {
&"planned_exit_at": 500,
})
_expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.ENTERING, 10)[&"ok"], "SPAWNING enters ENTERING")
_expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.DECIDING, 10)[&"ok"], "ENTERING enters DECIDING")
_expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.MOVING, 10)[&"ok"], "DECIDING enters MOVING")
_expect(service.transition_guest(guest.guest_id, GuestRuntimeType.State.REPLANNING, 10)[&"ok"], "MOVING enters REPLANNING")
var guarded := service.transition_guest(guest.guest_id, GuestRuntimeType.State.DECIDING, 10)
_expect(not guarded[&"ok"] and guarded[&"loop_guard"], "fifth transition in one Tick activates AI loop guard")
_expect(guest.state == GuestRuntimeType.State.DECIDING, "AI loop guard stops the guest in DECIDING")
_expect(service.get_diagnostics()[-1][&"code"] == &"AI_LOOP_GUARD", "loop guard leaves a diagnostic record")
var invalid := service.transition_guest(guest.guest_id, GuestRuntimeType.State.DESPAWNING, 11)
_expect(not invalid[&"ok"], "invalid state-machine edge is rejected")
func _test_atomic_queue_abandonment() -> void:
var service := _service_with_definitions()
var guest := _moving_guest(service, &"guest_queue")
var enqueue_calls := [0]
var entered := service.enter_queue(
guest.guest_id,
&"meal_room",
&"request_queue",
4,
1,
func(_guest_id: StringName, _facility_id: StringName, _request_id: StringName) -> bool:
enqueue_calls[0] += 1
return true
)
_expect(entered[&"ok"] and enqueue_calls[0] == 1, "queue entry crosses the Callable boundary once")
_expect(guest.state == GuestRuntimeType.State.QUEUING and guest.patience_remaining == 1, "queue entry records facility, request and initial patience")
var failed_calls := [0]
var failed_leave := service.tick_queue_patience(
guest.guest_id,
5,
0,
func(_guest_id: StringName, _facility_id: StringName, _request_id: StringName) -> bool:
failed_calls[0] += 1
return false
)
_expect(not failed_leave[&"ok"] and failed_calls[0] == 1, "external queue removal failure is surfaced")
_expect(guest.state == GuestRuntimeType.State.QUEUING and guest.patience_remaining == 1, "failed queue removal leaves guest state completely unchanged")
_expect(guest.target_facility_id == &"meal_room" and guest.queue_request_id == &"request_queue", "failed queue removal retains both cross-service references")
var successful_calls := [0]
var abandoned := service.tick_queue_patience(
guest.guest_id,
5,
0,
func(callback_guest_id: StringName, facility_id: StringName, request_id: StringName) -> Dictionary:
successful_calls[0] += 1
return {&"ok": callback_guest_id == guest.guest_id and facility_id == &"meal_room" and request_id == &"request_queue"}
)
_expect(abandoned[&"ok"] and abandoned[&"abandoned"] and successful_calls[0] == 1, "SYS-GST-004 removes an exhausted guest through one atomic callback")
_expect(guest.state == GuestRuntimeType.State.ABANDONING and guest.patience_remaining == 0, "successful patience exhaustion enters ABANDONING")
_expect(guest.target_facility_id.is_empty() and guest.queue_request_id.is_empty(), "atomic abandonment clears facility and queue ownership together")
func _test_service_interruption_is_idempotent() -> void:
var service := _service_with_definitions()
var guest := _moving_guest(service, &"guest_interrupt")
_expect(service.begin_service(guest.guest_id, &"bath", &"service_1", 4)[&"ok"], "moving guest begins a facility service")
var callback_calls := [0]
var first := service.interrupt_service(
guest.guest_id,
&"service_1",
5,
func(_guest_id: StringName, _service_id: StringName) -> bool:
callback_calls[0] += 1
return true
)
var replay := service.interrupt_service(
guest.guest_id,
&"service_1",
5,
func(_guest_id: StringName, _service_id: StringName) -> bool:
callback_calls[0] += 1
return true
)
_expect(first[&"ok"] and not first[&"replayed"], "first facility stop interrupts the active service")
_expect(replay[&"ok"] and replay[&"replayed"], "duplicate facility stop is an idempotent replay")
_expect(callback_calls[0] == 1, "SYS-GST-005 invokes interruption settlement only once")
_expect(guest.state == GuestRuntimeType.State.INTERRUPTED and guest.current_service_id.is_empty(), "interruption frees guest service ownership")
func _test_service_need_effect_schema() -> void:
var service := _service_with_definitions()
var guest := _moving_guest(service, &"guest_effect_schema")
guest.needs[&"sleep"] = 95
guest.needs[&"hygiene"] = 5
_expect(service.begin_service(guest.guest_id, &"bath", &"service_effects", 4)[&"ok"], "canonical Need-effect fixture begins service")
var before_invalid := service.create_snapshot()
var invalid := service.complete_service(
guest.guest_id,
&"service_effects",
5,
80,
{&"rest": -50}
)
_expect(not invalid[&"ok"], "service completion rejects a legacy Need-effect alias")
_expect(service.create_snapshot() == before_invalid, "invalid service effects do not partially complete service")
var completed := service.complete_service(
guest.guest_id,
&"service_effects",
5,
120,
{&"sleep": 20, &"hygiene": -20}
)
_expect(completed[&"ok"] and completed[&"satisfaction_sample"] == 100, "canonical service effects complete with satisfaction clamp")
_expect(guest.needs[&"sleep"] == 100 and guest.needs[&"hygiene"] == 0, "service Need effects clamp each canonical axis once")
func _test_path_failure_safe_recovery() -> void:
var service := _service_with_definitions()
var guest := _moving_guest(service, &"guest_path")
var recovery_targets: Array = [
{&"kind": &"emergency_exit", &"cell": Vector3i(9, 0, 0)},
{&"kind": &"entrance", &"cell": Vector3i(5, 0, 0)},
{&"kind": &"guidance", &"cell": Vector3i(2, 0, 0)},
]
var reachable := func(_from: Vector3i, target: Vector3i, _guest_id: StringName, _data: Dictionary) -> bool:
return target != Vector3i(2, 0, 0)
var first := service.record_path_failure(guest.guest_id, 4, recovery_targets, reachable)
var second := service.record_path_failure(guest.guest_id, 5, recovery_targets, reachable)
_expect(first[&"failure_count"] == 1 and not first[&"recovery_started"], "first path failure enters REPLANNING")
_expect(second[&"failure_count"] == 2 and not second[&"recovery_started"], "second path failure remains recoverable")
var third := service.record_path_failure(guest.guest_id, 6, recovery_targets, reachable)
_expect(third[&"ok"] and third[&"recovery_started"], "SYS-GST-007 starts safe recovery on the third path failure")
_expect(guest.state == GuestRuntimeType.State.LEAVING, "third failure cannot leave the guest stuck in REPLANNING")
_expect(guest.recovery_kind == &"entrance" and guest.recovery_target_cell == Vector3i(5, 0, 0), "recovery tries guidance, entrance, then emergency exit in order")
_expect(service.get_diagnostics()[-1][&"code"] == &"GUEST_PATH_RECOVERY", "safe path recovery emits a diagnostic record")
var fallback_service := _service_with_definitions()
var fallback_guest := _moving_guest(fallback_service, &"guest_fallback")
fallback_service.record_path_failure(fallback_guest.guest_id, 4)
fallback_service.record_path_failure(fallback_guest.guest_id, 5)
var fallback := fallback_service.record_path_failure(fallback_guest.guest_id, 6, [], Callable())
_expect(fallback[&"recovery_kind"] == &"emergency_despawn", "no-route case uses a virtual emergency egress rather than deadlocking")
func _test_exit_persistence_boundary() -> void:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_exit", &"adventurer", 0, {
&"state": GuestRuntimeType.State.LEAVING,
&"wallet_gold": 777,
&"debt_gold": 90,
&"satisfaction_total": 180,
&"satisfaction_samples": 2,
&"unresolved_incident_ids": [&"incident_b", &"incident_a"],
&"outstanding_reservation_ids": [&"reservation_1"],
&"planned_exit_at": 500,
})
var result := service.despawn_guest(guest.guest_id, 1)
_expect(result[&"ok"] and not service.has_guest(guest.guest_id), "leaving guest runtime is released after exit")
var record := service.get_persistent_record(guest.guest_id)
_expect(record[&"debt_gold"] == 90, "SYS-GST-006 preserves unresolved debt")
_expect(record[&"unresolved_incident_ids"] == ["incident_a", "incident_b"], "SYS-GST-006 preserves unresolved incidents")
_expect(record[&"outstanding_reservation_ids"] == ["reservation_1"], "SYS-GST-006 preserves outstanding reservations")
_expect(not record.has(&"wallet_gold") and not record.has(&"needs") and not record.has(&"satisfaction_total"), "ordinary transient guest state does not survive despawn")
func _test_snapshot_round_trip_and_atomic_rejection() -> void:
var original := _service_with_definitions()
var moving := _moving_guest(original, &"guest_saved_moving")
var queued := _moving_guest(original, &"guest_saved_queue")
original.enter_queue(queued.guest_id, &"cafeteria", &"request_saved", 4, 12)
moving.path_failure_count = 1
moving.suspicion = 33
original.advance_one_minute(20)
var snapshot := original.create_snapshot()
var restored := GuestServiceType.new()
_expect(restored.restore_snapshot(snapshot), "GuestService restores a valid snapshot")
_expect(restored.create_snapshot() == snapshot, "GuestService snapshot round trip is byte-structure equivalent")
_expect(restored.validate_invariants()[&"valid"], "restored guest state satisfies every invariant")
var before_corrupt := restored.create_snapshot()
var corrupt_need := snapshot.duplicate(true)
corrupt_need[&"guests"][0][&"needs"]["hunger"] = 101
_expect(not restored.restore_snapshot(corrupt_need), "out-of-range Need rejects snapshot restore")
_expect(restored.create_snapshot() == before_corrupt, "rejected Need snapshot leaves live state untouched")
var duplicate_guest := snapshot.duplicate(true)
duplicate_guest[&"guests"].append(duplicate_guest[&"guests"][0].duplicate(true))
_expect(not restored.restore_snapshot(duplicate_guest), "duplicate guest ID rejects snapshot restore")
_expect(restored.create_snapshot() == before_corrupt, "duplicate rejection is also atomic")
var orphan_type := snapshot.duplicate(true)
orphan_type[&"guests"][0][&"guest_type_id"] = "missing_type"
_expect(not restored.restore_snapshot(orphan_type), "unknown GuestDefinition reference rejects restore")
_expect(restored.create_snapshot() == before_corrupt, "orphan-reference rejection preserves live state")
func _test_v1_need_migration_and_schema_rejection() -> void:
var source := _service_with_definitions()
source.spawn_guest(&"guest_legacy", &"verified_noble", 0, {
&"needs": {&"hunger": 11, &"sleep": 22, &"hygiene": 0, &"medical": 44, &"leisure": 55, &"intimacy": 66},
&"planned_exit_at": 500,
})
var canonical := source.create_snapshot()
var legacy := canonical.duplicate(true)
legacy[&"version"] = 1
for definition: Dictionary in legacy[&"definitions"]:
var rates: Dictionary = definition[&"need_rates"]
definition[&"need_rates"] = {
&"hunger": rates["hunger"],
&"rest": rates["sleep"],
&"fun": rates["leisure"],
&"social": rates["intimacy"],
&"safety": rates["medical"],
}
for guest_snapshot: Dictionary in legacy[&"guests"]:
var needs: Dictionary = guest_snapshot[&"needs"]
guest_snapshot[&"needs"] = {
&"hunger": needs["hunger"],
&"rest": needs["sleep"],
&"fun": needs["leisure"],
&"social": needs["intimacy"],
&"safety": needs["medical"],
}
var migrated := GuestServiceType.new()
_expect(migrated.restore_snapshot(legacy), "GuestService deterministically migrates an exact v1 five-Need snapshot")
var migrated_snapshot := migrated.create_snapshot()
var migrated_needs: Dictionary = migrated_snapshot[&"guests"][0][&"needs"]
_expect(migrated_snapshot[&"version"] == 2, "v1 migration emits only the current GuestService schema")
_expect(migrated_needs == {
"hunger": 11,
"hygiene": 0,
"intimacy": 66,
"leisure": 55,
"medical": 44,
"sleep": 22,
}, "v1 rest/fun/social/safety map to sleep/leisure/intimacy/medical with Hygiene zero")
_expect(migrated.validate_invariants()[&"valid"], "migrated six-Need graph satisfies current invariants")
var stable_before := migrated.create_snapshot()
var missing := canonical.duplicate(true)
missing[&"guests"][0][&"needs"].erase("hygiene")
_expect(not migrated.restore_snapshot(missing), "current snapshots reject a missing canonical Need")
_expect(migrated.create_snapshot() == stable_before, "missing-Need rejection is atomic")
var unknown := canonical.duplicate(true)
unknown[&"guests"][0][&"needs"]["safety"] = 1
_expect(not migrated.restore_snapshot(unknown), "current snapshots reject unknown legacy Need keys")
_expect(migrated.create_snapshot() == stable_before, "unknown-Need rejection is atomic")
var wrong_type := canonical.duplicate(true)
wrong_type[&"definitions"][0][&"need_rates"]["sleep"] = "1"
_expect(not migrated.restore_snapshot(wrong_type), "current definition Need rates reject implicit integer coercion")
_expect(migrated.create_snapshot() == stable_before, "definition type rejection is atomic")
var mixed_v1 := legacy.duplicate(true)
mixed_v1[&"guests"][0][&"needs"]["sleep"] = 22
_expect(not migrated.restore_snapshot(mixed_v1), "v1 migration rejects mixed legacy/current Need maps")
_expect(migrated.create_snapshot() == stable_before, "mixed-schema rejection preserves live state")
func _test_deterministic_decision_snapshot() -> void:
var first := _deterministic_run()
var second := _deterministic_run()
_expect(first == second, "same guest definitions and candidates produce an identical authoritative snapshot")
func _deterministic_run() -> Dictionary:
var service := _service_with_definitions()
var guest := service.spawn_guest(&"guest_determinism", &"adventurer", 0, {
&"wallet_gold": 1_000,
&"planned_exit_at": 1_000,
&"state": GuestRuntimeType.State.DECIDING,
&"needs": {&"hunger": 80, &"leisure": 50},
})
var candidates: Array = [
_candidate(&"z_meal", {&"need_relief": {&"hunger": 60}, &"facility_tags": [&"meal"], &"travel_minutes": 4}),
_candidate(&"a_meal", {&"need_relief": {&"hunger": 60}, &"facility_tags": [&"meal"], &"travel_minutes": 4}),
_candidate(&"theater", {&"need_relief": {&"leisure": 50}, &"facility_tags": [&"leisure"], &"travel_minutes": 3}),
]
service.decide(guest.guest_id, candidates, 1, {&"absolute_minute": 1})
service.update_needs(guest.guest_id, {&"dirt": 2})
return service.create_snapshot()
func _service_with_definitions() -> GuestService:
var service := GuestServiceType.new()
service.register_definition(GuestDefinitionType.new().configure(&"adventurer", {
&"tags": [&"adventurer", &"general"],
&"wealth_min": 100,
&"wealth_max": 500,
&"base_patience": 45,
&"risk_tolerance": 80,
&"preference_weights": {&"meal": 8, &"leisure": 3},
&"group_size_weights": {1: 100},
&"legal_sensitivity": 60,
&"quality_sensitivity": 70,
&"adult_verified": false,
&"need_rates": {&"hunger": 3, &"sleep": -2, &"leisure": 1},
&"credit_limit_gold": 100,
&"minimum_quality": 40,
}))
service.register_definition(GuestDefinitionType.new().configure(&"verified_noble", {
&"tags": [&"wealthy", &"vip"],
&"wealth_min": 1_000,
&"wealth_max": 2_000,
&"base_patience": 20,
&"risk_tolerance": 40,
&"legal_sensitivity": 20,
&"adult_verified": true,
&"minimum_quality": 40,
}))
return service
func _moving_guest(service: GuestService, guest_id: StringName) -> GuestRuntime:
var guest := service.spawn_guest(guest_id, &"adventurer", 0, {
&"wallet_gold": 500,
&"planned_exit_at": 1_000,
})
service.transition_guest(guest_id, GuestRuntimeType.State.ENTERING, 1)
service.transition_guest(guest_id, GuestRuntimeType.State.DECIDING, 2)
service.transition_guest(guest_id, GuestRuntimeType.State.MOVING, 3)
return guest
func _candidate(facility_id: StringName, patch: Dictionary = {}) -> Dictionary:
var candidate := {
&"facility_id": facility_id,
&"is_open": true,
&"reachable": true,
&"price_gold": 100,
&"wait_minutes": 10,
&"risk_level": 10,
&"illegal_level": 10,
&"expected_quality": 60,
&"need_relief": 50,
}
for raw_key: Variant in patch:
candidate[raw_key] = patch[raw_key]
return candidate
func _candidate_ids(candidates: Array[Dictionary]) -> Array[StringName]:
var result: Array[StringName] = []
for candidate: Dictionary in candidates:
result.append(StringName(candidate[&"facility_id"]))
return result
func _rejection_map(rejections: Array[Dictionary]) -> Dictionary:
var result: Dictionary = {}
for rejection: Dictionary in rejections:
result[StringName(rejection[&"facility_id"])] = StringName(rejection[&"reason"])
return result
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://b5un0csk8il6t
+240
View File
@@ -0,0 +1,240 @@
extends SceneTree
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
var _facility_id: StringName
var _facility_inventory_id: StringName
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S3: integrated Guest and Staff runtime cycle")
_reset_runtime()
_build_cafeteria()
_register_staff_and_run_task()
_run_autonomous_guest_service_with_replay()
_run_long_horizon_replay()
_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() -> 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, 3303)
_clock.set_paused(true)
_expect(report[&"valid"], "runtime initializes S3 actor services")
_expect(_runtime.guest_service.get_definitions().size() == 35, "runtime preserves three legacy archetypes and registers 32 MVP catalog archetypes")
_expect(_runtime.guest_service.current_minute == _clock.absolute_minute, "Guest cadence starts aligned to the kernel")
_expect(_runtime.staff_service.current_minute == _clock.absolute_minute, "Staff cadence starts aligned to the kernel")
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 _build_cafeteria() -> void:
var build = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"cafeteria", &"origin": Vector3i(-5, 0, WorldService.LAYER_FRONT)},
&"player", &"", 50, -1, &"s3.runtime.build"
)
_expect(build.is_success(), "cafeteria construction command succeeds")
_clock.advance_minutes(180)
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
_expect(facilities.size() == 1, "construction completes before actor integration")
_facility_id = StringName(facilities[0][&"instance_id"])
var facility = _runtime.facility_service.get_instance(_facility_id)
_facility_inventory_id = facility.local_inventory_id
_expect(facility.state == FacilityInstance.State.NO_STAFF, "unassigned S3 facility starts in NO_STAFF")
func _register_staff_and_run_task() -> void:
var common_options := {
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"state": StaffRuntime.State.OFF_DUTY,
&"permitted_task_tags": [&"service", &"work"],
&"equipment_tags": [&"uniform"],
}
var cook_options: Dictionary = common_options.duplicate(true)
cook_options[&"role_tags"] = [&"cook"]
cook_options[&"skills"] = {&"cooking": 80, &"operation": 30}
var server_options: Dictionary = common_options.duplicate(true)
server_options[&"role_tags"] = [&"server"]
server_options[&"skills"] = {&"service": 75, &"operation": 20}
var cook = _runtime.submit_command(
&"register_staff",
{&"staff_id": &"cook_aya", &"role_id": &"cook", &"options": cook_options},
&"manager", &"cook_aya", 20, -1, &"s3.runtime.staff.cook"
)
var server = _runtime.submit_command(
&"register_staff",
{&"staff_id": &"server_ren", &"role_id": &"server", &"options": server_options},
&"manager", &"server_ren", 20, -1, &"s3.runtime.staff.server"
)
_expect(cook.is_success() and server.is_success(), "two StaffRuntime records register through commands")
var start_minute := posmod(_clock.absolute_minute, 1_440)
for staff_id: StringName in [&"cook_aya", &"server_ren"]:
var shift = _runtime.submit_command(
&"create_staff_shift",
{
&"staff_id": staff_id,
&"template_id": &"FLEX",
&"start_day": _clock.absolute_minute / 1_440,
&"start_minute_of_day": start_minute,
&"end_minute_of_day": start_minute + 600,
&"facility_id": _facility_id,
},
&"manager", staff_id, 20, -1, StringName("s3.runtime.shift.%s" % staff_id)
)
_expect(shift.is_success(), "Flex Shift registers for %s" % staff_id)
_clock.advance_one_minute()
var facility = _runtime.facility_service.get_instance(_facility_id)
_expect(facility.active_staff_ids == [&"cook_aya", &"server_ren"], "on-duty Staff project into FacilityService in stable ID order")
_expect(facility.state == FacilityInstance.State.OPEN, "required S3 staffing opens the cafeteria")
var tool = _runtime.submit_command(
&"add_inventory_stack",
{
&"owner_inventory_id": _facility_inventory_id,
&"item_id": &"tool/cleaning_kit",
&"quantity": 1,
},
&"manager", _facility_id, 10, -1, &"s3.runtime.tool"
)
_expect(tool.is_success(), "facility receives a reservable Staff task tool")
var task_result = _runtime.submit_command(
&"create_staff_task",
{
&"task_id": &"clean_cafeteria",
&"task_type": &"clean",
&"priority": 70,
&"options": {
&"target_entity_id": _facility_id,
&"target_cell": Vector3i(-1, 0, WorldService.LAYER_FRONT),
&"required_role_tags": [&"server"],
&"required_skill_min": {&"service": 10},
&"required_items": {&"tool/cleaning_kit": 1},
&"task_tags": [&"service"],
&"estimated_minutes": 2,
},
},
&"manager", _facility_id, 10, -1, &"s3.runtime.task"
)
_expect(task_result.is_success(), "Staff task enters the runtime dispatcher")
_clock.advance_minutes(8)
var task = _runtime.staff_service.get_task(&"clean_cafeteria")
_expect(task.state == StaffTask.State.COMPLETED, "runtime automatically dispatches, begins and completes the Staff task")
_expect(_runtime.inventory_service.available_quantity(_facility_inventory_id, &"tool/cleaning_kit") == 1, "completed task releases its atomic tool Reservation")
_expect(_runtime.staff_service.validate_invariants()[&"ok"], "integrated Staff task graph preserves bidirectional invariants")
func _run_autonomous_guest_service_with_replay() -> void:
var ingredient = _runtime.submit_command(
&"add_inventory_stack",
{
&"owner_inventory_id": _facility_inventory_id,
&"item_id": &"ingredient/basic_meal",
&"quantity": 1,
&"quality": 80,
},
&"manager", _facility_id, 10, -1, &"s3.runtime.ingredient"
)
_expect(ingredient.is_success(), "one service recipe is stocked")
var spawn = _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": &"guest_mio",
&"guest_type_id": &"traveler",
&"gold": 150,
&"options": {
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"needs": {&"hunger": 90, &"sleep": 20, &"hygiene": 20, &"medical": 0, &"leisure": 20, &"intimacy": 0},
&"planned_exit_at": _clock.absolute_minute + 240,
},
},
&"world", &"guest_mio", 10, -1, &"s3.runtime.guest"
)
_expect(spawn.is_success(), "spawn_guest atomically creates GuestRuntime and ledger wallet")
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:guest_mio") == 150, "Guest wallet is authoritative in EconomyService")
var active_service: ServiceInstance = null
for _minute in range(10):
_clock.advance_one_minute()
var active: Array = _runtime.facility_service.get_active_services(_facility_id)
if not active.is_empty():
active_service = active[0]
break
_expect(active_service != null, "autonomous Guest reaches the facility queue and starts service")
if active_service == null:
return
var guest = _runtime.guest_service.get_guest(&"guest_mio")
_expect(guest.state == GuestRuntime.State.RECEIVING_SERVICE, "Facility service start atomically updates Guest state")
_expect(guest.current_service_id == active_service.service_id, "Guest and Facility reference the same active service")
var mid_snapshot: Dictionary = _runtime.create_snapshot()
var mid_digest := SimulationDigest.compute(mid_snapshot)
var duration := active_service.duration_minutes
_clock.advance_minutes(duration)
var completed = _runtime.facility_service.get_service(active_service.service_id)
guest = _runtime.guest_service.get_guest(&"guest_mio")
_expect(completed != null and completed.state == ServiceInstance.State.COMPLETED, "service completes after its immutable duration")
_expect(guest != null and guest.satisfaction_samples == 1, "completion records exactly one Guest satisfaction sample")
_expect(guest != null and int(guest.needs[&"hunger"]) < 40, "cafeteria completion applies its Need relief")
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:guest_mio") == 115, "Guest pays the quoted service price once")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 4235, "service revenue reaches authoritative player cash")
_expect(_runtime.inventory_service.total_quantity(_facility_inventory_id, &"ingredient/basic_meal") == 0, "service consumes exactly one reserved recipe input")
var final_digest: String = _runtime.latest_digest
_expect(_runtime.restore_snapshot(mid_snapshot), "mid-service S3 snapshot restores")
_expect(_runtime.latest_digest == mid_digest, "S3 restore reproduces the exact mid-service digest")
_clock.advance_minutes(duration)
_expect(_runtime.latest_digest == final_digest, "replayed Guest, Staff, ledger and event transitions reproduce exactly")
_expect(_runtime.economy_service.get_entries().filter(
func(entry: LedgerEntry) -> bool: return entry.category == &"SERVICE_SALE"
).size() == 1, "replay records the service sale exactly once")
func _run_long_horizon_replay() -> void:
var horizon_snapshot: Dictionary = _runtime.create_snapshot()
_clock.advance_minutes(1_440)
var first_digest: String = _runtime.latest_digest
var first_snapshot: Dictionary = _runtime.create_snapshot()
_expect(_runtime.guest_service.validate_invariants()[&"valid"], "24-hour Guest graph has no orphan or illegal state")
_expect(_runtime.staff_service.validate_invariants()[&"ok"], "24-hour Staff graph has no orphan reservation")
_expect(_runtime.inventory_service.validate_invariants()[&"ok"], "24-hour actor simulation preserves Inventory invariants")
_expect(_runtime.economy_service.validate_ledger()[&"ok"], "24-hour actor simulation keeps the ledger balanced")
_expect(_runtime.restore_snapshot(horizon_snapshot), "long-horizon starting point restores")
_clock.advance_minutes(1_440)
_expect(_runtime.latest_digest == first_digest, "24-hour actor simulation is deterministic after restore")
_expect(_runtime.create_snapshot() == first_snapshot, "24-hour replay reproduces the complete authoritative snapshot")
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://0sc8xvrpbuoa
+415
View File
@@ -0,0 +1,415 @@
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("S3: runtime actor hardening regressions")
_test_service_affordability_is_atomic()
_test_guest_moves_one_cell_per_minute()
_test_unreachable_staff_task_is_not_assigned()
_test_shift_end_interrupts_and_fails_service()
_test_corrupt_cross_domain_snapshots_are_atomic()
_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) -> 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)
_clock.set_paused(true)
_expect(bool(report.get(&"valid", false)), "runtime initializes for hardening scenario %d" % seed)
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_service_affordability_is_atomic() -> void:
_reset_runtime(3311)
var facility: FacilityInstance = _build_cafeteria()
if facility == null:
return
_expect(_stock(facility.local_inventory_id, &"ingredient/basic_meal", 3, &"hardening.stock.affordability"), "affordability scenario stocks three meals")
_expect(_register_wallet(&"low_funds", 20, &"hardening.wallet.low"), "low-balance wallet registers")
var available_before: int = _runtime.inventory_service.available_quantity(
facility.local_inventory_id, &"ingredient/basic_meal"
)
var reservations_before: int = _runtime.inventory_service.get_reservations(true).size()
var low_result = _runtime.submit_command(
&"create_service_request",
{
&"request_id": &"request_low_funds",
&"facility_id": facility.instance_id,
&"guest_ids": [&"low_funds"],
&"service_type": &"default",
&"options": {&"quoted_price": 35},
},
&"low_funds", facility.instance_id, 10, -1, &"hardening.service.low"
)
_expect(not low_result.is_success(), "service request exceeding the wallet balance is rejected")
_expect(_runtime.facility_service.get_request(&"request_low_funds") == null, "rejected low-balance request leaves no Facility request")
_expect(_runtime.facility_service.get_queue(facility.instance_id).is_empty(), "rejected low-balance request leaves no queue entry")
_expect(
_runtime.inventory_service.available_quantity(facility.local_inventory_id, &"ingredient/basic_meal") == available_before,
"rejected low-balance request does not reserve stock"
)
_expect(_runtime.inventory_service.get_reservations(true).size() == reservations_before, "rejected low-balance request creates no active Reservation")
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:low_funds") == 20, "rejected low-balance request leaves the wallet unchanged")
_expect(_register_wallet(&"committed_guest", 50, &"hardening.wallet.commitment"), "commitment-test wallet registers")
var first_result = _runtime.submit_command(
&"create_service_request",
{
&"request_id": &"request_commitment_a",
&"facility_id": facility.instance_id,
&"guest_ids": [&"committed_guest"],
&"service_type": &"default",
&"options": {&"quoted_price": 30},
},
&"committed_guest", facility.instance_id, 10, -1, &"hardening.service.commitment.a"
)
_expect(first_result.is_success(), "first affordable service commitment is accepted")
var available_after_first: int = _runtime.inventory_service.available_quantity(
facility.local_inventory_id, &"ingredient/basic_meal"
)
var active_after_first: int = _runtime.inventory_service.get_reservations(true).size()
var second_result = _runtime.submit_command(
&"create_service_request",
{
&"request_id": &"request_commitment_b",
&"facility_id": facility.instance_id,
&"guest_ids": [&"committed_guest"],
&"service_type": &"default",
&"options": {&"quoted_price": 25},
},
&"committed_guest", facility.instance_id, 10, -1, &"hardening.service.commitment.b"
)
_expect(not second_result.is_success(), "combined service commitments exceeding the wallet are rejected")
var queue: Array = _runtime.facility_service.get_queue(facility.instance_id)
_expect(queue.size() == 1 and queue[0].request_id == &"request_commitment_a", "over-commit rejection preserves only the first queue entry")
_expect(_runtime.facility_service.get_request(&"request_commitment_b") == null, "over-commit rejection leaves no second request")
_expect(
_runtime.inventory_service.available_quantity(facility.local_inventory_id, &"ingredient/basic_meal") == available_after_first,
"over-commit rejection does not reserve another meal"
)
_expect(_runtime.inventory_service.get_reservations(true).size() == active_after_first, "over-commit rejection creates no additional Reservation")
_expect(_runtime.economy_service.get_balance(&"GUEST_WALLET:committed_guest") == 50, "queued commitments do not debit the wallet prematurely")
func _test_guest_moves_one_cell_per_minute() -> void:
_reset_runtime(3312)
var facility: FacilityInstance = _build_cafeteria()
if facility == null:
return
var opened = _runtime.submit_command(
&"set_facility_conditions",
{&"facility_id": facility.instance_id, &"conditions": {&"active_staff_ids": [&"cook_projection", &"server_projection"]}},
&"manager", facility.instance_id, 10, -1, &"hardening.guest.open"
)
_expect(opened.is_success() and facility.state == FacilityInstance.State.OPEN, "projected staffing opens the movement target")
_expect(_stock(facility.local_inventory_id, &"ingredient/basic_meal", 1, &"hardening.stock.path"), "movement target has a service input")
var spawn = _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": &"path_guest",
&"guest_type_id": &"traveler",
&"gold": 100,
&"options": {
&"current_cell": Vector3i(8, 0, WorldService.LAYER_FRONT),
&"needs": {&"hunger": 100, &"sleep": 0, &"hygiene": 0, &"medical": 0, &"leisure": 0, &"intimacy": 0},
&"planned_exit_at": _clock.absolute_minute + 240,
},
},
&"world", &"path_guest", 10, -1, &"hardening.guest.spawn"
)
_expect(spawn.is_success(), "distant Guest spawns for path progression")
var guest: GuestRuntime = null
for _minute in range(8):
_clock.advance_one_minute()
guest = _runtime.guest_service.get_guest(&"path_guest")
if guest != null and guest.state == GuestRuntime.State.MOVING:
break
_expect(guest != null and guest.state == GuestRuntime.State.MOVING, "autonomous Guest selects the reachable cafeteria")
if guest == null or guest.state != GuestRuntime.State.MOVING:
return
var target_cell: Vector3i = guest.path_handle.get(&"target_cell", guest.current_cell)
var planned_path: Array = _runtime.path_service.find_path(guest.current_cell, target_cell)
_expect(planned_path.size() >= 4, "movement regression uses a path longer than one minute")
var first_cell := guest.current_cell
_clock.advance_one_minute()
guest = _runtime.guest_service.get_guest(&"path_guest")
var second_cell := guest.current_cell
_expect(_cell_distance(first_cell, second_cell) == 1, "Guest advances exactly one graph cell in one minute")
_expect(second_cell != target_cell, "Guest does not teleport to a distant target in one minute")
_clock.advance_one_minute()
guest = _runtime.guest_service.get_guest(&"path_guest")
_expect(_cell_distance(second_cell, guest.current_cell) == 1, "Guest continues along the path at one cell per minute")
func _test_unreachable_staff_task_is_not_assigned() -> void:
_reset_runtime(3313)
var registered = _runtime.submit_command(
&"register_staff",
{
&"staff_id": &"isolated_worker",
&"role_id": &"worker",
&"options": {
&"state": StaffRuntime.State.AVAILABLE,
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"role_tags": [&"worker"],
&"permitted_task_tags": [&"work"],
&"skills": {&"operation": 50},
},
},
&"manager", &"isolated_worker", 10, -1, &"hardening.staff.unreachable.register"
)
_expect(registered.is_success(), "available Staff registers for unreachable-task test")
var created = _runtime.submit_command(
&"create_staff_task",
{
&"task_id": &"unreachable_task",
&"task_type": &"inspect",
&"priority": 95,
&"options": {
&"target_cell": Vector3i(0, 5, WorldService.LAYER_FRONT),
&"required_role_tags": [&"worker"],
&"required_skill_min": {&"operation": 10},
&"task_tags": [&"work"],
&"estimated_minutes": 5,
},
},
&"manager", &"unreachable_task", 10, -1, &"hardening.staff.unreachable.task"
)
_expect(created.is_success(), "unreachable StaffTask registers")
_clock.advance_one_minute()
var task: StaffTask = _runtime.staff_service.get_task(&"unreachable_task")
var staff: StaffRuntime = _runtime.staff_service.get_staff(&"isolated_worker")
_expect(task != null and task.state == StaffTask.State.OPEN, "unreachable StaffTask remains OPEN")
_expect(task != null and task.assigned_staff_id.is_empty(), "unreachable StaffTask has no assigned Staff")
_expect(staff != null and staff.current_task_id.is_empty(), "unreachable assignment does not reserve the Staff member")
var unassigned: Array = _runtime.staff_service.last_dispatch_report.get(&"unassigned", [])
_expect(not unassigned.is_empty() and unassigned[0].get(&"reason", &"") == &"NO_ELIGIBLE_STAFF", "dispatcher reports unreachable work as having no eligible Staff")
_expect(_runtime.inventory_service.get_reservations(true).is_empty(), "unreachable work creates no inventory Reservation")
func _test_shift_end_interrupts_and_fails_service() -> void:
_reset_runtime(3314)
var facility: FacilityInstance = _build_cafeteria()
if facility == null:
return
for staff_id: StringName in [&"short_cook", &"short_server"]:
var registered = _runtime.submit_command(
&"register_staff",
{
&"staff_id": staff_id,
&"role_id": &"worker",
&"options": {
&"state": StaffRuntime.State.OFF_DUTY,
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
},
},
&"manager", staff_id, 10, -1, StringName("hardening.shift.staff.%s" % staff_id)
)
_expect(registered.is_success(), "short-shift Staff %s registers" % staff_id)
var start_day: int = _clock.absolute_minute / 1_440
var start_minute: int = posmod(_clock.absolute_minute, 1_440)
var end_minute: int = start_minute + 5
for staff_id: StringName in [&"short_cook", &"short_server"]:
var shifted = _runtime.submit_command(
&"create_staff_shift",
{
&"shift_id": StringName("shift_%s" % staff_id),
&"staff_id": staff_id,
&"template_id": &"FLEX",
&"start_day": start_day,
&"start_minute_of_day": start_minute,
&"end_minute_of_day": end_minute,
&"facility_id": facility.instance_id,
},
&"manager", staff_id, 10, -1, StringName("hardening.shift.create.%s" % staff_id)
)
_expect(shifted.is_success(), "short Shift for %s registers" % staff_id)
_clock.advance_one_minute()
_expect(facility.active_staff_ids == [&"short_cook", &"short_server"], "active short-shift Staff project into the Facility")
_expect(facility.state == FacilityInstance.State.OPEN, "short-shift staffing opens the Facility")
_expect(_stock(facility.local_inventory_id, &"ingredient/basic_meal", 1, &"hardening.shift.stock"), "interruption scenario stocks one meal")
_expect(_register_wallet(&"shift_guest", 100, &"hardening.shift.wallet"), "service payer registers for Shift interruption")
var requested = _runtime.submit_command(
&"create_service_request",
{
&"request_id": &"shift_service_request",
&"facility_id": facility.instance_id,
&"guest_ids": [&"shift_guest"],
&"service_type": &"default",
&"options": {&"quoted_price": 35},
},
&"shift_guest", facility.instance_id, 10, -1, &"hardening.shift.service"
)
_expect(requested.is_success(), "service queues while short Shift is staffed")
_clock.advance_one_minute()
var active: Array = _runtime.facility_service.get_active_services(facility.instance_id)
_expect(active.size() == 1, "service starts before the short Shift ends")
if active.is_empty():
return
var service: ServiceInstance = active[0]
var service_id: StringName = service.service_id
var shift_end_absolute: int = start_day * 1_440 + end_minute
_clock.advance_minutes(maxi(shift_end_absolute - _clock.absolute_minute, 0))
_expect(facility.active_staff_ids.is_empty(), "Shift end removes all projected active Staff")
_expect(facility.state == FacilityInstance.State.NO_STAFF, "Shift end moves the Facility to NO_STAFF")
_clock.advance_one_minute()
service = _runtime.facility_service.get_service(service_id)
_expect(service != null and service.state == ServiceInstance.State.WAITING_FOR_STAFF, "active service waits for replacement Staff after Shift end")
var grace_deadline: int = service.staff_grace_deadline if service != null else _clock.absolute_minute
_expect(grace_deadline == _clock.absolute_minute + FacilityService.STAFF_REPLACEMENT_GRACE_MINUTES, "service records the deterministic Staff replacement grace deadline")
_clock.advance_minutes(maxi(grace_deadline - _clock.absolute_minute, 0))
service = _runtime.facility_service.get_service(service_id)
_expect(service != null and service.state == ServiceInstance.State.FAILED, "service fails when no replacement arrives before grace expiry")
_expect(service != null and service.failure_reason == FacilityService.INTERRUPTION_STAFF_UNAVAILABLE, "failed service preserves the Staff-unavailable reason")
_expect(_runtime.facility_service.get_active_services(facility.instance_id).is_empty(), "failed service leaves no active Facility slot")
_expect(_runtime.inventory_service.available_quantity(facility.local_inventory_id, &"ingredient/basic_meal") == 1, "failed service releases its meal Reservation")
func _test_corrupt_cross_domain_snapshots_are_atomic() -> void:
_reset_runtime(3315)
var registered = _runtime.submit_command(
&"register_staff",
{
&"staff_id": &"snapshot_worker",
&"role_id": &"worker",
&"options": {
&"state": StaffRuntime.State.AVAILABLE,
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"role_tags": [&"worker"],
&"permitted_task_tags": [&"work"],
},
},
&"manager", &"snapshot_worker", 10, -1, &"hardening.snapshot.staff"
)
_expect(registered.is_success(), "snapshot Staff registers")
_expect(_stock(&"staff.supplies", &"tool/snapshot_kit", 1, &"hardening.snapshot.stock"), "snapshot task tool is stocked")
var created = _runtime.submit_command(
&"create_staff_task",
{
&"task_id": &"snapshot_task",
&"task_type": &"inspect",
&"priority": 90,
&"options": {
&"target_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
&"required_role_tags": [&"worker"],
&"required_items": {&"tool/snapshot_kit": 1},
&"task_tags": [&"work"],
&"estimated_minutes": 10,
},
},
&"manager", &"snapshot_task", 10, -1, &"hardening.snapshot.task"
)
_expect(created.is_success(), "snapshot StaffTask registers")
_clock.advance_one_minute()
var task: StaffTask = _runtime.staff_service.get_task(&"snapshot_task")
_expect(task != null and task.state == StaffTask.State.WORKING and task.reservation_ids.size() == 1, "snapshot captures a working Task with an active Reservation")
var valid_snapshot: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(valid_snapshot), "valid hardening snapshot restores before corruption checks")
var before_corrupt: Dictionary = _runtime.create_snapshot()
var before_digest: String = SimulationDigest.compute(before_corrupt)
var minute_corrupt: Dictionary = before_corrupt.duplicate(true)
var runtime_data: Dictionary = minute_corrupt[&"systems"][&"runtime"]
runtime_data[&"facilities"][&"current_minute"] = _clock.absolute_minute + 1
_expect(not _runtime.restore_snapshot(minute_corrupt), "snapshot with FacilityService minute diverging from the clock is rejected")
_expect(_runtime.create_snapshot() == before_corrupt, "minute-mismatch restore rejection is fully atomic")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == before_digest, "minute-mismatch rejection preserves the live digest")
var reservation_corrupt: Dictionary = before_corrupt.duplicate(true)
var task_entries: Array = reservation_corrupt[&"systems"][&"runtime"][&"staff"][&"tasks"]
for raw_task: Dictionary in task_entries:
if StringName(raw_task.get(&"task_id", "")) == &"snapshot_task":
raw_task[&"reservation_ids"] = ["reservation_missing"]
_expect(not _runtime.restore_snapshot(reservation_corrupt), "snapshot with a dangling StaffTask Reservation reference is rejected")
_expect(_runtime.create_snapshot() == before_corrupt, "dangling-Reservation restore rejection is fully atomic")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == before_digest, "dangling-Reservation rejection preserves the live digest")
func _build_cafeteria() -> FacilityInstance:
var build = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"cafeteria", &"origin": Vector3i(-5, 0, WorldService.LAYER_FRONT)},
&"player", &"", 50, -1, StringName("hardening.build.%d" % _clock.absolute_minute)
)
_expect(build.is_success(), "cafeteria construction starts")
_clock.advance_minutes(180)
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
_expect(facilities.size() == 1, "cafeteria construction completes")
if facilities.size() != 1:
return null
var facility_id := StringName(facilities[0].get(&"instance_id", ""))
var facility: FacilityInstance = _runtime.facility_service.get_instance(facility_id)
_expect(facility != null, "completed cafeteria registers with FacilityService")
return facility
func _stock(
owner_inventory_id: StringName,
item_id: StringName,
quantity: int,
idempotency_key: StringName
) -> bool:
var result = _runtime.submit_command(
&"add_inventory_stack",
{
&"owner_inventory_id": owner_inventory_id,
&"item_id": item_id,
&"quantity": quantity,
},
&"manager", owner_inventory_id, 10, -1, idempotency_key
)
return result.is_success()
func _register_wallet(guest_id: StringName, gold: int, idempotency_key: StringName) -> bool:
var result = _runtime.submit_command(
&"register_guest_wallet",
{&"guest_id": guest_id, &"gold": gold},
&"manager", guest_id, 10, -1, idempotency_key
)
return result.is_success()
func _cell_distance(a: Vector3i, b: Vector3i) -> int:
var delta := a - b
return absi(delta.x) + absi(delta.y) + absi(delta.z)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://bx62i83nwxfbh
+619
View File
@@ -0,0 +1,619 @@
extends SceneTree
const POST_V2_RUNTIME_KEYS := [
&"movement_credits",
&"catalog_manifest",
&"progression",
&"hired_staff_candidate_ids",
&"operations",
&"excavation",
&"spatial_subslots",
&"route_projection",
&"facility_infrastructure",
]
const POST_V2_EVENT_TYPES := [
&"PATH_ACTOR_STUCK",
&"WORLD_TREE_RANK_ADVANCED",
&"STAFF_CANDIDATE_HIRED",
&"GUEST_AUTO_ARRIVED",
&"FACILITY_INFRASTRUCTURE_PROJECTED",
&"EXCAVATION_COMPLETED",
]
const POST_V2_COMMAND_TYPES := [
&"set_world_tree_rank",
&"update_progression_metrics",
&"complete_progression_quest",
&"hire_staff_candidate",
&"acknowledge_operations_pause",
&"start_excavation",
&"pause_excavation",
&"resume_excavation",
]
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("S3: Runtime Path traffic integration")
_test_namespaced_lifecycle_and_density()
_test_congestion_credit_and_alternate_route()
_test_occupied_cell_graph_rebuild_is_rejected()
_test_stuck_guest_and_staff_recovery()
_test_event_recovery_path_transaction()
_test_v6_snapshot_crosslinks_and_legacy_v2()
_test_dispatcher_request_ownership_crosslinks()
_test_twenty_four_hour_projection_integrity()
_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) -> 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)
_clock.set_paused(true)
_expect(bool(report.get(&"valid", false)), "Runtime initializes for Path traffic fixture %d" % seed)
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_namespaced_lifecycle_and_density() -> void:
_reset_runtime(8_101)
var cell := Vector3i(-4, 0, WorldService.LAYER_FRONT)
_expect(_spawn_moving_guest(&"shared", cell, Vector3i(4, 0, 0)), "Guest with shared raw id spawns")
_expect(_register_idle_staff(&"shared", cell), "Staff with the same raw id registers")
_expect(
_runtime.path_service.has_actor_projection(&"guest:shared")
and _runtime.path_service.has_actor_projection(&"staff:shared"),
"Guest and Staff use collision-free namespaced Path actor ids"
)
_expect(
_runtime.path_service.get_current_density(cell) == 2,
"spawn/register projects both actors into the authoritative cell density"
)
var guest: GuestRuntime = _runtime.guest_service.get_guest(&"shared")
_runtime.guest_service.begin_leaving(&"shared", _clock.absolute_minute, &"TEST_EXIT")
_runtime.guest_service.despawn_guest(&"shared", _clock.absolute_minute)
_expect(guest != null and not _runtime.path_service.has_actor_projection(&"guest:shared"), "Guest despawn removes only its namespaced projection")
_expect(
_runtime.path_service.has_actor_projection(&"staff:shared")
and _runtime.path_service.get_current_density(cell) == 1,
"Guest despawn preserves the colliding raw Staff id and lowers density once"
)
_expect(_runtime.get_actor_movement_credit(&"guest:shared").is_empty(), "Guest despawn removes movement credit")
_runtime.staff_service.remove_staff(&"shared", &"TEST_REMOVE", true, _clock.absolute_minute)
_expect(
not _runtime.path_service.has_actor_projection(&"staff:shared")
and _runtime.path_service.get_current_density(cell) == 0,
"Staff removal cleans projection and density without going negative"
)
var invalid_spawn: CommandResult = _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": &"invalid_cell",
&"guest_type_id": &"traveler",
&"options": {&"current_cell": Vector3i(0, 2, 0)},
},
&"world", &"invalid_cell", 10, -1, &"path.invalid.spawn"
)
_expect(
not invalid_spawn.is_success()
and not _runtime.guest_service.has_guest(&"invalid_cell")
and not _runtime.path_service.has_actor_projection(&"guest:invalid_cell")
and _runtime.get_actor_movement_credit(&"guest:invalid_cell").is_empty()
and not _has_recent_event(&"GUEST_SPAWNED", &"invalid_cell"),
"failed spawn leaves Actor, Path, credit, and lifecycle events uncommitted"
)
func _test_congestion_credit_and_alternate_route() -> void:
_reset_runtime(8_102)
var origin := Vector3i(-8, 0, WorldService.LAYER_FRONT)
var target := Vector3i(8, 0, WorldService.LAYER_FRONT)
_runtime.path_service.configure_cell_traffic(origin, 1, 0, false)
for index in range(5):
_expect(
_spawn_moving_guest(StringName("slow_%02d" % index), origin, target),
"congestion Guest %d spawns" % index
)
_expect(_runtime.path_service.get_current_density(origin) == 5, "five same-route actors create over-capacity density")
_clock.advance_one_minute()
var after_one: Array[Vector3i] = _guest_cells_with_prefix("slow_")
_expect(after_one.all(func(cell: Vector3i) -> bool: return cell == origin), "over-capacity speed credit prevents a first-minute burst")
_expect(
int(_runtime.get_actor_movement_credit(&"guest:slow_00").get(&"credit", -1)) == 250,
"Path speed multiplier is stored as deterministic fixed-point credit"
)
_clock.advance_minutes(3)
var after_four: Array[Vector3i] = _guest_cells_with_prefix("slow_")
_expect(after_four.all(func(cell: Vector3i) -> bool: return cell != origin), "minimum speed guarantees progress by the fourth congested minute")
_expect(_runtime.path_service.get_current_density(origin) == 0, "sequential committed movement lowers source density exactly")
_reset_runtime(8_103)
for x in range(0, 3):
_runtime.world_service.set_walkable(Vector3i(x, 1, WorldService.LAYER_FRONT), true)
_expect(_runtime.rebuild_path_graph_from_world(), "parallel walkable lane rebuilds into PathService")
var congested := Vector3i(1, 0, WorldService.LAYER_FRONT)
_runtime.path_service.configure_cell_traffic(congested, 1, 0, false)
for blocker_index in range(4):
_expect(
_register_idle_staff(StringName("blocker_%d" % blocker_index), congested),
"alternate-route blocker %d registers" % blocker_index
)
_expect(
_spawn_moving_guest(&"route_picker", Vector3i(0, 0, 0), Vector3i(2, 0, 0)),
"route-selection Guest spawns"
)
var planned: Array[Vector3i] = _runtime.path_service.find_path(
Vector3i(0, 0, 0), Vector3i(2, 0, 0)
)
_expect(
planned.size() > 1 and planned[1] == Vector3i(0, 1, 0),
"congestion-aware A* selects the parallel lane before Runtime movement (%s)" % [planned]
)
_clock.advance_one_minute()
var picker: GuestRuntime = _runtime.guest_service.get_guest(&"route_picker")
_expect(
picker != null and picker.current_cell == Vector3i(0, 1, 0),
"live congestion makes Runtime choose the cheaper parallel lane (actual %s)" % (
picker.current_cell if picker != null else "missing"
)
)
_expect(
_runtime.path_service.get_actor_projected_cell(&"guest:route_picker") == picker.current_cell,
"alternate-route movement commits Actor and Path projection together"
)
func _test_occupied_cell_graph_rebuild_is_rejected() -> void:
_reset_runtime(8_110)
var occupied := Vector3i(0, 0, WorldService.LAYER_FRONT)
_expect(_register_idle_staff(&"rebuild_guard", occupied), "graph-rebuild guard fixture registers")
var path_before: Dictionary = _runtime.path_service.create_snapshot()
_expect(_runtime.world_service.set_walkable(occupied, false), "fixture removes an occupied World cell")
_expect(
not _runtime.rebuild_path_graph_from_world(),
"Runtime rejects a graph rebuild that would evict a live actor projection"
)
_expect(
_runtime.path_service.create_snapshot() == path_before
and _runtime.path_service.get_actor_projected_cell(&"staff:rebuild_guard") == occupied
and not _runtime.get_actor_movement_credit(&"staff:rebuild_guard").is_empty(),
"rejected graph rebuild preserves Path projection, progress, density, and credit"
)
_expect(_runtime.world_service.set_walkable(occupied, true), "fixture restores the occupied World cell")
_expect(_runtime.rebuild_path_graph_from_world(), "safe topology rebuild succeeds after World repair")
func _test_stuck_guest_and_staff_recovery() -> void:
_reset_runtime(8_104)
_expect(
_spawn_moving_guest(&"stuck_guest", Vector3i(0, 0, 0), Vector3i(2, 0, 0)),
"stuck Guest fixture spawns"
)
_runtime.path_service.set_cell_temporary_block(Vector3i(1, 0, 0), true)
_clock.advance_minutes(PathService.STUCK_THRESHOLD_MINUTES - 1)
var guest: GuestRuntime = _runtime.guest_service.get_guest(&"stuck_guest")
_expect(guest != null and guest.state == GuestRuntime.State.MOVING, "Guest remains in movement recovery window before 30 minutes")
_clock.advance_one_minute()
guest = _runtime.guest_service.get_guest(&"stuck_guest")
_expect(guest != null and guest.state == GuestRuntime.State.LEAVING, "30-minute Path report enters existing Guest safe recovery")
_expect(
guest != null and guest.current_cell == Vector3i.ZERO,
"stuck recovery proposes a safe target without implicit teleport"
)
_expect(_has_recent_event(&"PATH_ACTOR_STUCK", &"guest:stuck_guest"), "Guest stuck recovery emits a structured Path DomainEvent")
_reset_runtime(8_105)
_expect(_register_idle_staff(&"stuck_staff", Vector3i(0, 0, 0)), "stuck Staff fixture registers")
var created: CommandResult = _runtime.staff_service.create_task(
&"inspect", 90, _clock.absolute_minute,
{&"target_cell": Vector3i(2, 0, 0), &"estimated_minutes": 5},
&"stuck_task"
)
var task: StaffTask = _runtime.staff_service.get_task(&"stuck_task")
var member: StaffRuntime = _runtime.staff_service.get_staff(&"stuck_staff")
var reserved: bool = created.is_success() \
and task.reserve(&"stuck_staff", [], _clock.absolute_minute) \
and member.reserve_task(&"stuck_task", _clock.absolute_minute) \
and task.begin_moving(_clock.absolute_minute)
_expect(reserved, "StaffTask enters a consistent MOVING ownership fixture")
_runtime.path_service.set_cell_temporary_block(Vector3i(1, 0, 0), true)
_clock.advance_minutes(PathService.STUCK_THRESHOLD_MINUTES)
task = _runtime.staff_service.get_task(&"stuck_task")
member = _runtime.staff_service.get_staff(&"stuck_staff")
_expect(task != null and task.state == StaffTask.State.CANCELLED, "30-minute Staff stall cancels its owned task safely")
_expect(member != null and member.current_task_id.is_empty(), "Staff stuck recovery releases task ownership")
_expect(_has_recent_event(&"PATH_ACTOR_STUCK", &"staff:stuck_staff"), "Staff stuck cancellation emits a structured Path DomainEvent")
func _test_event_recovery_path_transaction() -> void:
_reset_runtime(8_109)
_expect(
_spawn_moving_guest(&"event_mover", Vector3i(-2, 0, 0), Vector3i(2, 0, 0)),
"Event recovery movement fixture spawns"
)
var actor_before: Dictionary = _runtime.guest_service.create_snapshot()
var path_before: Dictionary = _runtime.path_service.create_snapshot()
var credit_before: Dictionary = _runtime.get_actor_movement_credit(&"guest:event_mover")
var events_before: Dictionary = _runtime.domain_events.create_snapshot()
var action: EventAction = EventAction.new().configure(
&"recovery_move",
EventAction.Type.TELEPORT_ENTITY_FOR_RECOVERY,
{&"entity_id": &"event_mover", &"cell": Vector3i(-1, 0, 0)}
)
var applied: Dictionary = _runtime._execute_event_action_boundary(
action, &"apply", {&"event_instance_id": &"event.fixture"}, null
)
var moved: GuestRuntime = _runtime.guest_service.get_guest(&"event_mover")
_expect(
bool(applied.get(&"ok", false))
and moved.current_cell == Vector3i(-1, 0, 0)
and _runtime.path_service.get_actor_projected_cell(&"guest:event_mover") == moved.current_cell
and int(_runtime.get_actor_movement_credit(&"guest:event_mover")[&"credit"]) == 0,
"explicit Event recovery commits Actor, Path, and credit together"
)
var rolled_back: Dictionary = _runtime._execute_event_action_boundary(
action, &"rollback", {}, applied.get(&"token", null)
)
_expect(
bool(rolled_back.get(&"ok", false))
and _runtime.guest_service.create_snapshot() == actor_before
and _runtime.path_service.create_snapshot() == path_before
and _runtime.get_actor_movement_credit(&"guest:event_mover") == credit_before
and _runtime.domain_events.create_snapshot() == events_before,
"Event rollback restores Actor, Path, credit, and DomainEvents atomically"
)
func _test_v6_snapshot_crosslinks_and_legacy_v2() -> void:
_reset_runtime(8_106)
var origin := Vector3i(-3, 0, 0)
_runtime.path_service.configure_cell_traffic(origin, 1, 0, false)
for index in range(2):
_spawn_moving_guest(StringName("save_%d" % index), origin, Vector3i(3, 0, 0))
_clock.advance_one_minute()
var snapshot: Dictionary = _runtime.create_snapshot()
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
_expect(int(runtime_data[&"runtime_schema_version"]) == 6, "Runtime snapshot advertises schema v6")
_expect(not runtime_data[&"movement_credits"].is_empty(), "mid-move snapshot persists fixed-point movement credits")
var digest := SimulationDigest.compute(snapshot)
_expect(_runtime.restore_snapshot(snapshot), "valid mid-move v6 snapshot restores")
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == digest, "mid-move v6 save/load is exact")
var corruptions: Array[Dictionary] = []
var bad_actor := snapshot.duplicate(true)
bad_actor[&"systems"][&"runtime"][&"path"][&"actor_cells"][0][&"cell"] = Vector3i(0, 0, 0)
corruptions.append({&"label": "actor cell", &"snapshot": bad_actor})
var bad_density := snapshot.duplicate(true)
for cell_entry: Dictionary in bad_density[&"systems"][&"runtime"][&"path"][&"cell_traffic"]:
if cell_entry[&"cell"] == origin:
cell_entry[&"current_density"] = int(cell_entry[&"current_density"]) + 1
break
corruptions.append({&"label": "density", &"snapshot": bad_density})
var bad_progress := snapshot.duplicate(true)
bad_progress[&"systems"][&"runtime"][&"path"][&"actor_progress"][0][&"last_cell"] = Vector3i(1, 0, 0)
corruptions.append({&"label": "progress", &"snapshot": bad_progress})
var bad_credit := snapshot.duplicate(true)
bad_credit[&"systems"][&"runtime"][&"movement_credits"][0][&"credit"] = 1_000
corruptions.append({&"label": "movement credit range", &"snapshot": bad_credit})
var bad_credit_time := snapshot.duplicate(true)
bad_credit_time[&"systems"][&"runtime"][&"movement_credits"][0][&"updated_at"] = _clock.absolute_minute + 1
corruptions.append({&"label": "movement credit time", &"snapshot": bad_credit_time})
var missing_path_cell := snapshot.duplicate(true)
missing_path_cell[&"systems"][&"runtime"][&"path"][&"cell_traffic"].pop_back()
corruptions.append({&"label": "Path/world cell-set", &"snapshot": missing_path_cell})
for corruption: Dictionary in corruptions:
var live_before: Dictionary = _runtime.create_snapshot()
_expect(
not _runtime.restore_snapshot(corruption[&"snapshot"])
and _runtime.create_snapshot() == live_before,
"corrupt %s crosslink is rejected atomically" % corruption[&"label"]
)
var legacy_v2 := snapshot.duplicate(true)
var legacy_data: Dictionary = legacy_v2[&"systems"][&"runtime"]
legacy_data[&"runtime_schema_version"] = 2
_strip_post_v2_runtime_state(legacy_data)
legacy_data[&"path"][&"actor_cells"] = []
legacy_data[&"path"][&"actor_progress"] = []
for traffic: Dictionary in legacy_data[&"path"][&"cell_traffic"]:
traffic[&"current_density"] = 0
if traffic[&"cell"] == origin:
traffic[&"temporary_block"] = true
_expect(
_is_true_runtime_v2_shape(legacy_data),
"legacy fixture removes every v3-v6 aggregate and dedicated history instead of relabelling v6"
)
_expect(_runtime.restore_snapshot(legacy_v2), "legacy Runtime v2 deterministically rebuilds Path actors and credits")
var migrated: Dictionary = _runtime.create_snapshot()
_expect(
int(migrated[&"systems"][&"runtime"][&"runtime_schema_version"]) == 6
and migrated[&"systems"][&"runtime"][&"path"][&"actor_cells"].size() == 2
and migrated[&"systems"][&"runtime"][&"movement_credits"].size() == 2
and bool(_runtime.path_service.get_cell_traffic(origin)[&"temporary_block"]),
"legacy migration emits canonical v6 ownership while preserving blocked traffic state"
)
func _test_dispatcher_request_ownership_crosslinks() -> void:
_reset_runtime(8_107)
var stock: CommandResult = _runtime.submit_command(
&"add_inventory_stack",
{&"owner_inventory_id": &"supplier", &"item_id": &"item/ore", &"quantity": 2, &"unit_weight": 3},
&"manager", &"supplier", 10, -1, &"path.runtime.dispatch.stock"
)
_expect(stock.is_success(), "dispatcher crosslink fixture stocks weighted items")
var requested: CommandResult = _runtime.submit_command(
&"create_logistics_request",
{
&"request_id": &"ownership_request",
&"source_inventory_id": &"supplier",
&"destination_inventory_id": &"destination",
&"item_requirements": {&"item/ore": 2},
},
&"manager", &"ownership_request", 10, -1, &"path.runtime.dispatch.request"
)
_expect(requested.is_success(), "dispatcher crosslink fixture creates a reserved request")
var dispatched: CommandResult = _runtime.submit_command(
&"dispatch_logistics",
{&"carriers": {
&"carrier_a": {&"capacity": 10, &"current_load": 0, &"path_minutes": 1},
}},
&"manager", &"ownership_request", 10, -1, &"path.runtime.dispatch.assign"
)
_expect(dispatched.is_success(), "dispatcher assigns request-ID ownership")
var snapshot: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(snapshot), "active dispatcher ownership snapshot restores")
var ownership_tamper: Dictionary = snapshot.duplicate(true)
ownership_tamper[&"systems"][&"runtime"][&"logistics_dispatcher"][&"assignment_ownership"][0][&"carrier_id"] = "carrier_b"
var live_before: Dictionary = _runtime.create_snapshot()
_expect(
not _runtime.restore_snapshot(ownership_tamper) and _runtime.create_snapshot() == live_before,
"request-level carrier tamper is rejected despite unchanged aggregate load"
)
var coherent_dispatcher_tamper: Dictionary = snapshot.duplicate(true)
var tampered_dispatcher: Dictionary = coherent_dispatcher_tamper[&"systems"][&"runtime"][&"logistics_dispatcher"]
tampered_dispatcher[&"assignment_ownership"][0][&"carrier_id"] = "carrier_b"
tampered_dispatcher[&"carrier_loads"][0][&"carrier_id"] = "carrier_b"
tampered_dispatcher[&"assignment_history"][0][&"carrier_id"] = "carrier_b"
_expect(
not _runtime.restore_snapshot(coherent_dispatcher_tamper)
and _runtime.create_snapshot() == live_before,
"Runtime rejects internally coherent Dispatcher carrier ownership that conflicts with its request"
)
var state_tamper: Dictionary = snapshot.duplicate(true)
state_tamper[&"systems"][&"runtime"][&"logistics_requests"][0][&"state"] = "COMPLETED"
_expect(
not _runtime.restore_snapshot(state_tamper) and _runtime.create_snapshot() == live_before,
"terminal-state tamper cannot retain active Dispatcher ownership"
)
var history_tamper: Dictionary = snapshot.duplicate(true)
history_tamper[&"systems"][&"runtime"][&"logistics_dispatcher"][&"assignment_history"][0][&"weight"] = 3
_expect(
not _runtime.restore_snapshot(history_tamper) and _runtime.create_snapshot() == live_before,
"request-level history weight tamper is rejected atomically"
)
func _test_twenty_four_hour_projection_integrity() -> void:
_reset_runtime(8_108)
_expect(_register_idle_staff(&"day_guard", Vector3i(0, 0, 0)), "24-hour Staff projection registers")
_clock.advance_minutes(1_440)
var snapshot: Dictionary = _runtime.create_snapshot()
_expect(
_runtime.path_service.has_actor_projection(&"staff:day_guard")
and _runtime.path_service.get_actor_projected_cell(&"staff:day_guard") == Vector3i.ZERO,
"24-hour idle processing leaves no Path actor orphan"
)
_expect(_runtime.restore_snapshot(snapshot), "24-hour Path/credit snapshot passes strict crosslink restore")
_expect(_runtime.create_snapshot() == snapshot, "24-hour Path traffic state restores exactly")
func _spawn_moving_guest(guest_id: StringName, cell: Vector3i, target: Vector3i) -> bool:
var result: CommandResult = _runtime.submit_command(
&"spawn_guest",
{
&"guest_id": guest_id,
&"guest_type_id": &"traveler",
&"gold": 100,
&"options": {
&"current_cell": cell,
&"state": GuestRuntime.State.MOVING,
&"state_entered_at": _clock.absolute_minute - 1,
&"planned_exit_at": _clock.absolute_minute + 2_000,
&"path_handle": {&"target_cell": target},
},
},
&"world", guest_id, 10, -1, StringName("path.spawn.%s" % guest_id)
)
return result.is_success()
func _register_idle_staff(staff_id: StringName, cell: Vector3i) -> bool:
var result: CommandResult = _runtime.submit_command(
&"register_staff",
{
&"staff_id": staff_id,
&"role_id": &"worker",
&"options": {
&"state": StaffRuntime.State.AVAILABLE,
&"current_cell": cell,
&"role_tags": [&"worker"],
&"permitted_task_tags": [&"work"],
},
},
&"manager", staff_id, 10, -1, StringName("path.staff.%s" % staff_id)
)
return result.is_success()
func _guest_cells_with_prefix(prefix: String) -> Array[Vector3i]:
var cells: Array[Vector3i] = []
for guest: GuestRuntime in _runtime.guest_service.get_guests():
if String(guest.guest_id).begins_with(prefix):
cells.append(guest.current_cell)
return cells
func _has_recent_event(event_type: StringName, actor_id: StringName) -> bool:
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if event.event_type == event_type and actor_id in event.entity_ids:
return true
return false
func _strip_post_v2_runtime_state(runtime_data: Dictionary) -> void:
for key: StringName in POST_V2_RUNTIME_KEYS:
runtime_data.erase(key)
runtime_data.erase(String(key))
_strip_post_v2_event_history(runtime_data)
_strip_post_v2_pending_commands(runtime_data)
func _strip_post_v2_event_history(runtime_data: Dictionary) -> void:
var stream_value: Variant = runtime_data.get(&"domain_events", null)
if not stream_value is Dictionary:
return
var stream: Dictionary = stream_value
var recent_value: Variant = stream.get("recent_events", null)
var published_value: Variant = stream.get("published_ids", null)
if not recent_value is Array or not published_value is Array:
return
var removed_event_ids: Dictionary = {}
var retained_events: Array = []
for raw_event: Variant in recent_value:
if raw_event is Dictionary and StringName(
(raw_event as Dictionary).get("event_type", "")
) in POST_V2_EVENT_TYPES:
removed_event_ids[StringName((raw_event as Dictionary).get("event_id", ""))] = true
continue
retained_events.append(raw_event)
var retained_published_ids: Array = []
for raw_event_id: Variant in published_value:
if not removed_event_ids.has(StringName(raw_event_id)):
retained_published_ids.append(raw_event_id)
stream["recent_events"] = retained_events
stream["published_ids"] = retained_published_ids
func _strip_post_v2_pending_commands(runtime_data: Dictionary) -> void:
var queue_value: Variant = runtime_data.get(&"command_queue", null)
if not queue_value is Dictionary:
return
var queue: Dictionary = queue_value
var removed_command_ids: Dictionary = {}
for queue_key: String in ["pending", "in_flight"]:
var commands_value: Variant = queue.get(queue_key, null)
if not commands_value is Array:
continue
var retained_commands: Array = []
for raw_command: Variant in commands_value:
if raw_command is Dictionary and StringName(
(raw_command as Dictionary).get("command_type", "")
) in POST_V2_COMMAND_TYPES:
removed_command_ids[int((raw_command as Dictionary).get("command_id", -1))] = true
continue
retained_commands.append(raw_command)
queue[queue_key] = retained_commands
if removed_command_ids.is_empty():
return
var accepted_value: Variant = queue.get("accepted", null)
if accepted_value is Array:
var retained_accepted: Array = []
for raw_metadata: Variant in accepted_value:
if not raw_metadata is Dictionary or not removed_command_ids.has(
int((raw_metadata as Dictionary).get("command_id", -1))
):
retained_accepted.append(raw_metadata)
queue["accepted"] = retained_accepted
var idempotency_value: Variant = queue.get("idempotency", null)
if idempotency_value is Dictionary:
var idempotency: Dictionary = idempotency_value
for key: Variant in idempotency.keys():
if removed_command_ids.has(int(idempotency[key])):
idempotency.erase(key)
func _is_true_runtime_v2_shape(runtime_data: Dictionary) -> bool:
if typeof(runtime_data.get(&"runtime_schema_version", null)) != TYPE_INT \
or int(runtime_data[&"runtime_schema_version"]) != 2:
return false
for key: StringName in POST_V2_RUNTIME_KEYS:
if runtime_data.has(key) or runtime_data.has(String(key)):
return false
var stream_value: Variant = runtime_data.get(&"domain_events", null)
if not stream_value is Dictionary:
return false
var recent_value: Variant = (stream_value as Dictionary).get("recent_events", null)
if not recent_value is Array:
return false
for raw_event: Variant in recent_value:
if raw_event is Dictionary and StringName(
(raw_event as Dictionary).get("event_type", "")
) in POST_V2_EVENT_TYPES:
return false
var queue_value: Variant = runtime_data.get(&"command_queue", null)
if not queue_value is Dictionary:
return false
for queue_key: String in ["pending", "in_flight"]:
var commands_value: Variant = (queue_value as Dictionary).get(queue_key, null)
if not commands_value is Array:
return false
for raw_command: Variant in commands_value:
if raw_command is Dictionary and StringName(
(raw_command as Dictionary).get("command_type", "")
) in POST_V2_COMMAND_TYPES:
return false
var staff_value: Variant = runtime_data.get(&"staff", null)
if staff_value is Dictionary:
var payroll_value: Variant = (staff_value as Dictionary).get(
&"payroll_settlements", []
)
if payroll_value is Array and not payroll_value.is_empty():
return false
var economy_value: Variant = runtime_data.get(&"economy", null)
if economy_value is Dictionary:
var entries_value: Variant = (economy_value as Dictionary).get(&"entries", [])
if entries_value is Array:
for raw_entry: Variant in entries_value:
if raw_entry is Dictionary and StringName(
(raw_entry as Dictionary).get(&"category", "")
) == &"WAGE":
return false
return true
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://bgrelqd3fwfkc
+283
View File
@@ -0,0 +1,283 @@
extends SceneTree
const StaffRuntimeType = preload("res://scripts/staff/staff_runtime.gd")
const StaffTaskType = preload("res://scripts/staff/staff_task.gd")
const StaffServiceType = preload("res://scripts/staff/staff_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S3: Staff payroll Operations boundary")
_test_preview_and_settlement_contract()
_test_employment_effective_minute_boundary()
_test_replay_conflict_and_atomic_validation()
_test_snapshot_contract()
_test_service_owned_task_cadence()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_preview_and_settlement_contract() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"z_active", 40, 0))
service.register_staff(_staff(&"a_active", 100, 20))
service.register_staff(_staff(&"m_departed", 70, 90))
service.remove_staff(&"m_departed", &"LEFT", true, 0)
var preview := service.preview_daily_payroll(1)
_expect(preview[&"ok"], "positive integer day previews")
_expect(
preview[&"rows"].map(func(row: Dictionary) -> StringName: return row[&"staff_id"]) \
== [&"a_active", &"m_departed", &"z_active"],
"preview rows are globally lexical"
)
_expect(preview[&"rows"][0][&"prior_arrears"] == 20 and preview[&"rows"][0][&"new_wages"] == 100, "active row separates arrears from today's wage")
_expect(preview[&"rows"][1][&"new_wages"] == 0 and preview[&"rows"][1][&"total_due"] == 90, "departed row carries debt without new wage")
_expect(preview[&"new_wages_total"] == 140, "Operations receives exact new-wage expense")
_expect(preview[&"prior_arrears_total"] == 110, "Operations receives exact carried arrears")
_expect(preview[&"total_due"] == 250, "Operations receives exact total obligation")
var applied = service.apply_daily_payroll_settlement(
&"operations.payroll.1", 1, {&"a_active": 120, &"m_departed": 30, &"z_active": 0}
)
_expect(applied.is_success() and not applied.details[&"replayed"], "settlement commits")
_expect(applied.details[&"paid_total"] == 150, "settlement reports actual cash allocation independently")
_expect(applied.details[&"unpaid_after_total"] == 100, "settlement reports remaining liability")
_expect(service.get_staff(&"a_active").unpaid_wages == 0, "full active payment clears balance")
_expect(service.get_staff(&"z_active").unpaid_wages == 40, "zero active payment becomes arrears")
_expect(service.get_departed_unpaid_wages(&"m_departed") == 60, "partial departed payment preserves remainder")
var settlement_copy := service.get_payroll_settlement(&"operations.payroll.1")
settlement_copy[&"paid_total"] = 999
_expect(service.get_payroll_settlement(&"operations.payroll.1")[&"paid_total"] == 150, "settlement query returns a defensive deep copy")
_expect(service.validate_invariants()[&"ok"], "committed settlement history satisfies Staff invariants")
var day_two := service.preview_daily_payroll(2)
_expect(day_two[&"new_wages_total"] == 140 and day_two[&"prior_arrears_total"] == 100, "next day accrues once on top of exact arrears")
service.apply_daily_payroll_settlement(
&"operations.payroll.2", 2, {&"a_active": 50, &"m_departed": 60, &"z_active": 80}
)
_expect(service.get_departed_staff_ids().is_empty(), "departed liability disappears after full paydown")
_expect(service.get_staff(&"a_active").unpaid_wages == 50, "partial next-day active payment is retained")
service.remove_staff(&"a_active", &"LEFT", true, 2)
_expect(service.get_departed_unpaid_wages(&"a_active") == 50, "departure transfers active arrears")
_expect(service.preview_daily_payroll(3)[&"rows"][0][&"new_wages"] == 0, "transferred departed debt never accrues wage")
func _test_employment_effective_minute_boundary() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"before_boundary", 100, 0, 1_439))
service.register_staff(_staff(&"exact_boundary", 80, 35, 1_440))
service.register_staff(_staff(&"after_boundary", 60, 0, 1_441))
var day_one := service.preview_daily_payroll(1)
var rows_by_id: Dictionary = {}
for row: Dictionary in day_one[&"rows"]:
rows_by_id[row[&"staff_id"]] = row
_expect(day_one[&"ok"] and day_one[&"new_wages_total"] == 100, "day close accrues only hires strictly before its end")
_expect(rows_by_id[&"before_boundary"][&"new_wages"] == 100, "minute before boundary accrues on that day")
_expect(rows_by_id[&"exact_boundary"][&"new_wages"] == 0, "exact-boundary hire starts accruing next day")
_expect(rows_by_id[&"exact_boundary"][&"total_due"] == 35, "exact-boundary exclusion preserves prior arrears")
_expect(rows_by_id[&"after_boundary"][&"new_wages"] == 0, "post-boundary hire does not accrue early")
var settled = service.apply_daily_payroll_settlement(
&"effective.day.1", 1, {&"before_boundary": 100, &"exact_boundary": 10}
)
_expect(settled.is_success() and settled.details[&"new_wages_total"] == 100, "settlement uses the same effective-time boundary as preview")
_expect(service.get_staff(&"exact_boundary").unpaid_wages == 25, "settlement pays carried arrears without inventing a boundary-day wage")
var day_two := service.preview_daily_payroll(2)
_expect(day_two[&"new_wages_total"] == 240, "all three hires accrue at the following day close")
var invalid_service := StaffServiceType.new()
var string_minute := StaffRuntimeType.new().configure(&"string_minute", &"worker", {
&"daily_wage": 10,
&"employment_effective_minute": "1440",
})
var float_minute := StaffRuntimeType.new().configure(&"float_minute", &"worker", {
&"daily_wage": 10,
&"employment_effective_minute": 1_440.0,
})
var negative_minute := StaffRuntimeType.new().configure(&"negative_minute", &"worker", {
&"daily_wage": 10,
&"employment_effective_minute": -1,
})
_expect(not invalid_service.register_staff(string_minute).is_success(), "string employment effective option is rejected")
_expect(not invalid_service.register_staff(float_minute).is_success(), "floating employment effective option is rejected")
_expect(not invalid_service.register_staff(negative_minute).is_success(), "negative employment effective option is rejected")
_expect(invalid_service.get_staff_members().is_empty(), "invalid effective-time options never partially register")
func _test_replay_conflict_and_atomic_validation() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"worker", 100, 0))
var before := service.create_snapshot()
_expect(not service.preview_daily_payroll(0)[&"ok"], "zero day fails closed")
_expect(not service.preview_daily_payroll("1")[&"ok"], "string day fails closed")
_expect(not service.apply_daily_payroll_settlement(&"bad id", 1, {}).is_success(), "unstable settlement ID fails closed")
_expect(not service.apply_daily_payroll_settlement(&"not_map", 1, []).is_success(), "non-map allocation fails closed")
_expect(not service.apply_daily_payroll_settlement(&"string", 1, {&"worker": "1"}).is_success(), "string payment is not coerced")
_expect(not service.apply_daily_payroll_settlement(&"float", 1, {&"worker": 1.0}).is_success(), "float payment is not coerced")
_expect(not service.apply_daily_payroll_settlement(&"negative", 1, {&"worker": -1}).is_success(), "negative payment fails closed")
_expect(not service.apply_daily_payroll_settlement(&"over", 1, {&"worker": 101}).is_success(), "overpayment fails closed")
_expect(not service.apply_daily_payroll_settlement(&"unknown", 1, {&"ghost": 0}).is_success(), "unknown Staff ID fails closed")
_expect(service.create_snapshot() == before, "all rejected allocations preserve state atomically")
var first = service.apply_daily_payroll_settlement(&"stable.1", 1, {&"worker": 25})
var committed := service.create_snapshot()
_expect(first.is_success() and service.get_staff(&"worker").unpaid_wages == 75, "valid allocation commits once")
var replay = service.apply_daily_payroll_settlement(&"stable.1", 1, {&"worker": 25})
_expect(replay.is_success() and replay.details[&"replayed"], "same ID and fingerprint replays")
_expect(service.create_snapshot() == committed, "replay never applies wages twice")
_expect(not service.apply_daily_payroll_settlement(&"stable.1", 1, {&"worker": 24}).is_success(), "same ID rejects conflicting payment")
_expect(not service.apply_daily_payroll_settlement(&"stable.1", 2, {&"worker": 25}).is_success(), "same ID rejects conflicting day")
_expect(not service.apply_daily_payroll_settlement(&"different.1", 1, {}).is_success(), "same day rejects a second ID")
_expect(service.create_snapshot() == committed, "all settlement conflicts preserve committed state")
func _test_snapshot_contract() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"a", 100, 20))
service.register_staff(_staff(&"b", 50, 0))
service.apply_daily_payroll_settlement(&"snapshot.1", 1, {&"a": 30, &"b": 0})
service.apply_daily_payroll_settlement(&"snapshot.2", 2, {&"a": 50, &"b": 100})
var snapshot := service.create_snapshot()
var restored := StaffServiceType.new()
_expect(restored.restore_snapshot(snapshot), "v3 snapshot restores")
_expect(restored.create_snapshot() == snapshot, "v3 snapshot round trip is exact")
var before := restored.create_snapshot()
var fingerprint := snapshot.duplicate(true)
fingerprint[&"payroll_settlements"][0][&"fingerprint"] = "corrupt"
_expect(not restored.restore_snapshot(fingerprint), "derived fingerprint tamper is rejected")
_expect(restored.create_snapshot() == before, "fingerprint rejection is atomic")
var total := snapshot.duplicate(true)
total[&"payroll_settlements"][0][&"paid_total"] += 1
_expect(not restored.restore_snapshot(total), "derived total tamper is rejected")
_expect(restored.create_snapshot() == before, "total rejection is atomic")
var order := snapshot.duplicate(true)
order[&"payroll_settlements"].reverse()
_expect(not restored.restore_snapshot(order), "non-canonical settlement order is rejected")
_expect(restored.create_snapshot() == before, "order rejection is atomic")
var duplicate_day := snapshot.duplicate(true)
duplicate_day[&"payroll_settlements"][1][&"day"] = 1
_expect(not restored.restore_snapshot(duplicate_day), "duplicate settled day is rejected")
_expect(restored.create_snapshot() == before, "day uniqueness rejection is atomic")
var negative := snapshot.duplicate(true)
negative[&"payroll_settlements"][0][&"rows"][0][&"payment"] = -1
_expect(not restored.restore_snapshot(negative), "negative historical amount is rejected")
_expect(restored.create_snapshot() == before, "negative history rejection is atomic")
var unknown_field := snapshot.duplicate(true)
unknown_field[&"payroll_settlements"][0][&"extra"] = 1
_expect(not restored.restore_snapshot(unknown_field), "unknown current history field is rejected")
_expect(restored.create_snapshot() == before, "unknown field rejection is atomic")
var effective_type := snapshot.duplicate(true)
effective_type[&"staff"][0][&"employment_effective_minute"] = 1_440.0
_expect(not restored.restore_snapshot(effective_type), "floating employment effective minute is rejected")
_expect(restored.create_snapshot() == before, "effective-minute type rejection is atomic")
var effective_negative := snapshot.duplicate(true)
effective_negative[&"staff"][0][&"employment_effective_minute"] = -1
_expect(not restored.restore_snapshot(effective_negative), "negative employment effective minute is rejected")
_expect(restored.create_snapshot() == before, "negative effective-minute rejection is atomic")
var effective_missing := snapshot.duplicate(true)
effective_missing[&"staff"][0].erase(&"employment_effective_minute")
_expect(not restored.restore_snapshot(effective_missing), "current Staff snapshot rejects a missing employment effective minute")
_expect(restored.create_snapshot() == before, "missing effective-minute rejection is atomic")
var unversioned_current_row: Dictionary = snapshot[&"staff"][0].duplicate(true)
unversioned_current_row.erase(&"snapshot_version")
_expect(StaffRuntimeType.from_snapshot(unversioned_current_row) == null, "StaffRuntime rejects an effective minute without its row version")
var erased_current_row_schema := snapshot.duplicate(true)
erased_current_row_schema[&"staff"][0].erase(&"snapshot_version")
erased_current_row_schema[&"staff"][0].erase(&"employment_effective_minute")
_expect(not restored.restore_snapshot(erased_current_row_schema), "v3 rejects a Staff row disguised as pre-effective schema")
_expect(restored.create_snapshot() == before, "row-schema erasure rejection is atomic")
var effective_version := snapshot.duplicate(true)
effective_version[&"staff"][0][&"snapshot_version"] = 2
_expect(not restored.restore_snapshot(effective_version), "unknown Staff row snapshot version is rejected")
_expect(restored.create_snapshot() == before, "Staff row version rejection is atomic")
var outer_downgrade := snapshot.duplicate(true)
outer_downgrade[&"version"] = 2
_expect(not restored.restore_snapshot(outer_downgrade), "changing only the outer version cannot downgrade a v3 Staff snapshot")
_expect(restored.create_snapshot() == before, "outer version downgrade rejection is atomic")
var pre_effective_v2 := snapshot.duplicate(true)
pre_effective_v2[&"version"] = 2
for staff_snapshot: Dictionary in pre_effective_v2[&"staff"]:
staff_snapshot.erase(&"snapshot_version")
staff_snapshot.erase(&"employment_effective_minute")
var migrated_v2 := StaffServiceType.new()
_expect(migrated_v2.restore_snapshot(pre_effective_v2), "pre-effective-minute v2 Staff snapshot migrates")
_expect(migrated_v2.get_staff(&"a").employment_effective_minute == 0, "v2 legacy Staff employment starts at minute zero")
_expect(
migrated_v2.create_snapshot()[&"version"] == 3
and migrated_v2.create_snapshot()[&"staff"][0][&"snapshot_version"] == 1,
"v2 legacy Staff row is re-emitted in the current service and row schemas"
)
var legacy := snapshot.duplicate(true)
legacy[&"version"] = 1
legacy.erase(&"last_advanced_minute")
legacy.erase(&"payroll_settlements")
for staff_snapshot: Dictionary in legacy[&"staff"]:
staff_snapshot.erase(&"snapshot_version")
staff_snapshot.erase(&"employment_effective_minute")
var migrated := StaffServiceType.new()
_expect(migrated.restore_snapshot(legacy), "v1 snapshot migrates")
_expect(migrated.get_staff(&"a").unpaid_wages == 140, "v1 migration preserves active unpaid wages")
_expect(migrated.get_staff(&"a").employment_effective_minute == 0, "legacy Staff employment starts deterministically at minute zero")
_expect(migrated.create_snapshot()[&"payroll_settlements"].is_empty(), "v1 migration starts with empty settlement history")
func _test_service_owned_task_cadence() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"task_worker", 0, 0)
var task := StaffTaskType.new().configure(&"task", &"work", 50, 0, {
&"required_role_tags": [&"worker"],
&"required_skill_min": {&"operation": 1},
&"task_tags": [&"work"],
&"estimated_minutes": 2,
&"target_cell": Vector3i.ZERO,
})
service.register_staff(staff)
service.register_task(task)
service.dispatch_now(0)
service.advance_one_minute(1)
_expect(task.state == StaffTaskType.State.WORKING and task.progress_minutes == 0, "RESERVED begins without same-tick work")
_expect(not service.progress_task(task.task_id, 1, 1).is_success(), "same-minute explicit progress is blocked")
service.advance_one_minute(2)
_expect(task.progress_minutes == 1, "WORKING advances exactly one minute per tick")
_expect(not service.advance_one_minute(2)[&"ok"] and task.progress_minutes == 1, "duplicate service tick is idempotently rejected")
service.advance_one_minute(3)
_expect(task.state == StaffTaskType.State.COMPLETED, "automatic work completes exactly")
func _staff(
staff_id: StringName,
daily_wage: int,
unpaid_wages: int,
employment_effective_minute: int = 0
) -> StaffRuntime:
return StaffRuntimeType.new().configure(staff_id, &"worker", {
&"state": StaffRuntimeType.State.AVAILABLE,
&"role_tags": [&"worker"],
&"permitted_task_tags": [&"work"],
&"skills": {&"operation": 10},
&"daily_wage": daily_wage,
&"employment_effective_minute": employment_effective_minute,
&"unpaid_wages": unpaid_wages,
})
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://bnqa31rwr3wx7
+828
View File
@@ -0,0 +1,828 @@
extends SceneTree
const StaffRuntimeType = preload("res://scripts/staff/staff_runtime.gd")
const ShiftInstanceType = preload("res://scripts/staff/shift_instance.gd")
const StaffTaskType = preload("res://scripts/staff/staff_task.gd")
const TaskDispatcherType = preload("res://scripts/staff/task_dispatcher.gd")
const StaffServiceType = preload("res://scripts/staff/staff_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S3: Staff Shift and Task Dispatcher")
_test_night_shift_crosses_midnight()
_test_shift_state_machine_and_override()
_test_multiple_future_shifts_and_strict_overlap()
_test_deterministic_dispatch_and_pair_reservation()
_test_skill_score_and_tie_break()
_test_item_reservation_failure_is_atomic()
_test_interrupt_levels()
_test_fatigue_blocks_dangerous_tasks()
_test_missing_staff_reopens_task()
_test_task_progression()
_test_automatic_task_progression_once_per_tick()
_test_employment_and_daily_payroll_contract()
_test_retirement_preserves_unpaid_wages()
_test_daily_payroll_settlement_lifecycle()
_test_payroll_validation_and_idempotency()
_test_payroll_snapshot_migration_and_tamper()
_test_remove_staff_cleans_schedule_graph()
_test_snapshot_round_trip_and_atomic_restore()
_test_orphan_recovery_is_atomic()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_night_shift_crosses_midnight() -> void:
var shift := ShiftInstanceType.create_standard(
&"night_d2",
&"staff_night",
&"NIGHT",
2,
&"warehouse"
)
_expect(shift != null and shift.is_valid(), "standard NIGHT Shift is valid")
_expect(shift.start_day == 2 and shift.shift_instance_id == &"night_d2", "overnight Shift keeps its start-day identity")
_expect(shift.start_at == 3_960 and shift.end_at == 4_680, "NIGHT uses exact absolute-minute bounds")
_expect(shift.crosses_midnight(), "NIGHT is marked as crossing midnight")
_expect(shift.contains_minute(4_379), "same Shift contains time before calendar midnight")
_expect(shift.contains_minute(4_380), "same Shift contains time after calendar midnight")
_expect(not shift.contains_minute(4_680), "Shift end minute is exclusive")
shift.record_duty_minute(4_379)
shift.record_duty_minute(4_380)
_expect(shift.worked_minutes == 2, "work on both calendar dates aggregates into one Shift")
var restored := ShiftInstanceType.from_snapshot(shift.create_snapshot())
_expect(restored != null and restored.create_snapshot() == shift.create_snapshot(), "overnight Shift snapshot round trip is exact")
func _test_shift_state_machine_and_override() -> void:
var service := StaffServiceType.new()
var staff := _staff(
&"shift_worker",
0,
{&"state": StaffRuntimeType.State.OFF_DUTY}
)
_expect(service.register_staff(staff).is_success(), "Staff registers before scheduling")
_expect(
service.set_schedule_override(
staff.staff_id,
1,
{&"start_minute_of_day": 420, &"end_minute_of_day": 900}
).is_success(),
"one-day ScheduleOverride is stored separately"
)
_expect(
service.create_standard_shift(staff.staff_id, &"DAY", 1, &"reception", &"shift_override").is_success(),
"standard Shift instance accepts a one-day override"
)
var shift := service.get_shift(&"shift_override")
_expect(shift.start_minute_of_day == 420 and shift.end_minute_of_day == 900, "override changes only the concrete Shift times")
_expect(shift.template_id == &"DAY", "ScheduleOverride does not rewrite the base template ID")
service.advance_one_minute(1_830)
_expect(staff.state == StaffRuntimeType.State.COMMUTING, "Staff starts commuting exactly 30 minutes before Shift")
service.advance_one_minute(1_860)
_expect(staff.state == StaffRuntimeType.State.AVAILABLE, "Staff becomes available at Shift start")
_expect(staff.assigned_facility_id == &"reception", "Shift assigns the work facility")
service.advance_one_minute(2_340)
_expect(staff.state == StaffRuntimeType.State.OFF_DUTY and staff.shift_id.is_empty(), "Staff goes off duty at Shift end")
_expect(shift.actual_ended_at == 2_340, "Shift records its exact end transition")
func _test_multiple_future_shifts_and_strict_overlap() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"multi_shift_worker", 10, {&"state": StaffRuntimeType.State.OFF_DUTY})
_expect(service.register_staff(staff).is_success(), "Staff registers for multiple future Shifts")
_expect(
service.create_flex_shift(staff.staff_id, 1, 540, 600, &"workshop", &"shift_second").is_success(),
"later future Shift can be registered first"
)
_expect(
service.create_flex_shift(staff.staff_id, 1, 420, 480, &"reception", &"shift_first").is_success(),
"earlier non-overlapping Shift can be registered second"
)
_expect(staff.shift_id.is_empty(), "future Shift registration does not overwrite the active Shift pointer")
service.advance_one_minute(1_830)
_expect(
staff.shift_id == &"shift_first" and staff.state == StaffRuntimeType.State.COMMUTING,
"chronologically first Shift enters COMMUTING at its commute boundary"
)
service.advance_one_minute(1_860)
var first := service.get_shift(&"shift_first")
_expect(
staff.shift_id == first.shift_instance_id and staff.state == StaffRuntimeType.State.AVAILABLE,
"chronologically first Shift becomes ACTIVE at start"
)
_expect(first.actual_started_at == 1_860 and staff.assigned_facility_id == &"reception", "first Shift records start and facility")
service.advance_one_minute(1_920)
_expect(
first.actual_ended_at == 1_920 and staff.shift_id.is_empty() and staff.state == StaffRuntimeType.State.OFF_DUTY,
"first Shift becomes COMPLETED and releases its active pointer"
)
service.advance_one_minute(1_950)
_expect(
staff.shift_id == &"shift_second" and staff.state == StaffRuntimeType.State.COMMUTING,
"second future Shift becomes the active commute after the first completes"
)
service.advance_one_minute(1_980)
var second := service.get_shift(&"shift_second")
_expect(
staff.shift_id == second.shift_instance_id and staff.state == StaffRuntimeType.State.AVAILABLE,
"second Shift becomes ACTIVE in sequence"
)
service.advance_one_minute(2_040)
_expect(
second.actual_ended_at == 2_040 and staff.shift_id.is_empty() and staff.state == StaffRuntimeType.State.OFF_DUTY,
"second Shift becomes COMPLETED in sequence"
)
_expect(service.validate_invariants()[&"ok"], "multiple completed Shift records preserve invariants")
var overlap_service := StaffServiceType.new()
var overlap_staff := _staff(&"overlap_worker", 10, {&"state": StaffRuntimeType.State.OFF_DUTY})
overlap_service.register_staff(overlap_staff)
overlap_service.create_flex_shift(overlap_staff.staff_id, 2, 420, 480, &"a", &"base_shift")
_expect(
not overlap_service.create_flex_shift(overlap_staff.staff_id, 2, 400, 430, &"b", &"overlap_left").is_success(),
"overlap is rejected when the proposed Shift starts before the existing Shift"
)
_expect(
not overlap_service.create_flex_shift(overlap_staff.staff_id, 2, 470, 500, &"b", &"overlap_right").is_success(),
"overlap is rejected when the proposed Shift starts inside the existing Shift"
)
_expect(
not overlap_service.create_flex_shift(overlap_staff.staff_id, 2, 400, 500, &"b", &"overlap_enclosing").is_success(),
"overlap is rejected when the proposed Shift encloses the existing Shift"
)
_expect(
overlap_service.create_flex_shift(overlap_staff.staff_id, 2, 480, 510, &"b", &"adjacent_shift").is_success(),
"exactly adjacent half-open Shift intervals are accepted"
)
func _test_deterministic_dispatch_and_pair_reservation() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"staff_b", 10))
service.register_staff(_staff(&"staff_a", 10))
service.register_task(_task(&"task_b", 50, 100))
service.register_task(_task(&"task_a", 50, 100))
var report: Dictionary = service.dispatch_now(100)
_expect(report[&"assigned"].size() == 2, "dispatcher fills two eligible Staff reservations")
_expect(report[&"assigned"][0][&"task_id"] == &"task_a", "Task tie breaks lexically after priority and deadline")
_expect(report[&"assigned"][0][&"staff_id"] == &"staff_a", "Staff score tie breaks lexically")
_expect(report[&"assigned"][1][&"task_id"] == &"task_b", "second Task remains in deterministic order")
_expect(report[&"assigned"][1][&"staff_id"] == &"staff_b", "one Staff is not assigned twice in one dispatch pass")
var task_a := service.get_task(&"task_a")
var staff_a := service.get_staff(&"staff_a")
_expect(task_a.assigned_staff_id == staff_a.staff_id, "Task points to its reserved Staff")
_expect(staff_a.current_task_id == task_a.task_id, "Staff points back to its reserved Task")
_expect(service.validate_invariants()[&"ok"], "dispatcher preserves Task/Staff bidirectional invariant")
func _test_skill_score_and_tie_break() -> void:
var dispatcher := TaskDispatcherType.new()
var task := _task(&"skill_task", 40, 0, {
&"required_skill_min": {&"operation": 5},
})
var novice := _staff(&"novice", 5)
var expert := _staff(&"expert", 30)
var novice_score := dispatcher.score_candidate(task, novice, 2)
var expert_score := dispatcher.score_candidate(task, expert, 2)
_expect(expert_score > novice_score, "higher matching Skill raises StaffTaskScore")
_expect(
dispatcher.score_candidate(task, expert, 5) < dispatcher.score_candidate(task, expert, 2),
"longer travel lowers StaffTaskScore with integer-minute penalty"
)
expert.is_overtime = true
_expect(dispatcher.score_candidate(task, expert, 2) < expert_score, "overtime applies a deterministic score penalty")
func _test_item_reservation_failure_is_atomic() -> void:
var service := StaffServiceType.new()
var reserve_calls := [0]
service.configure_callbacks(
func(_task_value: StaffTask, _minute: int) -> Dictionary:
reserve_calls[0] += 1
return {&"ok": false, &"reason": &"NO_STOCK"},
func(_ids: Array[StringName], _task_value: StaffTask, _minute: int) -> bool:
return true
)
var staff := _staff(&"stock_worker", 10)
service.register_staff(staff)
var task := _task(&"stock_task", 80, 0, {&"required_items": {&"meal": 2}})
service.register_task(task)
var report: Dictionary = service.dispatch_now(0)
_expect(reserve_calls[0] == 1 and report[&"blocked"].size() == 1, "required items are reserved before Staff mutation")
_expect(task.state == StaffTaskType.State.BLOCKED and task.assigned_staff_id.is_empty(), "stock failure sends Task to BLOCKED without half reservation")
_expect(staff.state == StaffRuntimeType.State.AVAILABLE and staff.current_task_id.is_empty(), "stock failure leaves Staff fully available")
_expect(service.validate_invariants()[&"ok"], "failed item transaction preserves Staff invariants")
func _test_interrupt_levels() -> void:
var protected_service := StaffServiceType.new()
var protected_staff := _staff(&"protected_worker", 10)
protected_service.register_staff(protected_staff)
var protected_task := _task(&"protected_task", 20, 0, {&"interrupt_level": 0})
protected_service.register_task(protected_task)
protected_service.dispatch_now(0)
protected_service.begin_task(protected_task.task_id, 0)
var replacement := _task(&"replacement", 100, 1, {&"emergency": true})
protected_service.register_task(replacement)
var protected_report: Dictionary = protected_service.dispatch_now(1)
_expect(protected_report[&"assigned"].is_empty(), "interrupt level 0 rejects even emergency replacement")
_expect(protected_task.state == StaffTaskType.State.WORKING and protected_staff.current_task_id == protected_task.task_id, "non-interruptible Task/Staff pair remains untouched")
var emergency_service := StaffServiceType.new()
var emergency_staff := _staff(&"response_worker", 10)
emergency_service.register_staff(emergency_staff)
var level_one := _task(&"level_one", 20, 0, {&"interrupt_level": 1})
emergency_service.register_task(level_one)
emergency_service.dispatch_now(0)
emergency_service.begin_task(level_one.task_id, 0)
var normal := _task(&"normal_high", 100, 1, {&"emergency": false})
var emergency := _task(&"emergency", 99, 1, {&"emergency": true})
emergency_service.register_task(normal)
emergency_service.register_task(emergency)
var emergency_report: Dictionary = emergency_service.dispatch_now(1)
_expect(emergency_report[&"assigned"].size() == 1 and emergency_report[&"assigned"][0][&"task_id"] == &"emergency", "interrupt level 1 accepts only emergency Task")
_expect(level_one.state == StaffTaskType.State.OPEN and level_one.assigned_staff_id.is_empty(), "interrupted Task returns atomically to OPEN")
_expect(emergency_staff.current_task_id == emergency.task_id and emergency.assigned_staff_id == emergency_staff.staff_id, "replacement Task and Staff reserve together")
_expect(emergency_service.validate_invariants()[&"ok"], "interrupt transaction preserves pair invariants")
func _test_fatigue_blocks_dangerous_tasks() -> void:
var service := StaffServiceType.new()
var exhausted := _staff(&"exhausted", 20, {&"fatigue": 90})
service.register_staff(exhausted)
var dangerous := _task(&"dangerous", 100, 0, {&"dangerous": true})
var safe := _task(&"rest_break", 90, 0, {&"dangerous": false, &"task_tags": [&"rest"]})
service.register_task(dangerous)
service.register_task(safe)
var report: Dictionary = service.dispatch_now(0)
_expect(dangerous.state == StaffTaskType.State.OPEN and dangerous.assigned_staff_id.is_empty(), "fatigue 90 forbids dangerous Task assignment")
_expect(report[&"assigned"].size() == 1 and safe.assigned_staff_id == exhausted.staff_id, "fatigued Staff can still receive a safe rest Task")
_expect(exhausted.current_task_id == safe.task_id, "safe Task reserves the fatigued Staff atomically")
func _test_missing_staff_reopens_task() -> void:
var service := StaffServiceType.new()
var released_ids: Array[StringName] = []
service.configure_callbacks(
func(task: StaffTask, _minute: int) -> Dictionary:
return {&"ok": true, &"reservation_ids": [StringName("reserve_%s" % task.task_id)]},
func(ids: Array[StringName], _task_value: StaffTask, _minute: int) -> bool:
for reservation_id: StringName in ids:
released_ids.append(reservation_id)
return true
)
var staff := _staff(&"vanishing_staff", 10)
var task := _task(&"orphan_task", 60, 0, {&"required_items": {&"tool": 1}})
service.register_staff(staff)
service.register_task(task)
service.dispatch_now(0)
_expect(task.state == StaffTaskType.State.RESERVED and task.reservation_ids.size() == 1, "Task owns item Reservation after successful dispatch")
service._staff.erase(staff.staff_id)
var audit: Dictionary = service.audit_orphan_tasks(1)
_expect(audit[&"reopened_task_ids"] == [&"orphan_task"], "orphan audit reports vanished Staff Task")
_expect(task.state == StaffTaskType.State.OPEN and task.assigned_staff_id.is_empty(), "vanished Staff Task returns to OPEN")
_expect(task.reservation_ids.is_empty() and released_ids == [&"reserve_orphan_task"], "orphan recovery releases every item Reservation")
_expect(service.validate_invariants()[&"ok"], "orphan recovery restores all Staff invariants")
func _test_task_progression() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"progress_worker", 10)
var task := _task(&"progress_task", 50, 0, {&"estimated_minutes": 2})
service.register_staff(staff)
service.register_task(task)
service.dispatch_now(0)
_expect(service.begin_task(task.task_id, 1).is_success(), "reserved Task enters WORKING with its Staff")
var first = service.progress_task(task.task_id, 1, 2)
_expect(first.is_success() and not first.details[&"completed"], "Task remains WORKING before estimated duration")
var second = service.progress_task(task.task_id, 1, 3)
_expect(second.is_success() and second.details[&"completed"], "Task completes exactly at estimated duration")
_expect(task.state == StaffTaskType.State.COMPLETED and staff.current_task_id.is_empty(), "completion releases Staff while Task becomes terminal")
_expect(staff.state == StaffRuntimeType.State.AVAILABLE, "completed Task returns on-duty Staff to AVAILABLE")
func _test_automatic_task_progression_once_per_tick() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"auto_worker", 10)
var task := _task(&"auto_task", 50, 0, {&"estimated_minutes": 2})
service.register_staff(staff)
service.register_task(task)
service.dispatch_now(0)
var begin_tick: Dictionary = service.advance_one_minute(1)
_expect(
begin_tick[&"task_actions"].size() == 1
and begin_tick[&"task_actions"][0][&"action"] == &"BEGAN_WORK",
"automatic cadence begins a RESERVED Task after its assignment minute"
)
_expect(task.state == StaffTaskType.State.WORKING and task.progress_minutes == 0, "begin tick does not also consume a work minute")
_expect(not service.progress_task(task.task_id, 1, 1).is_success(), "explicit same-minute progress cannot double apply after automatic begin")
_expect(not service.advance_one_minute(1)[&"ok"], "the same StaffService minute cannot run twice")
_expect(task.progress_minutes == 0, "duplicate tick rejection preserves Task progress")
var progress_tick: Dictionary = service.advance_one_minute(2)
_expect(
progress_tick[&"task_actions"][0][&"action"] == &"PROGRESSED"
and task.progress_minutes == 1,
"the following tick applies exactly one work minute"
)
_expect(not service.progress_task(task.task_id, 1, 2).is_success(), "automatic progress blocks a second same-minute write")
service.advance_one_minute(3)
_expect(task.state == StaffTaskType.State.COMPLETED and task.progress_minutes == 2, "automatic cadence completes at the exact estimated duration")
_expect(staff.state == StaffRuntimeType.State.AVAILABLE and staff.current_task_id.is_empty(), "automatic completion releases Staff exactly once")
var travel_service := StaffServiceType.new()
var traveler := _staff(&"travel_worker", 10, {&"current_cell": Vector3i.ZERO})
var remote := _task(&"remote_task", 50, 0, {
&"target_cell": Vector3i(2, 0, 0),
&"estimated_minutes": 1,
})
travel_service.register_staff(traveler)
travel_service.register_task(remote)
travel_service.dispatch_now(0)
travel_service.advance_one_minute(1)
_expect(remote.state == StaffTaskType.State.MOVING and remote.progress_minutes == 0, "non-arrived Staff enters MOVING without phantom work")
traveler.current_cell = remote.target_cell
travel_service.advance_one_minute(2)
_expect(remote.state == StaffTaskType.State.WORKING and remote.progress_minutes == 0, "arrival begins work without same-tick progress")
travel_service.advance_one_minute(3)
_expect(remote.state == StaffTaskType.State.COMPLETED, "arrived Staff completes through the same deterministic cadence")
func _test_employment_and_daily_payroll_contract() -> void:
var ordinary := _staff(&"staff_z", 10)
_expect(
ordinary.daily_wage == 0
and ordinary.employment_kind == StaffRuntimeType.EmploymentKind.FREE_HIRE,
"legacy generic Staff defaults to a zero-wage FREE_HIRE employment row"
)
var service := StaffServiceType.new()
_expect(service.register_staff(ordinary).is_success(), "generic Staff remains registrable without adult-content fields")
_expect(
service.register_staff(_staff(&"staff_a", 10, {
&"daily_wage": 240,
&"employment_kind": &"ADULT_CONTRACT",
})).is_success(),
"ADULT_CONTRACT is accepted as a commercial kind without duplicating the catalog gate"
)
_expect(
service.register_staff(_staff(&"staff_m", 10, {
&"daily_wage": 75,
&"employment_kind": StaffRuntimeType.EmploymentKind.UNIQUE,
})).is_success(),
"UNIQUE employment accepts the strict enum value"
)
var expected_rows: Array[Dictionary] = [
{&"staff_id": &"staff_a", &"employment_kind": &"ADULT_CONTRACT", &"daily_wage": 240},
{&"staff_id": &"staff_m", &"employment_kind": &"UNIQUE", &"daily_wage": 75},
{&"staff_id": &"staff_z", &"employment_kind": &"FREE_HIRE", &"daily_wage": 0},
]
_expect(service.get_daily_payroll_rows() == expected_rows, "daily payroll rows have an exact shape and lexical Staff order")
_expect(service.get_daily_payroll_total() == 315, "daily payroll total is the exact sum of active Staff wages")
var invalid_service := StaffServiceType.new()
_expect(
not invalid_service.register_staff(_staff(&"string_wage", 10, {&"daily_wage": "100"})).is_success(),
"string daily wage is rejected rather than coerced"
)
_expect(
not invalid_service.register_staff(_staff(&"float_wage", 10, {&"daily_wage": 10.0})).is_success(),
"floating-point daily wage is rejected rather than truncated"
)
_expect(
not invalid_service.register_staff(_staff(&"negative_wage", 10, {&"daily_wage": -1})).is_success(),
"negative daily wage is rejected"
)
_expect(
not invalid_service.register_staff(_staff(&"unknown_kind", 10, {&"employment_kind": &"CONTRACT"})).is_success(),
"unknown employment kind is rejected"
)
_expect(invalid_service.get_staff_members().is_empty(), "invalid employment rows never partially register")
var current_snapshot := service.create_snapshot()
var exact := StaffServiceType.new()
_expect(exact.restore_snapshot(current_snapshot), "current payroll snapshot restores")
_expect(exact.create_snapshot() == current_snapshot, "current payroll snapshot round trip is exact")
var legacy_snapshot: Dictionary = current_snapshot.duplicate(true)
for staff_snapshot: Dictionary in legacy_snapshot[&"staff"]:
staff_snapshot.erase(&"daily_wage")
staff_snapshot.erase(&"employment_kind")
var migrated := StaffServiceType.new()
_expect(migrated.restore_snapshot(legacy_snapshot), "pre-payroll Staff snapshot restores through deterministic defaults")
_expect(
migrated.get_daily_payroll_total() == 0
and migrated.get_staff(&"staff_a").employment_kind == StaffRuntimeType.EmploymentKind.FREE_HIRE,
"legacy payroll migration defaults every existing Staff to zero-wage FREE_HIRE"
)
var atomic_before := exact.create_snapshot()
var negative_snapshot: Dictionary = current_snapshot.duplicate(true)
negative_snapshot[&"staff"][0][&"daily_wage"] = -1
_expect(not exact.restore_snapshot(negative_snapshot), "negative wage snapshot corruption is rejected")
_expect(exact.create_snapshot() == atomic_before, "negative wage restore failure is atomic")
var typed_snapshot: Dictionary = current_snapshot.duplicate(true)
typed_snapshot[&"staff"][0][&"daily_wage"] = "240"
_expect(not exact.restore_snapshot(typed_snapshot), "malformed wage snapshot type is rejected")
_expect(exact.create_snapshot() == atomic_before, "malformed wage restore failure is atomic")
var kind_snapshot: Dictionary = current_snapshot.duplicate(true)
kind_snapshot[&"staff"][0][&"employment_kind"] = "UNKNOWN"
_expect(not exact.restore_snapshot(kind_snapshot), "unknown employment snapshot kind is rejected")
_expect(exact.create_snapshot() == atomic_before, "unknown kind restore failure is atomic")
var unpaid_type_snapshot: Dictionary = current_snapshot.duplicate(true)
unpaid_type_snapshot[&"staff"][0][&"unpaid_wages"] = "0"
_expect(not exact.restore_snapshot(unpaid_type_snapshot), "malformed active unpaid-wage type is rejected")
_expect(exact.create_snapshot() == atomic_before, "malformed active liability restore failure is atomic")
var invalid_runtime := service.get_staff(&"staff_a")
invalid_runtime.daily_wage = -1
_expect(not service.validate_invariants()[&"ok"], "Staff invariants detect a corrupted live daily wage")
invalid_runtime.daily_wage = 240
service._departed_payroll[&"former_staff"] = -5
_expect(not service.validate_invariants()[&"ok"], "Staff invariants detect a corrupted departed wage liability")
service._departed_payroll.erase(&"former_staff")
_expect(service.validate_invariants()[&"ok"], "valid employment and payroll state satisfies all Staff invariants")
func _test_retirement_preserves_unpaid_wages() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"retiring_staff", 10, {
&"daily_wage": 125,
&"employment_kind": &"ADULT_CONTRACT",
&"unpaid_wages": 375,
})
service.register_staff(staff)
var result = service.retire_staff(staff.staff_id, 50)
_expect(result.is_success() and not service.has_staff(staff.staff_id), "retirement removes active Staff after cleanup")
_expect(service.get_departed_unpaid_wages(staff.staff_id) == 375, "retirement preserves unpaid wages by former Staff ID")
_expect(service.get_daily_payroll_rows().is_empty() and service.get_daily_payroll_total() == 0, "departed Staff is removed from future daily wage expense")
var snapshot: Dictionary = service.create_snapshot()
_expect(snapshot[&"departed_payroll"][0][&"unpaid_wages"] == 375, "departed payroll liability is included in Save snapshot")
var restored := StaffServiceType.new()
_expect(restored.restore_snapshot(snapshot), "departed payroll snapshot restores exactly")
var before_corruption := restored.create_snapshot()
var malformed: Dictionary = snapshot.duplicate(true)
malformed[&"departed_payroll"][0][&"unpaid_wages"] = "375"
_expect(not restored.restore_snapshot(malformed), "malformed departed payroll amount is rejected")
_expect(restored.create_snapshot() == before_corruption, "departed payroll corruption rejection is atomic")
func _test_daily_payroll_settlement_lifecycle() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"staff_z", 10, {&"daily_wage": 40}))
service.register_staff(_staff(&"staff_a", 10, {
&"daily_wage": 100,
&"employment_kind": &"ADULT_CONTRACT",
&"unpaid_wages": 50,
}))
service.register_staff(_staff(&"former", 10, {
&"daily_wage": 80,
&"unpaid_wages": 90,
}))
service.remove_staff(&"former", &"CONTRACT_ENDED", true, 0)
var preview: Dictionary = service.preview_daily_payroll(1)
var expected_rows: Array[Dictionary] = [
{
&"staff_id": &"former", &"payroll_status": &"DEPARTED", &"employment_kind": &"",
&"daily_wage": 0, &"prior_arrears": 90, &"new_wages": 0, &"total_due": 90,
},
{
&"staff_id": &"staff_a", &"payroll_status": &"ACTIVE", &"employment_kind": &"ADULT_CONTRACT",
&"daily_wage": 100, &"prior_arrears": 50, &"new_wages": 100, &"total_due": 150,
},
{
&"staff_id": &"staff_z", &"payroll_status": &"ACTIVE", &"employment_kind": &"FREE_HIRE",
&"daily_wage": 40, &"prior_arrears": 0, &"new_wages": 40, &"total_due": 40,
},
]
_expect(preview[&"ok"] and preview[&"rows"] == expected_rows, "payroll preview has an exact uniform shape and lexical Staff order")
_expect(
preview[&"new_wages_total"] == 140
and preview[&"prior_arrears_total"] == 140
and preview[&"total_due"] == 280,
"payroll preview strictly separates new wages, prior arrears and total due"
)
preview[&"rows"][0][&"total_due"] = 999
_expect(service.get_departed_unpaid_wages(&"former") == 90, "payroll preview is a defensive copy")
var first = service.apply_daily_payroll_settlement(
&"payroll.day.1",
1,
{&"staff_a": 150, &"former": 30, &"staff_z": 0}
)
_expect(first.is_success() and not first.details[&"replayed"], "first payroll settlement commits once")
_expect(
first.details[&"paid_total"] == 180
and first.details[&"unpaid_after_total"] == 100,
"full, partial and zero payments settle in one atomic batch"
)
_expect(service.get_staff(&"staff_a").unpaid_wages == 0, "fully paid active Staff has no arrears")
_expect(service.get_staff(&"staff_z").unpaid_wages == 40, "zero payment carries the new active wage forward")
_expect(service.get_departed_unpaid_wages(&"former") == 60, "partial departed payment reduces only preserved debt")
_expect(not service.preview_daily_payroll(1)[&"ok"], "settled day cannot be previewed as a second accrual")
var obligations := service.get_payroll_obligations()
_expect(
obligations.map(func(row: Dictionary) -> StringName: return row[&"staff_id"]) == [&"former", &"staff_a", &"staff_z"],
"Operations obligation query is ordered across active and departed Staff"
)
obligations[0][&"unpaid_wages"] = 999
_expect(service.get_departed_unpaid_wages(&"former") == 60, "obligation query returns defensive row copies")
var second_preview := service.preview_daily_payroll(2)
_expect(
second_preview[&"new_wages_total"] == 140
and second_preview[&"prior_arrears_total"] == 100
and second_preview[&"total_due"] == 240,
"next day accrues active wages once while retaining all arrears"
)
var second = service.apply_daily_payroll_settlement(
&"payroll.day.2",
2,
{&"former": 60, &"staff_a": 50, &"staff_z": 80}
)
_expect(second.is_success() and second.details[&"paid_total"] == 190, "next-day partial allocation commits")
_expect(service.get_departed_staff_ids().is_empty(), "full departed paydown removes the departed liability row")
_expect(
service.get_staff(&"staff_a").unpaid_wages == 50
and service.get_staff(&"staff_z").unpaid_wages == 0,
"active Staff carry only their exact post-payment balances"
)
service.remove_staff(&"staff_a", &"CONTRACT_ENDED", true, 10)
_expect(service.get_departed_unpaid_wages(&"staff_a") == 50, "removal after settlement preserves remaining active arrears")
var third_preview := service.preview_daily_payroll(3)
_expect(
third_preview[&"rows"][0][&"staff_id"] == &"staff_a"
and third_preview[&"rows"][0][&"new_wages"] == 0
and third_preview[&"rows"][0][&"total_due"] == 50,
"departed Staff never accrue another daily wage"
)
_expect(
service.apply_daily_payroll_settlement(&"payroll.day.3", 3, {&"staff_a": 50, &"staff_z": 40}).is_success(),
"later settlement can fully clear active and departed obligations"
)
_expect(service.get_departed_staff_ids().is_empty() and service.get_staff(&"staff_z").unpaid_wages == 0, "full settlement clears every liability")
func _test_payroll_validation_and_idempotency() -> void:
var service := StaffServiceType.new()
service.register_staff(_staff(&"paid_staff", 10, {&"daily_wage": 100}))
var pristine := service.create_snapshot()
_expect(not service.preview_daily_payroll(0)[&"ok"], "zero payroll day is rejected")
_expect(not service.preview_daily_payroll("1")[&"ok"], "string payroll day is rejected rather than coerced")
_expect(not service.apply_daily_payroll_settlement(&"bad id", 1, {}).is_success(), "unstable settlement ID is rejected")
_expect(not service.apply_daily_payroll_settlement(&"bad_payments", 1, []).is_success(), "non-Dictionary payments are rejected")
_expect(not service.apply_daily_payroll_settlement(&"typed", 1, {&"paid_staff": "100"}).is_success(), "string payment is rejected")
_expect(not service.apply_daily_payroll_settlement(&"float", 1, {&"paid_staff": 100.0}).is_success(), "floating payment is rejected")
_expect(not service.apply_daily_payroll_settlement(&"negative", 1, {&"paid_staff": -1}).is_success(), "negative payment is rejected")
_expect(not service.apply_daily_payroll_settlement(&"unknown", 1, {&"missing": 0}).is_success(), "unknown Staff payment is rejected even when zero")
_expect(not service.apply_daily_payroll_settlement(&"overpay", 1, {&"paid_staff": 101}).is_success(), "payment above exact obligation is rejected")
_expect(service.create_snapshot() == pristine, "all malformed and overpayment requests are atomic")
var committed = service.apply_daily_payroll_settlement(&"stable.day.1", 1, {&"paid_staff": 25})
_expect(committed.is_success() and service.get_staff(&"paid_staff").unpaid_wages == 75, "valid partial settlement commits exact arrears")
var committed_snapshot := service.create_snapshot()
var replay = service.apply_daily_payroll_settlement(&"stable.day.1", 1, {&"paid_staff": 25})
_expect(replay.is_success() and replay.details[&"replayed"], "same settlement ID and fingerprint replays successfully")
_expect(service.create_snapshot() == committed_snapshot, "idempotent replay performs no second wage accrual or payment")
_expect(not service.apply_daily_payroll_settlement(&"stable.day.1", 1, {&"paid_staff": 24}).is_success(), "same settlement ID rejects a conflicting payment fingerprint")
_expect(not service.apply_daily_payroll_settlement(&"stable.day.1", 2, {&"paid_staff": 25}).is_success(), "same settlement ID rejects a conflicting day")
_expect(not service.apply_daily_payroll_settlement(&"other.day.1", 1, {}).is_success(), "different settlement ID cannot accrue the same day twice")
_expect(service.create_snapshot() == committed_snapshot, "all idempotency conflicts preserve committed state")
func _test_payroll_snapshot_migration_and_tamper() -> void:
var legacy_source := StaffServiceType.new()
legacy_source.register_staff(_staff(&"active_debt", 10, {&"daily_wage": 75, &"unpaid_wages": 125}))
legacy_source.register_staff(_staff(&"departed_debt", 10, {&"daily_wage": 50, &"unpaid_wages": 200}))
legacy_source.remove_staff(&"departed_debt", &"LEFT", true, 0)
var legacy: Dictionary = legacy_source.create_snapshot()
legacy[&"version"] = 1
legacy.erase(&"last_advanced_minute")
legacy.erase(&"payroll_settlements")
for staff_snapshot: Dictionary in legacy[&"staff"]:
staff_snapshot.erase(&"snapshot_version")
staff_snapshot.erase(&"employment_effective_minute")
var migrated := StaffServiceType.new()
_expect(migrated.restore_snapshot(legacy), "v1 StaffService snapshot migrates to empty settlement history")
_expect(
migrated.get_staff(&"active_debt").unpaid_wages == 125
and migrated.get_departed_unpaid_wages(&"departed_debt") == 200,
"v1 migration preserves active and departed unpaid wages"
)
var migrated_snapshot := migrated.create_snapshot()
_expect(migrated_snapshot[&"version"] == 3 and migrated_snapshot[&"payroll_settlements"].is_empty(), "v1 migration emits canonical v3 with empty history")
var service := StaffServiceType.new()
service.register_staff(_staff(&"staff_a", 10, {&"daily_wage": 100, &"unpaid_wages": 20}))
service.register_staff(_staff(&"staff_b", 10, {&"daily_wage": 50}))
service.apply_daily_payroll_settlement(&"snapshot.day.1", 1, {&"staff_a": 30, &"staff_b": 0})
service.apply_daily_payroll_settlement(&"snapshot.day.2", 2, {&"staff_a": 50, &"staff_b": 100})
var snapshot: Dictionary = service.create_snapshot()
var restored := StaffServiceType.new()
_expect(restored.restore_snapshot(snapshot), "canonical v3 payroll history restores")
_expect(restored.create_snapshot() == snapshot, "v3 payroll settlement history round trip is exact")
var v2_snapshot: Dictionary = snapshot.duplicate(true)
v2_snapshot[&"version"] = 2
for staff_snapshot: Dictionary in v2_snapshot[&"staff"]:
staff_snapshot.erase(&"snapshot_version")
staff_snapshot.erase(&"employment_effective_minute")
var v2_migrated := StaffServiceType.new()
_expect(v2_migrated.restore_snapshot(v2_snapshot), "pre-effective-minute v2 payroll history migrates")
_expect(
v2_migrated.create_snapshot()[&"version"] == 3
and v2_migrated.get_staff(&"staff_a").employment_effective_minute == 0,
"v2 migration emits canonical v3 Staff rows at effective minute zero"
)
var before_tamper := restored.create_snapshot()
var fingerprint_tamper: Dictionary = snapshot.duplicate(true)
fingerprint_tamper[&"payroll_settlements"][0][&"fingerprint"] = "0".repeat(64)
_expect(not restored.restore_snapshot(fingerprint_tamper), "tampered payroll fingerprint is rejected")
_expect(restored.create_snapshot() == before_tamper, "fingerprint rejection is atomic")
var total_tamper: Dictionary = snapshot.duplicate(true)
total_tamper[&"payroll_settlements"][0][&"total_due"] += 1
_expect(not restored.restore_snapshot(total_tamper), "tampered payroll derived total is rejected")
_expect(restored.create_snapshot() == before_tamper, "derived total rejection is atomic")
var amount_tamper: Dictionary = snapshot.duplicate(true)
amount_tamper[&"payroll_settlements"][0][&"rows"][0][&"payment"] = -1
_expect(not restored.restore_snapshot(amount_tamper), "negative historical payment is rejected")
_expect(restored.create_snapshot() == before_tamper, "negative history rejection is atomic")
var order_tamper: Dictionary = snapshot.duplicate(true)
order_tamper[&"payroll_settlements"].reverse()
_expect(not restored.restore_snapshot(order_tamper), "unordered settlement days are rejected")
_expect(restored.create_snapshot() == before_tamper, "history order rejection is atomic")
var duplicate_day: Dictionary = snapshot.duplicate(true)
duplicate_day[&"payroll_settlements"][1][&"day"] = 1
_expect(not restored.restore_snapshot(duplicate_day), "duplicate settlement day is rejected")
_expect(restored.create_snapshot() == before_tamper, "duplicate day rejection is atomic")
var extra_field: Dictionary = snapshot.duplicate(true)
extra_field[&"payroll_settlements"][0][&"unexpected"] = true
_expect(not restored.restore_snapshot(extra_field), "unknown current payroll history field is rejected")
_expect(restored.create_snapshot() == before_tamper, "unknown field rejection is atomic")
var typed_day: Dictionary = snapshot.duplicate(true)
typed_day[&"payroll_settlements"][0][&"day"] = "1"
_expect(not restored.restore_snapshot(typed_day), "string historical day is rejected rather than coerced")
_expect(restored.create_snapshot() == before_tamper, "historical type rejection is atomic")
func _test_remove_staff_cleans_schedule_graph() -> void:
var service := StaffServiceType.new()
var staff := _staff(&"scheduled_departure", 10, {&"state": StaffRuntimeType.State.OFF_DUTY})
service.register_staff(staff)
service.set_schedule_override(staff.staff_id, 3, {&"start_minute_of_day": 420})
service.set_schedule_override(staff.staff_id, 4, {&"assigned_facility_id": &"remote"})
service.create_standard_shift(staff.staff_id, &"DAY", 3, &"reception", &"departure_day_3")
service.create_standard_shift(staff.staff_id, &"DAY", 4, &"warehouse", &"departure_day_4")
var removal = service.remove_staff(staff.staff_id, &"CONTRACT_ENDED", true, 100)
_expect(removal.is_success() and not service.has_staff(staff.staff_id), "Staff removal succeeds with future schedule records")
_expect(service.get_shifts().is_empty(), "Staff removal deletes every related Shift instance")
_expect(
service.get_schedule_override(staff.staff_id, 3).is_empty()
and service.get_schedule_override(staff.staff_id, 4).is_empty(),
"Staff removal deletes every related ScheduleOverride"
)
_expect(
removal.details[&"removed_shift_ids"] == [&"departure_day_3", &"departure_day_4"]
and removal.details[&"removed_override_days"] == [3, 4],
"Staff removal reports deterministic schedule cleanup"
)
_expect(service.validate_invariants()[&"ok"], "Staff removal leaves no orphan schedule graph")
var snapshot := service.create_snapshot()
var restored := StaffServiceType.new()
_expect(restored.restore_snapshot(snapshot), "post-removal StaffService snapshot restores")
_expect(restored.create_snapshot() == snapshot, "post-removal snapshot round trip is exact")
func _test_snapshot_round_trip_and_atomic_restore() -> void:
var original := StaffServiceType.new()
var staff := _staff(&"snapshot_staff", 20)
original.register_staff(staff)
original.create_standard_shift(staff.staff_id, &"NIGHT", 0, &"warehouse", &"snapshot_shift")
original.advance_one_minute(1_080)
var task := _task(&"snapshot_task", 70, 1_080)
original.register_task(task)
original.dispatch_now(1_080)
var snapshot: Dictionary = original.create_snapshot()
var restored := StaffServiceType.new()
_expect(restored.restore_snapshot(snapshot), "valid StaffService snapshot restores")
_expect(restored.create_snapshot() == snapshot, "Staff, Shift, Task and Dispatcher round trip exactly")
_expect(restored.validate_invariants()[&"ok"], "restored Staff graph satisfies pair invariants")
var before_invalid: Dictionary = restored.create_snapshot()
var invalid: Dictionary = snapshot.duplicate(true)
invalid[&"tasks"][0][&"assigned_staff_id"] = "missing_staff"
_expect(not restored.restore_snapshot(invalid), "snapshot with orphan Task is rejected")
_expect(restored.create_snapshot() == before_invalid, "failed restore leaves live Staff state untouched")
func _test_orphan_recovery_is_atomic() -> void:
var release_outcome := {&"ok": false}
var release_calls: Array = []
var service := StaffServiceType.new()
service.configure_callbacks(
Callable(),
func(ids: Array[StringName], _task_value: StaffTask, _minute: int) -> Dictionary:
release_calls.append(ids.duplicate())
return {&"ok": bool(release_outcome[&"ok"])}
)
service.register_staff(_staff(&"live_staff", 10))
var live_before: Dictionary = service.create_snapshot()
var reopened_signals: Array[StringName] = []
service.task_reopened.connect(
func(task_id: StringName, _reason: StringName) -> void:
reopened_signals.append(task_id)
)
var candidate := StaffServiceType.new().create_snapshot()
var orphan_a := _task(&"orphan_a", 50, 0, {
&"required_items": {&"tool": 1},
&"state": StaffTaskType.State.RESERVED,
&"assigned_staff_id": &"missing_a",
&"reservation_ids": [&"reservation_a"],
})
var orphan_b := _task(&"orphan_b", 40, 0, {
&"required_items": {&"tool": 1},
&"state": StaffTaskType.State.RESERVED,
&"assigned_staff_id": &"missing_b",
&"reservation_ids": [&"reservation_b"],
})
candidate[&"tasks"] = [orphan_a.create_snapshot(), orphan_b.create_snapshot()]
_expect(not service.restore_snapshot_with_orphan_recovery(candidate), "failed external Reservation release rejects orphan recovery")
_expect(service.create_snapshot() == live_before and reopened_signals.is_empty(), "failed orphan recovery preserves live Staff snapshot and emits no repair signals")
_expect(release_calls == [[&"reservation_a", &"reservation_b"]], "all orphan Reservations use one external release transaction")
_expect(service.last_recovery_repair_report[&"release_failed_task_ids"] == [&"orphan_a", &"orphan_b"], "failed transaction reports every unrepaired Task exactly")
release_outcome[&"ok"] = true
release_calls.clear()
_expect(service.restore_snapshot_with_orphan_recovery(candidate), "successful external release commits the isolated repaired candidate")
_expect(release_calls == [[&"reservation_a", &"reservation_b"]], "successful recovery still performs exactly one Reservation transaction")
_expect(service.get_task(&"orphan_a").state == StaffTaskType.State.OPEN and service.get_task(&"orphan_b").state == StaffTaskType.State.OPEN, "committed recovery reopens only the two orphan Tasks")
_expect(reopened_signals == [&"orphan_a", &"orphan_b"] and service.validate_invariants()[&"ok"], "repair signals publish only after the valid candidate commits")
var committed_before: Dictionary = service.create_snapshot()
var signal_count_before := reopened_signals.size()
var unrelated_damage: Dictionary = candidate.duplicate(true)
var missing_staff_shift := ShiftInstanceType.create_standard(&"invalid_shift", &"missing_shift_staff", &"DAY", 0)
unrelated_damage[&"shifts"] = [missing_staff_shift.create_snapshot()]
release_calls.clear()
_expect(not service.restore_snapshot_with_orphan_recovery(unrelated_damage), "recovery rejects non-orphan Shift corruption mixed with repairable Tasks")
_expect(release_calls.is_empty(), "non-repairable damage is rejected before any external Reservation release")
_expect(service.create_snapshot() == committed_before and reopened_signals.size() == signal_count_before, "non-repairable recovery failure preserves live snapshot and signals atomically")
var exact := StaffServiceType.new()
_expect(exact.restore_snapshot_with_orphan_recovery(committed_before), "ordinary valid snapshot also restores through the recovery boundary")
_expect(exact.create_snapshot() == committed_before, "ordinary recovery-boundary round trip remains exact")
func _staff(
staff_id: StringName,
operation_skill: int,
options: Dictionary = {}
) -> StaffRuntime:
var configuration := {
&"state": StaffRuntimeType.State.AVAILABLE,
&"role_tags": [&"worker"],
&"permitted_task_tags": [&"work", &"rest"],
&"skills": {&"operation": operation_skill},
}
for key: Variant in options:
configuration[key] = options[key]
return StaffRuntimeType.new().configure(staff_id, &"worker", configuration)
func _task(
task_id: StringName,
priority: int,
created_at: int,
options: Dictionary = {}
) -> StaffTask:
var configuration := {
&"required_role_tags": [&"worker"],
&"required_skill_min": {&"operation": 1},
&"task_tags": [&"work"],
&"estimated_minutes": 5,
&"interrupt_level": 3,
}
for key: Variant in options:
configuration[key] = options[key]
return StaffTaskType.new().configure(task_id, &"work", priority, created_at, configuration)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://djxblic7ohru8