test(engine): Update lifecycle unit tests for courier behavior and coverage

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-28 16:04:39 -07:00
parent 0832067691
commit 5a3733aac4

View file

@ -0,0 +1,241 @@
extends GutTest
## p3-01 bullet 10 — GUT headless tests for courier lifecycle.
##
## Six scenarios: route resolution, intercept, payment-vs-delivery,
## tier upgrade, infrastructure severance, agreement expiry.
##
## All tests run headless (no display server). GdTrade, GdTradeLedger,
## GdCourierRoute, GdOpenBordersAgreement, GdSharedMapAgreement, and
## GdCourierMapView are GDExtension types instantiated via ClassDB.
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
func _courier_bindings_available() -> bool:
## GdTradeLedger is the sentinel: it only exists once the courier
## diplomacy bindings are compiled into the GDExtension.
return ClassDB.class_exists("GdTradeLedger")
func _make_trade() -> RefCounted:
return ClassDB.instantiate("GdTrade") as RefCounted
func _make_ledger() -> RefCounted:
return ClassDB.instantiate("GdTradeLedger") as RefCounted
func _make_map_view() -> RefCounted:
## Headless map view: constructed from a default GdGameState handle.
## step_shared_map_agreements calls route_intact / adamantine_echo_active
## on the view; for agreement-lifecycle tests those are driven by
## agreement state, not the real grid.
var gs: RefCounted = ClassDB.instantiate("GdGameState") as RefCounted
return ClassDB.instantiate("GdCourierMapView").call("from_game_state", gs)
func _collect_events_of_type(events: Array, event_type: String) -> Array:
var result: Array = []
for ev: Dictionary in events:
if ev.get("type", "") == event_type:
result.append(ev)
return result
# ---------------------------------------------------------------------------
# Scenario 1 — Route resolution: higher courier tier produces shorter ETA
## (era_2 tier 2 vs era_8 tier 8 on the same capital pair)
# ---------------------------------------------------------------------------
func test_higher_tier_produces_shorter_eta() -> void:
if not _courier_bindings_available():
pending("GdTradeLedger not found — courier GDExtension bindings not compiled in")
return
var trade: RefCounted = _make_trade()
var route_slow: RefCounted = trade.call("courier_route_new", 0, 1)
route_slow.call("set_courier_era_tier", 2)
var route_fast: RefCounted = trade.call("courier_route_new", 0, 1)
route_fast.call("set_courier_era_tier", 8)
var eta_slow: int = route_slow.call("get_eta_turn")
var eta_fast: int = route_fast.call("get_eta_turn")
assert_true(
eta_fast <= eta_slow,
"era_8 tier should produce equal-or-shorter ETA than era_2 tier"
)
# ---------------------------------------------------------------------------
# Scenario 2 — Intercept: no map delivered; payment retained in agreement
# ---------------------------------------------------------------------------
func test_courier_intercept_no_delivery_payment_retained() -> void:
if not _courier_bindings_available():
pending("GdTradeLedger not found — courier GDExtension bindings not compiled in")
return
var trade: RefCounted = _make_trade()
var ledger: RefCounted = _make_ledger()
var map_view: RefCounted = _make_map_view()
var agreement: RefCounted = trade.call("shared_map_agreement_new", 0, 1, 50, 10)
var route: RefCounted = trade.call("courier_route_new", 0, 1)
route.call("force_intercepted")
agreement.call("set_courier_route", route)
ledger.call("add_shared_map", agreement)
var events: Array = trade.call("step_shared_map_agreements", ledger, map_view, 1)
var intercept_events: Array = _collect_events_of_type(events, "courier_intercepted")
assert_eq(intercept_events.size(), 1, "exactly one courier_intercepted event expected")
assert_eq(
intercept_events[0].get("agreement_id"),
agreement.call("get_agreement_id"),
"intercepted event must carry the correct agreement_id"
)
var deliver_events: Array = _collect_events_of_type(events, "shared_map_delivered")
assert_eq(deliver_events.size(), 0, "no delivery event when courier intercepted")
assert_eq(agreement.call("get_payment_gold"), 50,
"payment_gold must be retained in agreement after intercept")
# ---------------------------------------------------------------------------
# Scenario 3 — Payment-vs-delivery: payment recorded at agreement creation
# ---------------------------------------------------------------------------
func test_payment_recorded_at_agreement_creation() -> void:
if not _courier_bindings_available():
pending("GdTradeLedger not found — courier GDExtension bindings not compiled in")
return
var trade: RefCounted = _make_trade()
var ob: RefCounted = trade.call("open_borders_agreement_new", 0, 1, 80, 5)
assert_eq(ob.call("get_payment_gold"), 80,
"open_borders payment_gold must equal the value passed at creation")
assert_eq(ob.call("get_player_a"), 0)
assert_eq(ob.call("get_player_b"), 1)
assert_eq(ob.call("get_turns_remaining"), 5)
var sm: RefCounted = trade.call("shared_map_agreement_new", 2, 3, 120, 8)
assert_eq(sm.call("get_payment_gold"), 120,
"shared_map payment_gold must equal the value passed at creation")
assert_false(sm.call("is_delivered"),
"shared_map must not be delivered at creation time")
# ---------------------------------------------------------------------------
# Scenario 4 — Tier upgrade: era_8 ETA <= era_2 ETA after step
# ---------------------------------------------------------------------------
func test_tier_upgrade_shrinks_eta() -> void:
if not _courier_bindings_available():
pending("GdTradeLedger not found — courier GDExtension bindings not compiled in")
return
var trade: RefCounted = _make_trade()
var map_view: RefCounted = _make_map_view()
var ledger_slow: RefCounted = _make_ledger()
var sm_slow: RefCounted = trade.call("shared_map_agreement_new", 0, 1, 50, 20)
var route_slow: RefCounted = trade.call("courier_route_new", 0, 1)
route_slow.call("set_courier_era_tier", 2)
sm_slow.call("set_courier_route", route_slow)
ledger_slow.call("add_shared_map", sm_slow)
var ledger_fast: RefCounted = _make_ledger()
var sm_fast: RefCounted = trade.call("shared_map_agreement_new", 0, 1, 50, 20)
var route_fast: RefCounted = trade.call("courier_route_new", 0, 1)
route_fast.call("set_courier_era_tier", 8)
sm_fast.call("set_courier_route", route_fast)
ledger_fast.call("add_shared_map", sm_fast)
trade.call("step_shared_map_agreements", ledger_slow, map_view, 1)
trade.call("step_shared_map_agreements", ledger_fast, map_view, 1)
var eta_slow: int = route_slow.call("get_eta_turn")
var eta_fast: int = route_fast.call("get_eta_turn")
assert_true(eta_fast <= eta_slow,
"era_8 route ETA must be <= era_2 route ETA after first step")
# ---------------------------------------------------------------------------
# Scenario 5 — Infrastructure severance: severed route → intercept on next step
# ---------------------------------------------------------------------------
func test_infrastructure_severance_triggers_intercept() -> void:
if not _courier_bindings_available():
pending("GdTradeLedger not found — courier GDExtension bindings not compiled in")
return
var trade: RefCounted = _make_trade()
var ledger: RefCounted = _make_ledger()
var map_view: RefCounted = _make_map_view()
var sm: RefCounted = trade.call("shared_map_agreement_new", 0, 1, 60, 15)
var route: RefCounted = trade.call("courier_route_new", 0, 1)
route.call("set_courier_era_tier", 7)
route.call("mark_infrastructure_severed")
sm.call("set_courier_route", route)
ledger.call("add_shared_map", sm)
var events: Array = trade.call("step_shared_map_agreements", ledger, map_view, 3)
var intercept_events: Array = _collect_events_of_type(events, "courier_intercepted")
assert_eq(intercept_events.size(), 1,
"severed infrastructure must emit courier_intercepted on next step")
assert_eq(
intercept_events[0].get("agreement_id"),
sm.call("get_agreement_id"),
"intercepted event must carry the correct agreement_id"
)
assert_false(sm.call("is_delivered"),
"agreement must not be delivered after infrastructure severance")
# ---------------------------------------------------------------------------
# Scenario 6 — Agreement expiry: OpenBorders decrements and fires expiry event
# ---------------------------------------------------------------------------
func test_open_borders_decrements_and_expires() -> void:
if not _courier_bindings_available():
pending("GdTradeLedger not found — courier GDExtension bindings not compiled in")
return
var trade: RefCounted = _make_trade()
var map_view: RefCounted = _make_map_view()
var ob: RefCounted = trade.call("open_borders_agreement_new", 0, 1, 40, 3)
var agreement_id: int = ob.call("get_agreement_id")
var ledger: RefCounted = _make_ledger()
ledger.call("add_open_borders", ob)
# Turn 1 — turns_remaining 3 → 2, no expiry yet.
var events_t1: Array = trade.call("step_shared_map_agreements", ledger, map_view, 1)
assert_eq(_collect_events_of_type(events_t1, "open_borders_expired").size(), 0,
"no expiry event when turns_remaining > 0")
var remaining_t1: int = ledger.call("iter_open_borders")[0].call("get_turns_remaining")
assert_eq(remaining_t1, 2, "turns_remaining must decrement from 3 to 2")
# Turn 2 — turns_remaining 2 → 1.
trade.call("step_shared_map_agreements", ledger, map_view, 2)
var remaining_t2: int = ledger.call("iter_open_borders")[0].call("get_turns_remaining")
assert_eq(remaining_t2, 1, "turns_remaining must decrement from 2 to 1")
# Turn 3 — turns_remaining 1 → 0; expiry event must fire.
var events_t3: Array = trade.call("step_shared_map_agreements", ledger, map_view, 3)
var expiry_events: Array = _collect_events_of_type(events_t3, "open_borders_expired")
assert_eq(expiry_events.size(), 1,
"open_borders_expired event must fire when turns_remaining reaches 0")
assert_eq(expiry_events[0].get("agreement_id"), agreement_id,
"expiry event must carry the correct agreement_id")