Files
TheTree_mock/tests/supply/test_supply_management_service.gd
T

338 lines
19 KiB
GDScript

extends SceneTree
## Runtime-level purchasing and replenishment. All physical stock is purchased
## and cooked normally; failure fixtures cover payment, closed routes and expiry.
class RejectingPaymentEconomy:
extends EconomyService
func transfer(_from: StringName, _to: StringName, _amount: int,
_minute: int = -1, _category: StringName = DEFAULT_CATEGORY,
_source: StringName = &"", _key: StringName = &"", _memo: StringName = &"",
_related: StringName = &"", _notify := true) -> CommandResult:
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Injected payment refusal")
const PORTER := &"staff_gina"
const COOK := &"staff_chika"
const RETURN_REQUEST := &"supply.management_return"
var _checks := 0
var _failures := 0
var _completed_groups := 0
var _runtime: Variant
var _clock: Variant
var _state: Variant
var _ids: Dictionary = {}
var _built: Dictionary = {}
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_clock.reset(1, 6, 0)
_state.reset()
_expect(_runtime.initialize(_clock, _state, 553221)[&"valid"], "Runtime initializes")
_clock.set_paused(true)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for row: Array in [
[&"warehouse", Vector3i(-8, 0, 1)], [ &"kitchen", Vector3i(2, 0, 1)],
[&"cafeteria", Vector3i(2, 0, 0)], [&"staff_door", Vector3i(0, 0, 1)],
]:
_command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]})
_clock.advance_minutes(180)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
_ids[facility.definition_id] = facility.instance_id
var completed := 0
for job: ConstructionJob in _runtime.construction_service.get_jobs():
if job.state == ConstructionJob.State.COMPLETED:
completed += 1
_expect(_ids.has(&"warehouse") and _ids.has(&"kitchen") and _ids.has(&"cafeteria") and completed == 4,
"normal starting budget completes three supply facilities and the staff door")
if not _ids.has(&"warehouse") or not _ids.has(&"kitchen") or not _ids.has(&"cafeteria"):
_finish()
return
_built = _runtime.create_snapshot()
_test_paid_purchase_and_rejection()
_test_disabled_kitchen_still_delivers()
_test_multiple_sources_choose_connected_nearest()
_test_returning_cargo_reserves_actual_space()
_expect(_completed_groups == 4, "all four regression scenarios reach their final assertions")
_finish()
func _test_paid_purchase_and_rejection() -> void:
var cash: int = _runtime.economy_service.get_balance(&"PLAYER_CASH")
var quote: Dictionary = _runtime.preview_supply_purchase(_ids[&"warehouse"], SupplyCatalog.RAW_FOOD, 8)
_expect(bool(quote[&"ok"]), "unassigned completed warehouse accepts a paid purchase")
var result := _purchase(8)
_expect(result.is_success() and _quantity(&"warehouse", SupplyCatalog.RAW_FOOD) == 8,
"successful purchase creates exactly eight physical units")
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == cash - int(quote[&"total_cost"])
and _state.get_resource(&"gold") == cash - int(quote[&"total_cost"]),
"ledger and player cash both reflect the exact purchase price")
var entries: Array = _runtime.economy_service.get_transaction_entries(StringName(result.details.get(&"transaction_id", &"")))
_expect(entries.size() == 1 and entries[0].amount == int(quote[&"total_cost"])
and entries[0].source_entity_id == _ids[&"warehouse"], "purchase records one expense attributed to its warehouse")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 0}, &"INVALID_QUANTITY")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 100}, &"STORAGE_FULL")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1000}, &"INSUFFICIENT_FUNDS")
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.BASIC_MEAL, &"quantity": 1}, &"ITEM_NOT_PURCHASABLE")
_assert_purchase_rejected({&"warehouse_id": _ids[&"kitchen"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}, &"NOT_WAREHOUSE")
_runtime.facility_service.set_operational_status(_ids[&"warehouse"], {&"operator_open": false}, _clock.absolute_minute)
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}, &"FACILITY_CLOSED")
_runtime.facility_service.set_operational_status(_ids[&"warehouse"], {&"operator_open": true}, _clock.absolute_minute)
# Exercise the rollback after stock has been inserted, rather than only
# testing quote failures that reject before the first inventory write.
var original_economy: EconomyService = _runtime.economy_service
var rejecting := RejectingPaymentEconomy.new()
_expect(rejecting.restore_snapshot(original_economy.create_snapshot()), "payment-failure fixture retains all real balances")
_runtime.economy_service = rejecting
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}, &"PAYMENT_FAILED")
_runtime.economy_service = original_economy
_completed_groups += 1
func _test_disabled_kitchen_still_delivers() -> void:
_expect(_runtime.restore_snapshot(_built), "reset to the paid buildings for production")
_hire_and_assign(PORTER, &"warehouse")
_hire_and_assign(COOK, &"kitchen")
_expect(_purchase(8).is_success(), "purchase ingredients for real production")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 4})
for minute: int in 100:
if _quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) >= 4:
break
_clock.advance_minutes(1)
_expect(_quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) == 4, "paid food is transported and cooked into one real batch")
if _quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) != 4:
return
_expect(_meal_requests().is_empty(), "production finishes before the following logistics tick creates a meal request")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": false, &"target_stock": 4})
# Preserve the legacy compatibility boundary: a kitchen with no explicit
# production plan must not suddenly become an automatic source.
_expect(_runtime.kitchen_production.remove_plan(_ids[&"kitchen"])[&"ok"], "completed disabled kitchen can model a legacy unconfigured source")
_runtime.supply_management.ensure_deliveries(_runtime, _clock.absolute_minute)
_expect(_meal_requests().is_empty(), "unconfigured kitchen stock does not trigger unsolicited automatic delivery")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": false, &"target_stock": 4})
_clock.advance_minutes(1)
_expect(_meal_requests().size() == 1 and not _runtime.kitchen_production.get_plan(_ids[&"kitchen"])[&"enabled"],
"an explicitly disabled production plan still dispatches its completed meals")
_expect(_quantity(&"cafeteria", SupplyCatalog.BASIC_MEAL) == 0
and _runtime.inventory_service.available_quantity(_inventory(&"kitchen"), SupplyCatalog.BASIC_MEAL) == 0,
"meal request reserves stock without instantly moving it to the dining room")
_runtime.supply_management.ensure_deliveries(_runtime, _clock.absolute_minute)
_runtime.supply_management.ensure_deliveries(_runtime, _clock.absolute_minute)
_expect(_meal_requests().size() == 1, "repeated replenishment cannot duplicate the already reserved meal shipment")
for minute: int in 100:
if _quantity(&"cafeteria", SupplyCatalog.BASIC_MEAL) == 4:
break
_clock.advance_minutes(1)
_expect(_quantity(&"cafeteria", SupplyCatalog.BASIC_MEAL) == 4
and _quantity(&"kitchen", SupplyCatalog.BASIC_MEAL) == 0,
"real porter completes the outgoing shipment while kitchen production remains off")
_completed_groups += 1
func _test_multiple_sources_choose_connected_nearest() -> void:
_expect(_runtime.restore_snapshot(_built), "reset to the paid buildings for competing suppliers")
_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(6, 0, 1)})
_clock.advance_minutes(180)
var near_id: StringName = &""
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"warehouse" and facility.instance_id != _ids[&"warehouse"]:
near_id = facility.instance_id
_expect(not near_id.is_empty(), "starting budget also completes a second, closer warehouse")
if near_id.is_empty():
return
_expect(_purchase(8).is_success(), "older distant supplier holds eight paid units")
_command(&"purchase_supplies", {&"warehouse_id": near_id, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 8})
var near: FacilityInstance = _runtime.facility_service.get_instance(near_id)
var kitchen: FacilityInstance = _runtime.facility_service.get_instance(_ids[&"kitchen"])
var near_route: Dictionary = _runtime.supply_logistics.find_facility_access_path(Vector3i(6, 0, 1), kitchen)
var far_route: Dictionary = _runtime.supply_logistics.find_facility_access_path(Vector3i(-5, 0, 1), kitchen)
_expect(bool(near_route[&"reachable"]) and bool(far_route[&"reachable"])
and int(near_route[&"steps"]) < int(far_route[&"steps"])
and String(near_id) > String(_ids[&"warehouse"]),
"both actual routes connect, and the shorter route belongs to the later warehouse ID")
var stocked: Dictionary = _runtime.create_snapshot()
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 8})
_clock.advance_minutes(1)
var requests := _raw_requests_for_kitchen()
_expect(requests.size() == 1 and int(requests[0].item_requirements[SupplyCatalog.RAW_FOOD]) == 4,
"two stocked suppliers create only one four-unit shipment in the first logistics tick")
_expect(not requests.is_empty() and requests[0].source_inventory_id == near.local_inventory_id,
"connected nearby supplier wins over the older, farther warehouse")
_expect(_runtime.inventory_service.available_quantity(near.local_inventory_id, SupplyCatalog.RAW_FOOD) == 4
and _runtime.inventory_service.available_quantity(_inventory(&"warehouse"), SupplyCatalog.RAW_FOOD) == 8
and _incoming(&"kitchen", SupplyCatalog.RAW_FOOD) == 4,
"exactly four units are reserved once and counted once as incoming")
_clock.advance_minutes(1)
requests = _raw_requests_for_kitchen()
_expect(requests.size() == 2 and _incoming(&"kitchen", SupplyCatalog.RAW_FOOD) == 8,
"next tick adds at most one batch to cover the remaining four-unit deficit")
_clock.advance_minutes(1)
_expect(_raw_requests_for_kitchen().size() == 2
and _runtime.inventory_service.available_quantity(_inventory(&"warehouse"), SupplyCatalog.RAW_FOOD) == 8,
"incoming stock at target stops extra requests and leaves other supplier stock untouched")
# Geometric proximity must not beat an actual connection. Closing every
# access cell of the near warehouse leaves the far route into the kitchen.
_expect(_runtime.restore_snapshot(stocked), "rewind to the two unreserved suppliers")
near = _runtime.facility_service.get_instance(near_id)
kitchen = _runtime.facility_service.get_instance(_ids[&"kitchen"])
for cell: Vector3i in near.occupied_cells:
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT, Vector3i.UP, Vector3i.DOWN]:
if _runtime.path_service.has_cell(cell + delta):
_runtime.path_service.set_cell_temporary_block(cell + delta, true)
far_route = _runtime.supply_logistics.find_facility_access_path(Vector3i(-5, 0, 1), kitchen)
near_route = _runtime.supply_logistics.find_facility_access_path(Vector3i(6, 0, 1), kitchen)
_expect(bool(far_route[&"reachable"]) and not bool(near_route[&"reachable"]),
"route closure really disconnects the nearby supplier while preserving the far route")
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 8})
_clock.advance_minutes(1)
requests = _raw_requests_for_kitchen()
_expect(requests.size() == 1 and requests[0].source_inventory_id == _inventory(&"warehouse")
and int(requests[0].item_requirements[SupplyCatalog.RAW_FOOD]) == 4,
"unreachable near stock cannot steal the single shipment from a connected supplier")
_completed_groups += 1
func _test_returning_cargo_reserves_actual_space() -> void:
_expect(_runtime.restore_snapshot(_built), "reset to the paid buildings for returning cargo")
_hire_and_assign(PORTER, &"warehouse")
_expect(_purchase(2).is_success() and _purchase(6).is_success(), "two paid stacks provide an eight-unit shipment")
_command(&"create_logistics_request", {
&"request_id": RETURN_REQUEST, &"source_inventory_id": _inventory(&"warehouse"),
&"destination_inventory_id": _inventory(&"kitchen"), &"item_requirements": {SupplyCatalog.RAW_FOOD: 8},
})
var request: LogisticsRequest = _runtime.logistics_requests[RETURN_REQUEST]
for minute: int in 60:
if request.state == LogisticsRequest.State.DELIVERING:
break
_clock.advance_minutes(1)
var cargo_id := StringName("cargo:%s" % PORTER)
_expect(request.state == LogisticsRequest.State.DELIVERING
and _runtime.inventory_service.total_quantity(cargo_id, SupplyCatalog.RAW_FOOD) == 8,
"actual pickup puts purchased goods in the employee's cargo")
if request.state != LogisticsRequest.State.DELIVERING:
return
var inventory_snapshot: Dictionary = _runtime.inventory_service.create_snapshot()
var injected := false
for inventory: Dictionary in inventory_snapshot[&"inventories"]:
if inventory[&"owner_inventory_id"] != String(cargo_id):
continue
for stack: Dictionary in inventory[&"stacks"]:
if int(stack[&"quantity"]) == 2:
stack[&"expires_at"] = _clock.absolute_minute + 1
injected = true
_expect(injected, "the original two-unit paid stack remains identifiable inside real cargo")
_expect(_runtime.inventory_service.restore_snapshot(inventory_snapshot), "inject expiry of only the two-unit paid stack")
_clock.advance_minutes(1)
_expect(request.state == LogisticsRequest.State.DELIVERING and request.state_reason == "RETURNING_EXPIRED_CARGO"
and _runtime.inventory_service.total_quantity(cargo_id, SupplyCatalog.RAW_FOOD) == 6,
"partial expiry starts a real return of the six surviving units")
if request.state != LogisticsRequest.State.DELIVERING or request.state_reason != "RETURNING_EXPIRED_CARGO":
return
for reason: String in ["RETURNING_EXPIRED_CARGO", "EXPIRED_CARGO_RETURN_BLOCKED", "CARRIER_UNAVAILABLE"]:
# Both bridge states retain the same real cargo. Capacity accounting
# must follow that cargo even if the return route becomes blocked.
request.state_reason = reason
var quote: Dictionary = _runtime.preview_supply_purchase(_ids[&"warehouse"], SupplyCatalog.RAW_FOOD, 94)
_expect(bool(quote[&"ok"]) and int(quote[&"max_quantity"]) == 94,
"%s reserves six actual returning units, not the original eight" % reason)
_expect(_incoming(&"warehouse", SupplyCatalog.RAW_FOOD) == 6 and _incoming(&"kitchen", SupplyCatalog.RAW_FOOD) == 0,
"%s counts returning goods only at the true receiving warehouse" % reason)
_assert_purchase_rejected({&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 95}, &"STORAGE_FULL")
request.state_reason = "RETURNING_EXPIRED_CARGO"
_expect(_purchase(94).is_success(), "buyer may use the exact free space left after reserving the return")
for minute: int in 30:
if request.is_terminal():
break
_clock.advance_minutes(1)
_expect(request.state == LogisticsRequest.State.CANCELLED and request.state_reason == "CARGO_EXPIRED_RETURNED",
"capacity-aware purchase leaves enough room for the real return to complete")
_expect(_quantity(&"warehouse", SupplyCatalog.RAW_FOOD) == 100
and _runtime.inventory_service.total_quantity(cargo_id, SupplyCatalog.RAW_FOOD) == 0
and _incoming(&"warehouse", SupplyCatalog.RAW_FOOD) == 0,
"return fills warehouse exactly once and clears incoming cargo")
_expect(_runtime.inventory_service.validate_invariants()[&"ok"]
and SupplyLogisticsBridge.validate_crosslinks(_runtime.logistics_requests, _runtime.staff_service, _runtime.inventory_service, _runtime.facility_service),
"completed return preserves inventory and carrier ownership invariants")
_completed_groups += 1
func _assert_purchase_rejected(payload: Dictionary, reason: StringName) -> void:
var inventory_before: Dictionary = _runtime.inventory_service.create_snapshot()
var economy_before: Dictionary = _runtime.economy_service.create_snapshot()
var gold_before: int = _state.get_resource(&"gold")
var result: CommandResult = _runtime.submit_command(&"purchase_supplies", payload)
_expect(not result.is_success() and result.details.get(&"reason") == reason, "purchase rejects %s" % reason)
_expect(_runtime.inventory_service.create_snapshot() == inventory_before
and _runtime.economy_service.create_snapshot() == economy_before and _state.get_resource(&"gold") == gold_before,
"%s leaves full inventory, ledger and player cash unchanged" % reason)
func _command(type: StringName, payload: Dictionary) -> CommandResult:
var result: CommandResult = _runtime.submit_command(type, payload)
_expect(result.is_success(), "%s succeeds: %s" % [type, result.details])
return result
func _purchase(quantity: int) -> CommandResult:
return _runtime.submit_command(&"purchase_supplies", {&"warehouse_id": _ids[&"warehouse"], &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": quantity})
func _hire_and_assign(staff_id: StringName, facility: StringName) -> void:
_command(&"hire_staff_candidate", {&"candidate_id": staff_id})
_command(&"set_staff_work_assignment", {&"staff_id": staff_id, &"facility_id": _ids[facility], &"template_id": &"DAY"})
func _inventory(definition: StringName) -> StringName:
return _runtime.facility_service.get_instance(_ids[definition]).local_inventory_id
func _quantity(definition: StringName, item: StringName) -> int:
return _runtime.inventory_service.total_quantity(_inventory(definition), item)
func _incoming(definition: StringName, item: StringName) -> int:
var report: Dictionary = _runtime.get_supply_management_report()
var group := &"warehouses" if definition == &"warehouse" else &"kitchens"
for row: Dictionary in report[group]:
if StringName(row[&"facility_id"]) != _ids[definition]:
continue
for stock: Dictionary in row[&"stock"]:
if StringName(stock[&"item_id"]) == item:
return int(stock[&"in_transit"])
return -1
func _meal_requests() -> Array[LogisticsRequest]:
var requests: Array[LogisticsRequest] = []
for request: LogisticsRequest in _runtime.logistics_requests.values():
if request.item_requirements.has(SupplyCatalog.BASIC_MEAL):
requests.append(request)
return requests
func _raw_requests_for_kitchen() -> Array[LogisticsRequest]:
var requests: Array[LogisticsRequest] = []
for request: LogisticsRequest in _runtime.logistics_requests.values():
if request.destination_inventory_id == _inventory(&"kitchen") and request.item_requirements.has(SupplyCatalog.RAW_FOOD):
requests.append(request)
return requests
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
func _finish() -> void:
print("Supply management service: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures > 0 else 0)