From 24c0e0c24c611a1b5270dd8dacf048a39e2326cf Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 27 Jun 2026 09:44:39 -0400 Subject: [PATCH] =?UTF-8?q?test(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=9B=A4=EF=B8=8F=20Rail-1=20Phase-1=20=E2=80=94=20end-to-e?= =?UTF-8?q?nd=20live-unit-store=20loop=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proves the spawn → command → view contract the GdGameState bridge exposes for the render-gated live flip, at the mc_player_api layer its shims call: a MapUnit pushed onto inner (as spawn_unit_into_inner produces) appears in project_view; a Fortify via apply_action is reflected in the next view; a command on a stale unit id is a typed error, not a panic. Existing integration tests load pre-built states — none exercised the spawn-then-act-then-view triple a freshly-spawned live unit goes through. De-risks the foundation before the GDScript flip depends on it. mc-player-api 3/3. Co-Authored-By: Claude Opus 4.8 --- .../tests/live_unit_store_loop.rs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/simulator/crates/mc-player-api/tests/live_unit_store_loop.rs diff --git a/src/simulator/crates/mc-player-api/tests/live_unit_store_loop.rs b/src/simulator/crates/mc-player-api/tests/live_unit_store_loop.rs new file mode 100644 index 00000000..e819dd9a --- /dev/null +++ b/src/simulator/crates/mc-player-api/tests/live_unit_store_loop.rs @@ -0,0 +1,73 @@ +//! Rail-1 Phase-1 — the live unit-store loop, end-to-end. +//! +//! Proves the exact three-call contract the new `GdGameState` bridge exposes for +//! the (later, render-gated) live flip — `spawn_unit_into_inner` → +//! `apply_action_json` → `inner_view_json` — at the `mc_player_api` layer those +//! shims call into. The existing integration tests load pre-built states; none +//! exercise the **spawn → command → view** triple a freshly-spawned live unit +//! goes through. This is that proof: a unit pushed onto `inner` shows up in the +//! projected view and responds to a command, with the command's effect visible +//! in the next view. + +use mc_player_api::{apply_action, project_view, PlayerAction}; +use mc_state::game_state::{GameState, MapUnit, PlayerState}; + +/// Build a one-player `GameState` holding a single durable unit at `(2, 2)`, +/// mirroring what `GdGameState::spawn_unit_into_inner` produces (a `MapUnit` +/// pushed onto `players[pi].units`). +fn state_with_spawned_unit() -> GameState { + let mut state = GameState::default(); + state.players.push(PlayerState { + units: vec![MapUnit { + id: 1, + col: 2, + row: 2, + hp: 10, + max_hp: 10, + unit_id: "dwarf_warrior".into(), + ..Default::default() + }], + ..Default::default() + }); + state +} + +#[test] +fn spawned_unit_appears_in_the_projected_view() { + let state = state_with_spawned_unit(); + // `inner_view_json` → project_view(omniscient=true is the harness path; the + // bound player sees its own units regardless of fog). + let view = project_view(&state, 0, true); + let u = view + .units + .iter() + .find(|u| u.id == "1") + .expect("the spawned unit must appear in the owner's view"); + assert_eq!(u.position, [2, 2], "view carries the spawn position"); + assert_eq!(u.type_id, "dwarf_warrior"); + assert!(!u.fortified, "freshly spawned unit is not fortified"); +} + +#[test] +fn command_via_act_is_reflected_in_the_next_view() { + let mut state = state_with_spawned_unit(); + // act(): Fortify the unit (no grid/movement preconditions — isolates the + // act→view round-trip from pathfinding). + let res = apply_action(&mut state, 0, &PlayerAction::Fortify { unit_id: "1".into() }); + assert!(res.is_ok(), "Fortify must dispatch on the live store: {res:?}"); + + // The very next view reflects the command — the contract the live renderer + // relies on (render strictly from getState() after each act()). + let view = project_view(&state, 0, true); + let u = view.units.iter().find(|u| u.id == "1").expect("unit still present"); + assert!(u.fortified, "the view must show the unit fortified after the act()"); +} + +#[test] +fn act_on_a_missing_unit_is_rejected_not_panicking() { + let mut state = state_with_spawned_unit(); + // The bridge must surface a clean error (not panic) for a stale unit id — + // the live UI can desync transiently and must get a typed rejection. + let res = apply_action(&mut state, 0, &PlayerAction::Fortify { unit_id: "999".into() }); + assert!(res.is_err(), "commanding a non-existent unit must be a typed error"); +}