From 5a3733aac445057c2bf0236b236a89e9d6415c86 Mon Sep 17 00:00:00 2001 From: autocommit Date: Tue, 28 Apr 2026 16:04:39 -0700 Subject: [PATCH] =?UTF-8?q?test(engine):=20=E2=9C=85=20Update=20lifecycle?= =?UTF-8?q?=20unit=20tests=20for=20courier=20behavior=20and=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../tests/unit/test_courier_lifecycle.gd | 241 ++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 src/game/engine/tests/unit/test_courier_lifecycle.gd diff --git a/src/game/engine/tests/unit/test_courier_lifecycle.gd b/src/game/engine/tests/unit/test_courier_lifecycle.gd new file mode 100644 index 00000000..831cc9e6 --- /dev/null +++ b/src/game/engine/tests/unit/test_courier_lifecycle.gd @@ -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")