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>
90 lines
3.3 KiB
Rust
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());
|
|
}
|