magicciv/src/simulator/api-gdext/tests/ai_controller.rs
Natalie d4c4a5aa1b fix(test): add missing fields to stale struct literals across workspace tests
Four E0063 compile errors broke `cargo test --workspace --no-run`, blocking
`./run dist:test` on the DO fleet. Each is a stale struct literal in test/test-cfg
code that drifted from its current definition:

- mc-worldsim event_dispatch low_bio_thresholds: BiologicalThresholds missing
  migration_drought_factor / migration_drought_max (p3-21 drought coupling) —
  set to 0.0 / 1.0 to keep the helper's migration-suppression intent.
- mc-mod-host wasm_controller_{noop,limits}: TacticalState missing embark_level —
  Default::default() (EmbarkLevel::None) to match the empty-state intent.
- api-gdext ai.rs tile_with + ai_controller test: TacticalTile missing explored /
  TacticalState missing embark_level — explored:true (pre-field default = seen),
  embark_level default.

Mirrors the sibling fix 04fabbc1c. `cargo test --workspace --no-run` now compiles
clean; full suite passes except 3 pre-existing GPU-parity tests (Metal fp drift,
unrelated to these changes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:35:44 -04:00

90 lines
3.3 KiB
Rust

//! Integration tests for the `GdAiController` bridge surface.
//!
//! Godot's runtime is not available during `cargo test`, so these tests
//! exercise the pure-Rust helpers (`parse_tactical_state_json`,
//! `player_index_to_slot`, `run_tactical`) rather than the `#[func]`-decorated
//! instance methods. The bridge class itself is constructed under a live
//! runtime during the p0-26 phase-gate proof scene.
use magic_civ_physics_gdext::ai::{
parse_tactical_state_json, player_index_to_slot,
};
use mc_ai::abstract_state::MAX_PLAYERS;
use mc_ai::tactical::{TacticalMap, TacticalState};
/// (1) Constructor smoke — the bridge's helper surface is linkable and
/// `player_index_to_slot` behaves at the slot boundaries. This exercises
/// the same validation path the `#[func] fn decide_actions` runs before
/// dispatching to `decide_tactical_actions`.
#[test]
fn constructor_smoke_player_index_bounds() {
assert!(
player_index_to_slot(-1).is_err(),
"negative player_index must error"
);
assert_eq!(player_index_to_slot(0).unwrap(), 0);
assert_eq!(
player_index_to_slot((MAX_PLAYERS - 1) as i64).unwrap(),
MAX_PLAYERS - 1
);
// Out-of-range indices cap to the last slot (graceful degradation for games
// with more players than MAX_PLAYERS, e.g. the 5-clan demo) rather than
// erroring and taking no actions — see player_index_to_slot in ai.rs.
assert_eq!(
player_index_to_slot(MAX_PLAYERS as i64).unwrap(),
MAX_PLAYERS - 1,
"player_index >= MAX_PLAYERS must cap to the last slot"
);
}
/// (2) Empty state JSON produces an empty dispatch. The bridge's
/// `decide_actions` returns an empty `PackedStringArray` on parse failure —
/// we verify the parse step itself returns `Err` so the caller's empty-array
/// fallback kicks in.
#[test]
fn empty_state_json_produces_parse_error() {
assert!(
parse_tactical_state_json("").is_err(),
"empty string must not parse"
);
assert!(
parse_tactical_state_json(" \n\t ").is_err(),
"whitespace-only string must not parse"
);
assert!(
parse_tactical_state_json("not-json").is_err(),
"non-JSON must not parse"
);
assert!(
parse_tactical_state_json("[]").is_err(),
"JSON array at the top level is not a valid TacticalState"
);
}
/// Supplementary: a well-formed minimal `TacticalState` round-trips through
/// the bridge's parser. This anchors the contract with GDScript's
/// `_build_tactical_state_json` — any field-name drift in `TacticalState`
/// surfaces as a parse failure here.
#[test]
fn minimal_tactical_state_json_parses() {
let minimal = TacticalState {
current_player: 0,
turn: 0,
map: TacticalMap {
width: 0,
height: 0,
tiles: Vec::new(),
},
players: Vec::new(),
unit_catalog: Vec::new(),
building_catalog: Vec::new(),
difficulty_threshold_mult: 1.0,
embark_level: Default::default(),
};
let json = serde_json::to_string(&minimal).expect("serialize");
let parsed = parse_tactical_state_json(&json).expect("parse");
assert_eq!(parsed.current_player, 0);
assert_eq!(parsed.turn, 0);
assert_eq!(parsed.map.width, 0);
assert!(parsed.players.is_empty());
}