test(simulation): sprint 10 — replay loading, content scaling, serialization v9, observer tests

#483: Replay loading in test-client — JSONL file loading, tick-scheduled
PlayerInput sending, 13 unit tests, 3 sample replay files.
#500: Content scaling test — baseline + extra NPC comparative, tick budget
assertion (D-026), determinism check across content packs.
#514: Serialization tests for protocol v9 — blocked_entities roundtrip,
backward compat (v5→v9, v8→v9), regenerated msgpack fixtures.
Observer perception tests for confrontation + walk-away mechanics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 12:58:27 +01:00
co-authored by Claude Opus 4.6
parent 273d29f26f
commit 6c62e2228f
23 changed files with 782 additions and 3 deletions
+92 -1
View File
@@ -24,6 +24,7 @@ fn test_snapshot(tick: u64, entities: Vec<VisibleEntity>) -> ObserverSnapshot {
current_monologue: None,
pending_recognitions: vec![],
dialogue_response: None,
blocked_entities: vec![],
}
}
@@ -250,6 +251,7 @@ fn snapshot_v2_fields_roundtrip() {
current_monologue: None,
pending_recognitions: vec![],
dialogue_response: None,
blocked_entities: vec![],
};
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
@@ -304,7 +306,7 @@ fn protocol_version_constant_matches_snapshot() {
let snapshot = test_snapshot(0, vec![]);
assert_eq!(snapshot.version, PROTOCOL_VERSION);
assert_eq!(
PROTOCOL_VERSION, 8,
PROTOCOL_VERSION, 9,
"bump this assertion when protocol version changes"
);
}
@@ -342,6 +344,7 @@ fn all_facing_direction_variants_roundtrip() {
current_monologue: None,
pending_recognitions: vec![],
dialogue_response: None,
blocked_entities: vec![],
};
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
@@ -469,6 +472,10 @@ fn v5_payload_deserializes_into_v6_struct() {
decoded.pending_recognitions.is_empty(),
"missing pending_recognitions should default to empty"
);
assert!(
decoded.blocked_entities.is_empty(),
"missing blocked_entities should default to empty"
);
}
/// Full 9-slot inventory roundtrip (D-065: 3x3 grid = 9 slots universal)
@@ -1095,6 +1102,90 @@ fn gdscript_generated_fixtures_deserialize() {
eprintln!("Verified {} GDScript-generated fixtures", count);
}
/// blocked_entities Vec<u64> round-trips through MessagePack (#514).
/// Guards the debug field survives serialization.
#[test]
fn blocked_entities_roundtrip() {
let mut snapshot = test_snapshot(0, vec![]);
snapshot.blocked_entities = vec![42, 99, 1024];
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
assert_eq!(
decoded.blocked_entities,
vec![42, 99, 1024],
"blocked_entities should survive roundtrip"
);
}
/// Empty blocked_entities round-trips correctly (#514).
#[test]
fn blocked_entities_empty_roundtrip() {
let snapshot = test_snapshot(0, vec![]);
assert!(snapshot.blocked_entities.is_empty());
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
assert!(
decoded.blocked_entities.is_empty(),
"empty blocked_entities should survive roundtrip"
);
}
/// v8 payloads (without blocked_entities) must deserialize into the v9 struct
/// via #[serde(default)]. Guards backwards compat during migration (#514).
#[test]
fn v8_payload_deserializes_into_v9_struct() {
#[derive(serde::Serialize)]
struct ObserverSnapshotV8 {
version: u8,
tick: u64,
game_time: GameTime,
player_facing: FacingDirection,
player_stance: MovementStance,
player_inventory: Vec<InventoryItem>,
entities: Vec<VisibleEntity>,
visible_tiles: Vec<VisibleTile>,
nearby_interactions: Vec<NearbyInteraction>,
current_monologue: Option<MonologueEvent>,
pending_recognitions: Vec<PendingRecognitionWire>,
dialogue_response: Option<DialogueResponseEvent>,
}
let v8 = ObserverSnapshotV8 {
version: 8,
tick: 100,
game_time: GameTime {
day: 0,
time_of_day: 0,
day_phase: DayPhase::Morning,
tick_rate: TickRate::Full,
},
player_facing: FacingDirection::North,
player_stance: MovementStance::Walk,
player_inventory: vec![],
entities: vec![],
visible_tiles: vec![],
nearby_interactions: vec![],
current_monologue: None,
pending_recognitions: vec![],
dialogue_response: None,
};
let bytes = rmp_serde::to_vec_named(&v8).expect("serialize v8");
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes)
.expect("v8 payload should deserialize into v9 struct via serde(default)");
assert_eq!(decoded.version, 8, "version field preserved from v8");
assert_eq!(decoded.tick, 100);
assert!(
decoded.blocked_entities.is_empty(),
"missing blocked_entities should default to empty"
);
}
/// NearbyInteraction.object_type round-trips through MessagePack (#422).
/// Verifies object_type=Some(Container) survives the wire.
#[test]