//! Phase H gate: "0 hidden-state leaks". The hidden ground truth must never //! cross the wire. These tests check the invariant both in-process (the //! authoritative match) and over the live socket (the serialized bytes). use game_runtime::{solo_roster, Match}; use protocol::{Action, ClientMessage, MatchId, ServerMessage}; use web_tests::{spawn_test_server, WsClient}; use world_model::{HIDDEN_LANES, LANES, NUM_DOMAINS}; #[test] fn visible_snapshot_redacts_all_hidden_lanes() { let mut m = Match::new(MatchId(1), 4242, solo_roster("dev")); // Mask half of the observed lanes so redaction is non-trivial too. for d in 0..NUM_DOMAINS { for l in 0..LANES { if (d + l) % 2 == 0 { m.world.observation_state.visible[d][l] = false; } } } // The generator may already mask lanes; count actual non-visible lanes. let masked: u32 = (0..NUM_DOMAINS) .flat_map(|d| (0..LANES).map(move |l| (d, l))) .filter(|&(d, l)| !m.world.observation_state.visible[d][l]) .count() as u32; m.set_program(1, vec![protocol::RuneTokenWire { op: 0, a: 1, b: 2, c: 3, imm: 1 }]); m.resolve_turn(&[(1, Action::Cast)]); let snap = m.visible_for(1); // Every masked observed lane is None. for vd in &snap.observed_domains { assert_eq!(vd.observed.len(), LANES); for (l, o) in vd.observed.iter().enumerate() { let visible = m.world.observation_state.visible[vd.index as usize][l]; assert_eq!(o.is_some(), visible, "lane visibility/value mismatch"); } } // Redaction count accounts for every masked observed lane and every hidden // lane in the world. let expected = masked + (NUM_DOMAINS * HIDDEN_LANES) as u32; assert_eq!(snap.hidden_state_redactions, expected); // The serialized snapshot must not carry a "hidden" key at all. let wire = snap.to_json().to_compact(); assert!(!wire.contains("\"hidden\""), "serialized snapshot mentions hidden state"); } #[test] fn no_server_message_over_the_wire_carries_hidden_keys() { let addr = spawn_test_server(12); let mut c = WsClient::connect(&addr).unwrap(); c.send(&ClientMessage::JoinMatch { name: "leak-check".into(), match_id: None }).unwrap(); let mut checked = 0; // Drive a few turns and scan every raw frame the server emits. let mut live_turn = 0u64; for _ in 0..6 { c.send(&ClientMessage::SubmitTurn { turn: live_turn, action: Action::Cast }).unwrap(); // Read several frames; scan each. for _ in 0..4 { let raw = match c.recv_text() { Ok(r) => r, Err(_) => break, }; checked += 1; assert!(!raw.contains("\"hidden\""), "raw frame leaked hidden key: {raw}"); // Track turn progression from resolved frames. if let Ok(ServerMessage::TurnResolved { turn, .. }) = ServerMessage::decode(&raw) { live_turn = turn; } } } assert!(checked > 0, "no frames scanned"); }