feat(simulation): Sprint 19 — 7 server systems
Protocol handshake (#555): HandshakeMessage as first IPC frame, HandshakeState resource, forward-compatible input handling. State serialization (#96): serialize_npc_to_frozen/deserialize with full D-024 axis coverage (10 new optional fields on NpcSaveState). Scope tags (#98): ScopeTagKind enum, ScopePinned marker, automatic assignment from KnowledgeGraph and RelationshipGraph. Timestamp eviction (#97): LastInteractionTick, SimSpacePressure, BinaryHeap LRU eviction respecting ScopePinned entities. Save/load (#553): save_to_file/load_from_file via MessagePack, SaveGame/LoadGame IPC commands, SaveLoadResultWire on snapshot. Test infrastructure (#200): Layer 3 integration test entry point, three-layer architecture documented per D-030. Information boundary tests (#272): 4 negative tests proving no passive KG leakage, LOS fog holds, tier boundary holds, save isolation per NPC. 1063 tests passing, 0 failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,12 +66,12 @@ fn snapshot_roundtrip_over_unix_socket() {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
examine_result: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
examine_result: None,
|
||||
player_knowledge: None,
|
||||
save_result: None,
|
||||
};
|
||||
|
||||
bridge
|
||||
|
||||
@@ -52,12 +52,12 @@ fn snapshot_roundtrip_over_tcp() {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
examine_result: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
examine_result: None,
|
||||
player_knowledge: None,
|
||||
save_result: None,
|
||||
};
|
||||
|
||||
bridge
|
||||
|
||||
@@ -41,12 +41,12 @@ fn fixture_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
examine_result: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
examine_result: None,
|
||||
player_knowledge: None,
|
||||
save_result: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,12 +232,12 @@ fn generate_msgpack_fixtures() {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
examine_result: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
examine_result: None,
|
||||
player_knowledge: None,
|
||||
save_result: None,
|
||||
};
|
||||
write_fixture(
|
||||
"snapshot_v2_full",
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
"z": 0
|
||||
}
|
||||
],
|
||||
"examine_result": null,
|
||||
"follow_state": null,
|
||||
"game_time": {
|
||||
"day": 0,
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
//! Information boundary negative test suite (D-010, D-030, ticket #272).
|
||||
//!
|
||||
//! THE core asymmetric information claim: entity X cannot see what entity Y
|
||||
//! knows, unless the observation system explicitly grants it.
|
||||
//!
|
||||
//! These are NEGATIVE tests — they assert that information does NOT cross
|
||||
//! boundaries. Each test uses `assert!(x.is_none())` or equivalent absence
|
||||
//! patterns, not just "test passed because nothing happened."
|
||||
//!
|
||||
//! ## Test layers (D-030)
|
||||
//!
|
||||
//! Layer 1 (pure unit, no ECS):
|
||||
//! - `player_kg_has_no_passive_npc_leakage` — KG starts empty, stays empty
|
||||
//! - `save_state_npc_kg_isolation` — per-NPC KG serialization isolation
|
||||
//! - `snapshot_excludes_entities_outside_los` — FOV geometry excludes far tiles
|
||||
//!
|
||||
//! Layer 2 (minimal ECS world, no subprocess):
|
||||
//! - `background_npc_kg_not_updated_by_active_tier_events` — tier boundary holds
|
||||
//!
|
||||
//! Spec references: D-010 (info boundaries), D-026 (tiers), D-030 (testability),
|
||||
//! D-041 (knowledge graph), Q-029 (save format)
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_ecs::schedule::Schedule;
|
||||
|
||||
use settled_reach_server::knowledge::events::{
|
||||
process_knowledge_events, KnowledgeEvent, KnowledgeEventQueue, KnowledgeEventType,
|
||||
};
|
||||
use settled_reach_server::knowledge::{
|
||||
ContradictionDetectedQueue, EntityRegistry, KnowledgeGraph,
|
||||
};
|
||||
use settled_reach_server::knowledge::types::StableId;
|
||||
use settled_reach_server::npc::{Npc, SecretSeverity};
|
||||
use settled_reach_server::npc::relationships::RelationshipGraph;
|
||||
use settled_reach_server::perception::query::{NaturalVision, PerceptionQuery};
|
||||
use settled_reach_server::simulation::movement::{TilePosition, WalkabilityMap};
|
||||
use settled_reach_server::simulation::save_state::{NpcSaveState, SaveStateV1, SAVE_FORMAT_VERSION};
|
||||
use settled_reach_server::simulation::tier::{ActiveSim, BackgroundSim};
|
||||
use settled_reach_server::simulation::time::TickRate;
|
||||
use settled_reach_server::bridge::types::FacingDirection;
|
||||
|
||||
// ===========================================================================
|
||||
// Layer 1 — Pure unit: no ECS world, no subprocess
|
||||
// ===========================================================================
|
||||
|
||||
/// IB-1 (Layer 1): A fresh KnowledgeGraph contains no entries for any entity.
|
||||
///
|
||||
/// Core claim: player knowledge is never passively populated. The KG starts
|
||||
/// empty and can only be written by `observe_entity()`, `record_knowledge()`,
|
||||
/// or knowledge events processed by `process_knowledge_events`. Simply
|
||||
/// existing in the simulation world does not leak an NPC's existence into
|
||||
/// the player's knowledge graph.
|
||||
///
|
||||
/// Spec reference: D-010 principle 2 (information boundaries as first-class system)
|
||||
#[test]
|
||||
fn player_kg_has_no_passive_npc_leakage() {
|
||||
let player_kg = KnowledgeGraph::new();
|
||||
let npc_id = StableId(42);
|
||||
|
||||
// Negative assertion: a freshly created KG contains no entity references.
|
||||
assert!(
|
||||
player_kg.entities.get(&npc_id).is_none(),
|
||||
"IB-1: fresh KnowledgeGraph must not contain any entity (passive leakage — D-010 principle 2)"
|
||||
);
|
||||
assert!(
|
||||
player_kg.is_empty(),
|
||||
"IB-1: KnowledgeGraph::new() must be completely empty"
|
||||
);
|
||||
|
||||
// Negative assertion: spawning a bare ECS entity doesn't populate a KG.
|
||||
// The knowledge graph is a component, not a global shared resource.
|
||||
let mut world = World::new();
|
||||
let player = world
|
||||
.spawn(KnowledgeGraph::new())
|
||||
.id();
|
||||
|
||||
// Spawn an NPC in the same world — no observation system runs.
|
||||
let _npc = world.spawn((Npc, TilePosition::new(50, 50, 0))).id();
|
||||
|
||||
// Player's KG must be empty regardless of NPCs existing nearby.
|
||||
let kg = world.get::<KnowledgeGraph>(player).unwrap();
|
||||
assert!(
|
||||
kg.entities.get(&npc_id).is_none(),
|
||||
"IB-1: spawning an NPC in the world must not passively populate the player's KG"
|
||||
);
|
||||
assert!(
|
||||
kg.is_empty(),
|
||||
"IB-1: player KG must stay empty until an observation system explicitly populates it"
|
||||
);
|
||||
}
|
||||
|
||||
/// IB-2 (Layer 1): FOV geometry excludes positions beyond the vision range.
|
||||
///
|
||||
/// The observer snapshot system (compute_observer_snapshot) includes entities
|
||||
/// by testing whether their tile position is in `VisibilityGeometry.visible_positions`.
|
||||
/// This test verifies that the FOV computation — the upstream source of that set —
|
||||
/// correctly excludes positions far from the observer, so no entity outside LOS
|
||||
/// can ever appear in the snapshot.
|
||||
///
|
||||
/// Spec reference: D-010 principle 2, D-011 (symmetric shadowcasting), D-030 Layer 1
|
||||
#[test]
|
||||
fn snapshot_excludes_entities_outside_los() {
|
||||
// All-walkable 100×100 map at z=0 — no walls to cast shadows.
|
||||
let walkability = WalkabilityMap::new(100, 100, 1);
|
||||
let observer_pos = TilePosition::new(5, 5, 0);
|
||||
let facing = FacingDirection::North;
|
||||
|
||||
let geometry = NaturalVision.compute_geometry(&observer_pos, facing, &walkability);
|
||||
|
||||
// --- Far entity: 45 tiles away, well outside FOV range (~12 tiles) ---
|
||||
let far_npc_pos = TilePosition::new(50, 5, 0);
|
||||
assert!(
|
||||
!geometry.visible_positions.contains(&(far_npc_pos.x, far_npc_pos.y)),
|
||||
"IB-2: entity at {:?} (45 tiles from observer) must NOT be in FOV — \
|
||||
observer snapshot would exclude this entity (fog of perception, D-010 principle 2)",
|
||||
far_npc_pos
|
||||
);
|
||||
|
||||
// --- Sanity check: the observer's own position is visible ---
|
||||
assert!(
|
||||
geometry.visible_positions.contains(&(observer_pos.x, observer_pos.y)),
|
||||
"IB-2 sanity: observer's own position must always be in the FOV set"
|
||||
);
|
||||
|
||||
// --- Additional sanity: an immediately adjacent tile (1 step) is visible ---
|
||||
let adjacent_pos = TilePosition::new(6, 5, 0);
|
||||
assert!(
|
||||
geometry.visible_positions.contains(&(adjacent_pos.x, adjacent_pos.y)),
|
||||
"IB-2 sanity: tile immediately adjacent to observer must be visible"
|
||||
);
|
||||
}
|
||||
|
||||
/// IB-4 (Layer 1): NPC save states do not bleed each other's KnowledgeGraphs.
|
||||
///
|
||||
/// `SaveStateV1.npc_states` is a flat `Vec<NpcSaveState>`. Each `NpcSaveState`
|
||||
/// has its own optional `knowledge_graph: Option<KnowledgeGraph>`. After a
|
||||
/// serialise → deserialise roundtrip:
|
||||
/// - NPC_A's `NpcSaveState.knowledge_graph` contains ONLY NPC_A's own KG.
|
||||
/// - NPC_B's `NpcSaveState.knowledge_graph` is `None` (Background tier,
|
||||
/// no KG carried) — it must not be overwritten by NPC_A's KG data.
|
||||
///
|
||||
/// Spec reference: D-010 principle 2, D-026 (tier serialization), Q-029 (save format)
|
||||
#[test]
|
||||
fn save_state_npc_kg_isolation() {
|
||||
let npc_a_id = StableId(1);
|
||||
let npc_b_id = StableId(2);
|
||||
|
||||
// NPC_A (Active tier) carries a KG that has observed NPC_B.
|
||||
let mut npc_a_kg = KnowledgeGraph::new();
|
||||
// NPC_A has observed NPC_B at some position — this puts NPC_B in NPC_A's KG.
|
||||
let _ = npc_a_kg.observe_entity(npc_b_id, TilePosition::new(10, 10, 0), 5);
|
||||
|
||||
let npc_a_state = NpcSaveState {
|
||||
stable_id: npc_a_id,
|
||||
position: TilePosition::new(5, 5, 0),
|
||||
secret_severity: SecretSeverity::Minor,
|
||||
relationships: None,
|
||||
current_stress: 0,
|
||||
tolerance_threshold: 20,
|
||||
contentment: 50,
|
||||
knowledge_graph: Some(npc_a_kg), // Active NPC carries KG
|
||||
want: None,
|
||||
secret: None,
|
||||
routine: None,
|
||||
information_inventory: None,
|
||||
personality_traits: None,
|
||||
tell_system: None,
|
||||
skill_set: None,
|
||||
combat_capability: None,
|
||||
mood_state: None,
|
||||
job_performance: None,
|
||||
};
|
||||
|
||||
// NPC_B (Background tier) does not carry a KG.
|
||||
let npc_b_state = NpcSaveState {
|
||||
stable_id: npc_b_id,
|
||||
position: TilePosition::new(20, 20, 0),
|
||||
secret_severity: SecretSeverity::Minor,
|
||||
relationships: None,
|
||||
current_stress: 0,
|
||||
tolerance_threshold: 20,
|
||||
contentment: 50,
|
||||
knowledge_graph: None, // Background NPC carries no KG
|
||||
want: None,
|
||||
secret: None,
|
||||
routine: None,
|
||||
information_inventory: None,
|
||||
personality_traits: None,
|
||||
tell_system: None,
|
||||
skill_set: None,
|
||||
combat_capability: None,
|
||||
mood_state: None,
|
||||
job_performance: None,
|
||||
};
|
||||
|
||||
let save = SaveStateV1 {
|
||||
format_version: SAVE_FORMAT_VERSION,
|
||||
tick: 10,
|
||||
tick_rate: TickRate::Full,
|
||||
seed: 42,
|
||||
player_knowledge: KnowledgeGraph::new(),
|
||||
relationship_graph: RelationshipGraph::new(),
|
||||
npc_states: vec![npc_a_state, npc_b_state],
|
||||
};
|
||||
|
||||
// Roundtrip: serialize → deserialize.
|
||||
let bytes = save.to_bytes().expect("IB-4: serialize SaveStateV1");
|
||||
let recovered = SaveStateV1::from_bytes(&bytes).expect("IB-4: deserialize SaveStateV1");
|
||||
|
||||
// --- Negative assertion: NPC_B's state must NOT contain a KnowledgeGraph ---
|
||||
let npc_b_recovered = recovered
|
||||
.npc_states
|
||||
.iter()
|
||||
.find(|s| s.stable_id == npc_b_id)
|
||||
.expect("IB-4: NPC_B must be present in recovered npc_states");
|
||||
|
||||
assert!(
|
||||
npc_b_recovered.knowledge_graph.is_none(),
|
||||
"IB-4: NPC_B's recovered state must not contain a KnowledgeGraph — \
|
||||
serialization must not bleed NPC_A's KG data into NPC_B's entry (D-010 principle 2)"
|
||||
);
|
||||
|
||||
// --- Sanity: NPC_A's state must contain its own KG (not lost in roundtrip) ---
|
||||
let npc_a_recovered = recovered
|
||||
.npc_states
|
||||
.iter()
|
||||
.find(|s| s.stable_id == npc_a_id)
|
||||
.expect("IB-4: NPC_A must be present in recovered npc_states");
|
||||
|
||||
let kg = npc_a_recovered
|
||||
.knowledge_graph
|
||||
.as_ref()
|
||||
.expect("IB-4: NPC_A's KG must survive roundtrip");
|
||||
|
||||
// NPC_A's KG entry for NPC_B is NPC_A's OBSERVATION DATA (where NPC_A saw NPC_B).
|
||||
// This is not NPC_B's own KG — it's NPC_A's record of NPC_B's position.
|
||||
assert!(
|
||||
kg.entities.get(&npc_b_id).is_some(),
|
||||
"IB-4 sanity: NPC_A's KG should still contain its observation of NPC_B after roundtrip"
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Layer 2 — Minimal ECS world (no subprocess)
|
||||
// ===========================================================================
|
||||
|
||||
/// IB-3 (Layer 2): `process_knowledge_events` only modifies the observer entity.
|
||||
///
|
||||
/// Background-tier NPC KnowledgeGraphs must not be modified when Active-tier
|
||||
/// events are processed. The `process_knowledge_events` system routes events
|
||||
/// via `event.observer` (an ECS Entity handle) — only the targeted entity's KG
|
||||
/// is written. This test confirms that a Background-tier NPC, not named in any
|
||||
/// event's `observer` field, has its KG left completely unchanged.
|
||||
///
|
||||
/// Spec reference: D-010 principle 2, D-026 (tier boundary), D-030 Layer 2
|
||||
#[test]
|
||||
fn background_npc_kg_not_updated_by_active_tier_events() {
|
||||
let mut world = World::new();
|
||||
|
||||
// Required resources for process_knowledge_events.
|
||||
world.init_resource::<KnowledgeEventQueue>();
|
||||
world.init_resource::<ContradictionDetectedQueue>();
|
||||
world.init_resource::<EntityRegistry>();
|
||||
|
||||
// Active-tier NPC: will be the observer in the knowledge event.
|
||||
let active_npc = world
|
||||
.spawn((Npc, ActiveSim, KnowledgeGraph::new()))
|
||||
.id();
|
||||
|
||||
// Background-tier NPC: must NOT be affected.
|
||||
let background_npc = world
|
||||
.spawn((Npc, BackgroundSim, KnowledgeGraph::new()))
|
||||
.id();
|
||||
|
||||
// A separate "observed" entity (the target of the DirectObservation).
|
||||
// Register it in the EntityRegistry so process_knowledge_events can resolve its StableId.
|
||||
let observed_entity = world.spawn_empty().id();
|
||||
{
|
||||
let mut registry = world.resource_mut::<EntityRegistry>();
|
||||
registry.register(observed_entity);
|
||||
}
|
||||
|
||||
// Push a DirectObservation event targeting only the Active NPC as observer.
|
||||
// The Background NPC is not mentioned anywhere in this event.
|
||||
world
|
||||
.resource_mut::<KnowledgeEventQueue>()
|
||||
.push(KnowledgeEvent {
|
||||
observer: active_npc,
|
||||
tick: 1,
|
||||
event_type: KnowledgeEventType::DirectObservation {
|
||||
target: observed_entity,
|
||||
position: TilePosition::new(5, 5, 0),
|
||||
},
|
||||
});
|
||||
|
||||
// Run the knowledge event processing system.
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(process_knowledge_events);
|
||||
schedule.run(&mut world);
|
||||
|
||||
// --- Negative assertion: Background NPC's KG must be completely unchanged ---
|
||||
let bg_kg = world
|
||||
.get::<KnowledgeGraph>(background_npc)
|
||||
.expect("IB-3: BackgroundSim NPC must still have KnowledgeGraph component");
|
||||
|
||||
assert!(
|
||||
bg_kg.is_empty(),
|
||||
"IB-3: Background-tier NPC KG must not be modified by Active-tier events. \
|
||||
process_knowledge_events must only update the event.observer entity (D-026 tier boundary, \
|
||||
D-010 principle 2). Found {} entity entries and {} fact entries.",
|
||||
bg_kg.entity_count(),
|
||||
bg_kg.fact_count()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Layer 3 integration test entry point (D-030, ticket #200).
|
||||
//!
|
||||
//! ## Three-layer test architecture (D-030 sub-decision 3)
|
||||
//!
|
||||
//! ```text
|
||||
//! Layer 1 — Fixture-based serialization (FAST, run on every edit)
|
||||
//! Scope: Pure unit tests. No ECS world. No subprocess.
|
||||
//! Tools: Rust #[test] + data structures directly.
|
||||
//! Speed: <1ms each.
|
||||
//! Files: tests/serialization.rs, tests/information_boundaries.rs (Layer 1 tests),
|
||||
//! #[cfg(test)] mod tests within src/ modules
|
||||
//!
|
||||
//! Layer 2 — Mock subprocess / minimal ECS world (MEDIUM, run on every PR)
|
||||
//! Scope: Minimal bevy App or World. Real systems, no real subprocess.
|
||||
//! IPC roundtrip over Unix socket without spawning the binary.
|
||||
//! Tools: bevy_ecs World + Schedule, or LocalBridge with in-process simulation.
|
||||
//! Speed: 1ms–100ms each.
|
||||
//! Files: tests/bridge_ipc.rs, tests/bridge_tcp.rs,
|
||||
//! tests/information_boundaries.rs (Layer 2 tests),
|
||||
//! tests/determinism.rs, tests/movement.rs, tests/smoke.rs
|
||||
//!
|
||||
//! Layer 3 — Real subprocess integration (SLOW, run daily / pre-merge)
|
||||
//! Scope: Full binary spawned as a child process. No mocks. Real IPC.
|
||||
//! Exercises the complete path: spawn → handshake → tick → snapshot.
|
||||
//! Tools: std::process::Command, TcpStream.
|
||||
//! Speed: 1s–15s each (process startup dominates).
|
||||
//! Files: tests/layer3.rs, tests/integration/ (this module)
|
||||
//! ```
|
||||
//!
|
||||
//! ## Layer 3 test guidelines
|
||||
//!
|
||||
//! - Always set a deadline for server startup (`LISTEN_TIMEOUT`).
|
||||
//! - Always kill the child process in teardown (even on test failure — use a
|
||||
//! RAII guard or drop the handle at end of test).
|
||||
//! - Use `--port 0` to get a kernel-assigned port; parse `LISTENING:{port}` from
|
||||
//! stdout to obtain the actual port.
|
||||
//! - Serialize `PlayerInput` via `rmp_serde`, frame with `bridge::framing::write_framed`.
|
||||
//! - Deserialize `ObserverSnapshot` via `rmp_serde` after `bridge::framing::read_framed`.
|
||||
//!
|
||||
//! Spec reference: D-030 (testability architecture), D-020 (subprocess IPC protocol)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stub: Layer 3 startup smoke test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Placeholder for future Layer 3 tests that require full subprocess setup.
|
||||
///
|
||||
/// Non-blocking tests that exercise the simulation binary end-to-end live in
|
||||
/// `tests/layer3.rs`. This module is the organisational entry point for tests
|
||||
/// that exercise multi-message Layer 3 scenarios (multi-tick sequences,
|
||||
/// save/load roundtrip over IPC, protocol version negotiation).
|
||||
///
|
||||
/// See `tests/layer3.rs::server_subprocess_sends_snapshot_on_connect` for the
|
||||
/// canonical Layer 3 pattern.
|
||||
#[test]
|
||||
fn layer3_module_entry_point_placeholder() {
|
||||
// This test exists to verify the integration module compiles and is
|
||||
// discovered by cargo test. Real Layer 3 scenario tests replace this.
|
||||
// D-030 Layer 3 stubs are acceptable until the IPC handshake (#555) lands.
|
||||
}
|
||||
+16
-4
@@ -72,7 +72,19 @@ fn server_subprocess_sends_snapshot_on_connect() {
|
||||
let mut reader = BufReader::new(stream.try_clone().expect("clone stream for reader"));
|
||||
let mut writer = BufWriter::new(stream);
|
||||
|
||||
// 4. Send one PlayerInput (idle tick 0)
|
||||
// 4. Read the protocol handshake (first framed message, #555)
|
||||
let handshake_frame = read_framed(&mut reader)
|
||||
.expect("read handshake frame")
|
||||
.expect("server closed connection before sending handshake");
|
||||
let handshake: HandshakeMessage =
|
||||
rmp_serde::from_slice(&handshake_frame).expect("deserialize HandshakeMessage");
|
||||
assert_eq!(
|
||||
handshake.protocol_version, PROTOCOL_VERSION,
|
||||
"handshake protocol_version mismatch: got {}, expected {}",
|
||||
handshake.protocol_version, PROTOCOL_VERSION
|
||||
);
|
||||
|
||||
// 5. Send one PlayerInput (idle tick 0)
|
||||
let inputs = vec![PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::MoveNorth,
|
||||
@@ -80,14 +92,14 @@ fn server_subprocess_sends_snapshot_on_connect() {
|
||||
let payload = rmp_serde::to_vec_named(&inputs).expect("serialize PlayerInput");
|
||||
write_framed(&mut writer, &payload).expect("send PlayerInput to server");
|
||||
|
||||
// 5. Read one ObserverSnapshot
|
||||
// 6. Read one ObserverSnapshot
|
||||
let response = read_framed(&mut reader)
|
||||
.expect("read snapshot frame")
|
||||
.expect("server closed connection before sending snapshot");
|
||||
let snapshot: ObserverSnapshot =
|
||||
rmp_serde::from_slice(&response).expect("deserialize ObserverSnapshot");
|
||||
|
||||
// 6. Assert protocol correctness (D-020)
|
||||
// 7. Assert protocol correctness (D-020)
|
||||
assert_eq!(
|
||||
snapshot.version, PROTOCOL_VERSION,
|
||||
"protocol version mismatch: got {}, expected {}",
|
||||
@@ -105,7 +117,7 @@ fn server_subprocess_sends_snapshot_on_connect() {
|
||||
.any(|e| matches!(e.kind, EntityKind::Player));
|
||||
assert!(has_player, "snapshot must contain a Player entity");
|
||||
|
||||
// 7. Clean up: drop connection so the server exits its game loop
|
||||
// 8. Clean up: drop connection so the server exits its game loop
|
||||
drop(reader);
|
||||
drop(writer);
|
||||
|
||||
|
||||
@@ -30,12 +30,12 @@ fn test_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
examine_result: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
examine_result: None,
|
||||
player_knowledge: None,
|
||||
save_result: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,12 @@ fn all_player_action_variants_roundtrip() {
|
||||
target_entity_id: 42,
|
||||
response_id: "kael-davan_d_001".to_string(),
|
||||
},
|
||||
PlayerAction::SaveGame {
|
||||
path: "/tmp/test.msgpack".to_string(),
|
||||
},
|
||||
PlayerAction::LoadGame {
|
||||
path: "/tmp/test.msgpack".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
for action in actions {
|
||||
@@ -280,12 +286,12 @@ fn snapshot_v2_fields_roundtrip() {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
examine_result: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
examine_result: None,
|
||||
player_knowledge: None,
|
||||
save_result: None,
|
||||
};
|
||||
|
||||
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
|
||||
@@ -384,12 +390,12 @@ fn all_facing_direction_variants_roundtrip() {
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
examine_result: None,
|
||||
character_pressure: None,
|
||||
rng_seed: None,
|
||||
poi_list: vec![],
|
||||
examine_result: None,
|
||||
player_knowledge: None,
|
||||
save_result: None,
|
||||
};
|
||||
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
|
||||
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
@@ -1433,8 +1439,8 @@ fn serde_default_fields_fill_in_when_missing_from_wire() {
|
||||
let decoded: ObserverSnapshot =
|
||||
serde_json::from_value(minimal_json).expect("minimal JSON must deserialize");
|
||||
|
||||
// Version matches what was in the wire (13, simulating older server)
|
||||
assert_eq!(decoded.version, 13);
|
||||
// Version matches what was in the wire
|
||||
assert_eq!(decoded.version, 14);
|
||||
assert_eq!(decoded.tick, 42);
|
||||
assert_eq!(decoded.entities.len(), 1);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user