//! 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()); }