Files
TheTree_mock/tests/s3/test_staff_service.gd
Takeshi 5dde0c5ec2 Record original-alignment implementation and traceability checkpoint
Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
2026-09-30 23:52:24 +09:00

853 lines
46 KiB
GDScript

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_unique_employment_protection()
_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": 0},
{&"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() == 240, "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(&"work_assignments")
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"] == StaffServiceType.SNAPSHOT_VERSION and migrated_snapshot[&"payroll_settlements"].is_empty(), "v1 migration emits canonical current schema 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 current schema payroll history restores")
_expect(restored.create_snapshot() == snapshot, "current payroll settlement history round trip is exact")
var v2_snapshot: Dictionary = snapshot.duplicate(true)
v2_snapshot[&"version"] = 2
v2_snapshot.erase(&"work_assignments")
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"] == StaffServiceType.SNAPSHOT_VERSION
and v2_migrated.get_staff(&"staff_a").employment_effective_minute == 0,
"v2 migration emits canonical current schema 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)
func _test_unique_employment_protection() -> void:
var service := StaffServiceType.new()
var unique := _staff(&"original_unique", 10, {&"employment_kind": &"UNIQUE", &"daily_wage": 75, &"unpaid_wages": 25})
_expect(service.register_staff(unique).is_success(), "legacy unique with stored wage remains loadable")
var before := service.create_snapshot()
for reason: StringName in [&"DISMISSED", &"SOLD", &"REMOVED"]:
_expect(not service.remove_staff(unique.staff_id, reason).is_success() and service.create_snapshot() == before, "unique removal is rejected atomically for %s" % reason)
_expect(not service.retire_staff(unique.staff_id).is_success() and not unique.is_retiring and service.create_snapshot() == before, "unique retirement rejects before setting retirement flag")
var preview := service.preview_daily_payroll(1)
_expect(preview[&"ok"] and preview[&"new_wages_total"] == 0 and preview[&"prior_arrears_total"] == 25 and preview[&"total_due"] == 25, "unique accrues zero wages without silently deleting historical liabilities")
_expect(service.get_daily_payroll_total() == 0 and service.get_payroll_obligations()[0][&"daily_wage"] == 0, "all payroll views use unique zero upkeep")
var restored := StaffServiceType.new()
_expect(restored.restore_snapshot(before) and restored.create_snapshot() == before and restored.get_daily_payroll_total() == 0, "unique zero-upkeep rule survives exact save round trip")
_expect(not restored.retire_staff(unique.staff_id).is_success() and restored.create_snapshot() == before, "restored unique remains protected against removal")
_expect(restored.apply_daily_payroll_settlement(&"unique_day1", 1, {unique.staff_id: 25}).is_success(), "unique settlement pays only preserved historical arrears")
_expect(restored.get_staff(unique.staff_id).unpaid_wages == 0 and restored.get_payroll_settlement(&"unique_day1")[&"new_wages_total"] == 0, "unique settlement adds no new upkeep")
var settled := restored.create_snapshot()
_expect(restored.apply_daily_payroll_settlement(&"unique_day1", 1, {unique.staff_id: 25}).is_success() and restored.create_snapshot() == settled, "unique settlement retry is idempotent")
_expect(restored.preview_daily_payroll(2)[&"total_due"] == 0, "following day keeps unique upkeep free")