✅ test(@projects/@magic-civilization): ✨ add end-to-end ai promotion gut test
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
8204c095b4
commit
bd6a34c603
1 changed files with 120 additions and 0 deletions
|
|
@ -0,0 +1,120 @@
|
|||
extends GutTest
|
||||
## p2-44 — End-to-end GUT verification that AI-owned units promote and that
|
||||
## the `EventBus.unit_promoted` signal fires through the live dispatch path.
|
||||
##
|
||||
## Headless-compatible: no rendering, no UI. Drives the dispatch entry point
|
||||
## directly with a synthetic `PromotionPicked` action JSON, mirroring the
|
||||
## same envelope `AiTurnBridge` ships from Rust. Asserts:
|
||||
## 1. `unit.promote(promo_id)` applied (veteran_level += 1, promo_ids += id)
|
||||
## 2. `EventBus.unit_promoted` emitted exactly once with (unit, promo_id)
|
||||
## 3. `dispatch_action` returns true for the PromotionPicked variant
|
||||
##
|
||||
## This locks the GDScript half of the p2-44 chain — the Rust picker side is
|
||||
## covered by 5 unit tests in `mc-ai::tactical::promotion::tests`. The full
|
||||
## chain (build_tactical_state -> Rust pick_promotion -> dispatch) is gated
|
||||
## by the apricot 10-seed batch counting `unit_promoted` rows in events.jsonl.
|
||||
|
||||
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
|
||||
const DispatchScript: GDScript = preload(
|
||||
"res://engine/src/modules/ai/ai_turn_bridge_dispatch.gd"
|
||||
)
|
||||
|
||||
var _emitted: Array[Dictionary] = []
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
_emitted = []
|
||||
if EventBus.unit_promoted.is_connected(_on_unit_promoted):
|
||||
EventBus.unit_promoted.disconnect(_on_unit_promoted)
|
||||
EventBus.unit_promoted.connect(_on_unit_promoted)
|
||||
|
||||
|
||||
func after_each() -> void:
|
||||
if EventBus.unit_promoted.is_connected(_on_unit_promoted):
|
||||
EventBus.unit_promoted.disconnect(_on_unit_promoted)
|
||||
|
||||
|
||||
func _on_unit_promoted(unit: Variant, promotion: String) -> void:
|
||||
_emitted.append({"unit": unit, "promotion": promotion})
|
||||
|
||||
|
||||
func _make_ai_unit(uid: int) -> Unit:
|
||||
var unit: Unit = UnitScript.new("dwarf_warrior", 0, Vector2i(3, 3))
|
||||
unit.id = "ai_unit_%d" % uid
|
||||
unit.hp = 10
|
||||
unit.max_hp = 10
|
||||
unit.xp = 12
|
||||
unit.veteran_level = 0
|
||||
return unit
|
||||
|
||||
|
||||
func test_dispatch_promotion_picked_promotes_and_emits() -> void:
|
||||
# Arrange — synthetic AI unit eligible for tier-1 promotion.
|
||||
var unit: Unit = _make_ai_unit(7)
|
||||
var index_maps: Dictionary = {"units": {7: unit}, "cities": {}}
|
||||
var prev_level: int = unit.veteran_level
|
||||
var action_str: String = JSON.stringify({
|
||||
"PromotionPicked": {"unit_id": 7, "promotion_id": "shock"},
|
||||
})
|
||||
|
||||
# Act — drive the same entry point AiTurnBridge uses on AI turns.
|
||||
var ok: bool = DispatchScript.dispatch_action(
|
||||
action_str, null, index_maps, ""
|
||||
)
|
||||
|
||||
# Assert — return value, mutation, signal.
|
||||
assert_true(ok, "dispatch_action should accept PromotionPicked variant")
|
||||
assert_eq(
|
||||
unit.veteran_level, prev_level + 1,
|
||||
"veteran_level must increment on dispatch"
|
||||
)
|
||||
assert_true(
|
||||
unit.promo_ids.has("shock"),
|
||||
"promo_ids must contain the picked promotion"
|
||||
)
|
||||
assert_eq(
|
||||
_emitted.size(), 1,
|
||||
"EventBus.unit_promoted must fire exactly once per dispatch"
|
||||
)
|
||||
if _emitted.size() == 1:
|
||||
assert_eq(
|
||||
_emitted[0]["promotion"], "shock",
|
||||
"signal payload promotion id must match dispatched id"
|
||||
)
|
||||
assert_eq(
|
||||
_emitted[0]["unit"], unit,
|
||||
"signal payload unit must be the same RefCounted instance"
|
||||
)
|
||||
|
||||
|
||||
func test_dispatch_rejects_dead_unit() -> void:
|
||||
# A dead AI unit must not promote — guards against late dispatch races.
|
||||
var unit: Unit = _make_ai_unit(11)
|
||||
unit.hp = 0
|
||||
var index_maps: Dictionary = {"units": {11: unit}, "cities": {}}
|
||||
var action_str: String = JSON.stringify({
|
||||
"PromotionPicked": {"unit_id": 11, "promotion_id": "shock"},
|
||||
})
|
||||
|
||||
var ok: bool = DispatchScript.dispatch_action(
|
||||
action_str, null, index_maps, ""
|
||||
)
|
||||
|
||||
assert_false(ok, "dispatch_action must reject dead-unit promotion")
|
||||
assert_eq(unit.veteran_level, 0, "dead unit must not promote")
|
||||
assert_eq(_emitted.size(), 0, "no signal must fire for dead-unit dispatch")
|
||||
|
||||
|
||||
func test_dispatch_rejects_empty_promotion_id() -> void:
|
||||
var unit: Unit = _make_ai_unit(13)
|
||||
var index_maps: Dictionary = {"units": {13: unit}, "cities": {}}
|
||||
var action_str: String = JSON.stringify({
|
||||
"PromotionPicked": {"unit_id": 13, "promotion_id": ""},
|
||||
})
|
||||
|
||||
var ok: bool = DispatchScript.dispatch_action(
|
||||
action_str, null, index_maps, ""
|
||||
)
|
||||
|
||||
assert_false(ok, "empty promotion_id must be rejected")
|
||||
assert_eq(_emitted.size(), 0, "no signal for empty promotion_id")
|
||||
Loading…
Add table
Reference in a new issue