feat(ci): IPC serialization fixture files and cross-language tests (#271)
Five new .msgpack fixtures: snapshot_minimal, snapshot_full, player_input_move, player_input_interact, malformed. Rust generator in gen_fixtures.rs, 7 Rust validation tests in serialization.rs, GDScript cross-language test in test_ipc_fixtures.gd (22 assertions). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
��
|
||||
@@ -0,0 +1 @@
|
||||
うtickヲaction→Interactげtarget_entity_idc、verb、Talk
|
||||
@@ -0,0 +1 @@
|
||||
うtickヲactionゥMoveNorth
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,182 @@
|
||||
## D-030 Layer 1: Cross-language IPC fixture tests (#271)
|
||||
## Validates that Protocol.gd decodes the #271 named fixtures identically to Rust.
|
||||
## Fixtures generated by: cargo test --test gen_fixtures -- --ignored
|
||||
## Rust validation: server/tests/serialization.rs (fixture_* tests)
|
||||
class_name TestIpcFixtures
|
||||
extends GdUnitTestSuite
|
||||
|
||||
const FIXTURE_DIR = "res://tests/fixtures/msgpack/"
|
||||
|
||||
|
||||
func _load_fixture(name: String) -> PackedByteArray:
|
||||
var path = FIXTURE_DIR + name + ".msgpack"
|
||||
var file = FileAccess.open(path, FileAccess.READ)
|
||||
assert_that(file).is_not_null().override_failure_message(
|
||||
"Fixture not found: %s — run 'make fixtures' to regenerate" % path
|
||||
)
|
||||
return file.get_buffer(file.get_length())
|
||||
|
||||
|
||||
# -- snapshot_minimal ----------------------------------------------------------
|
||||
|
||||
func test_fixture_snapshot_minimal_version() -> void:
|
||||
var bytes = _load_fixture("snapshot_minimal")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot).is_not_null()
|
||||
assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION)
|
||||
|
||||
|
||||
func test_fixture_snapshot_minimal_tick() -> void:
|
||||
var bytes = _load_fixture("snapshot_minimal")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.tick).is_equal(0)
|
||||
|
||||
|
||||
func test_fixture_snapshot_minimal_entity_count() -> void:
|
||||
var bytes = _load_fixture("snapshot_minimal")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.entities.size()).is_equal(1)
|
||||
|
||||
|
||||
func test_fixture_snapshot_minimal_entity_kind() -> void:
|
||||
var bytes = _load_fixture("snapshot_minimal")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
var entity = snapshot.entities[0]
|
||||
assert_that(entity.entity_id).is_equal(1)
|
||||
# entity.kind is {"variant": "Player", "data": null} from _decode_enum_variant
|
||||
assert_that(entity.kind.variant).is_equal("Player")
|
||||
|
||||
|
||||
func test_fixture_snapshot_minimal_no_monologue() -> void:
|
||||
var bytes = _load_fixture("snapshot_minimal")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.current_monologue).is_null()
|
||||
|
||||
|
||||
func test_fixture_snapshot_minimal_no_dialogue() -> void:
|
||||
var bytes = _load_fixture("snapshot_minimal")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.dialogue_response).is_null()
|
||||
|
||||
|
||||
# -- snapshot_full -------------------------------------------------------------
|
||||
|
||||
func test_fixture_snapshot_full_tick() -> void:
|
||||
var bytes = _load_fixture("snapshot_full")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot).is_not_null()
|
||||
assert_that(snapshot.tick).is_equal(42)
|
||||
|
||||
|
||||
func test_fixture_snapshot_full_monologue_id() -> void:
|
||||
var bytes = _load_fixture("snapshot_full")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.current_monologue).is_not_null()
|
||||
assert_that(snapshot.current_monologue.id).is_equal("test_monologue_001")
|
||||
|
||||
|
||||
func test_fixture_snapshot_full_monologue_text() -> void:
|
||||
var bytes = _load_fixture("snapshot_full")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.current_monologue.text).is_equal("Something feels off about this place.")
|
||||
|
||||
|
||||
func test_fixture_snapshot_full_dialogue_speaker() -> void:
|
||||
var bytes = _load_fixture("snapshot_full")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.dialogue_response).is_not_null()
|
||||
# dialogue_response has: line_id, text, speaker_entity_id (per protocol.gd v8 decode)
|
||||
assert_that(snapshot.dialogue_response.speaker_entity_id).is_equal(99)
|
||||
|
||||
|
||||
func test_fixture_snapshot_full_inventory() -> void:
|
||||
var bytes = _load_fixture("snapshot_full")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.player_inventory.size()).is_equal(1)
|
||||
assert_that(snapshot.player_inventory[0].name).is_equal("Forged Customs Cert")
|
||||
|
||||
|
||||
func test_fixture_snapshot_full_poi_list() -> void:
|
||||
var bytes = _load_fixture("snapshot_full")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.poi_list.size()).is_equal(1)
|
||||
assert_that(snapshot.poi_list[0].poi_id).is_equal("docking_bay_7")
|
||||
|
||||
|
||||
func test_fixture_snapshot_full_player_knowledge_entity() -> void:
|
||||
var bytes = _load_fixture("snapshot_full")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.player_knowledge).is_not_null()
|
||||
assert_that(snapshot.player_knowledge.entities.size()).is_equal(1)
|
||||
assert_that(snapshot.player_knowledge.entities[0].name).is_equal("Kael")
|
||||
|
||||
|
||||
func test_fixture_snapshot_full_player_knowledge_fact() -> void:
|
||||
var bytes = _load_fixture("snapshot_full")
|
||||
var snapshot = Protocol.decode_snapshot(bytes)
|
||||
assert_that(snapshot.player_knowledge.facts.size()).is_equal(1)
|
||||
assert_that(snapshot.player_knowledge.facts[0].fact_id).is_equal("poi.docking_bay_7")
|
||||
|
||||
|
||||
# -- player_input_move ---------------------------------------------------------
|
||||
|
||||
func test_fixture_player_input_move_tick() -> void:
|
||||
var bytes = _load_fixture("player_input_move")
|
||||
var input = Protocol.decode_player_input(bytes)
|
||||
assert_that(input).is_not_null()
|
||||
assert_that(input.tick).is_equal(1)
|
||||
|
||||
|
||||
func test_fixture_player_input_move_action() -> void:
|
||||
var bytes = _load_fixture("player_input_move")
|
||||
var input = Protocol.decode_player_input(bytes)
|
||||
# action is {"variant": "MoveNorth", "data": null} from _decode_enum_variant
|
||||
assert_that(input.action.variant).is_equal("MoveNorth")
|
||||
|
||||
|
||||
# -- player_input_interact -----------------------------------------------------
|
||||
|
||||
func test_fixture_player_input_interact_tick() -> void:
|
||||
var bytes = _load_fixture("player_input_interact")
|
||||
var input = Protocol.decode_player_input(bytes)
|
||||
assert_that(input).is_not_null()
|
||||
assert_that(input.tick).is_equal(2)
|
||||
|
||||
|
||||
func test_fixture_player_input_interact_action() -> void:
|
||||
var bytes = _load_fixture("player_input_interact")
|
||||
var input = Protocol.decode_player_input(bytes)
|
||||
# Interact is a struct variant: {"variant": "Interact", "data": {"target_entity_id": 99, "verb": "Talk"}}
|
||||
assert_that(input.action.variant).is_equal("Interact")
|
||||
|
||||
|
||||
func test_fixture_player_input_interact_target() -> void:
|
||||
var bytes = _load_fixture("player_input_interact")
|
||||
var input = Protocol.decode_player_input(bytes)
|
||||
assert_that(input.action.data.target_entity_id).is_equal(99)
|
||||
|
||||
|
||||
func test_fixture_player_input_interact_verb() -> void:
|
||||
var bytes = _load_fixture("player_input_interact")
|
||||
var input = Protocol.decode_player_input(bytes)
|
||||
assert_that(input.action.data.verb).is_equal("Talk")
|
||||
|
||||
|
||||
# -- malformed -----------------------------------------------------------------
|
||||
|
||||
func test_fixture_malformed_snapshot_fails() -> void:
|
||||
var bytes = _load_fixture("malformed")
|
||||
# Intentionally truncated — decode_snapshot must return null (not crash)
|
||||
var result = Protocol.decode_snapshot(bytes)
|
||||
assert_that(result).is_null().override_failure_message(
|
||||
"malformed fixture should not decode as a valid ObserverSnapshot"
|
||||
)
|
||||
|
||||
|
||||
func test_fixture_malformed_input_fails() -> void:
|
||||
var bytes = _load_fixture("malformed")
|
||||
# Intentionally truncated — decode_player_input must return null (not crash)
|
||||
var result = Protocol.decode_player_input(bytes)
|
||||
assert_that(result).is_null().override_failure_message(
|
||||
"malformed fixture should not decode as a valid PlayerInput"
|
||||
)
|
||||
@@ -2,6 +2,7 @@
|
||||
//! Run with: cargo test --test gen_fixtures -- --ignored
|
||||
|
||||
use settled_reach_server::bridge::types::*;
|
||||
use settled_reach_server::simulation::poi::PoiCategory;
|
||||
use settled_reach_server::simulation::time::{DayPhase, TickRate};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
@@ -41,7 +42,6 @@ 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![],
|
||||
@@ -232,7 +232,6 @@ 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![],
|
||||
@@ -287,6 +286,150 @@ fn generate_msgpack_fixtures() {
|
||||
write_fixture(name, &rmp_serde::to_vec_named(&input).unwrap());
|
||||
}
|
||||
|
||||
// === #271 fixtures: named fixtures for cross-language Layer 1 testing ===
|
||||
|
||||
// snapshot_minimal: version=14, tick=0, one Player entity, all optionals absent
|
||||
let snapshot_minimal = fixture_snapshot(
|
||||
0,
|
||||
vec![VisibleEntity {
|
||||
entity_id: 1,
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 0,
|
||||
kind: EntityKind::Player,
|
||||
visibility: VisibilitySector::Forward,
|
||||
relationship: RelationshipState::Unknown,
|
||||
observation: EntityVisibility::Visible,
|
||||
tell_state: None,
|
||||
}],
|
||||
);
|
||||
write_fixture(
|
||||
"snapshot_minimal",
|
||||
&rmp_serde::to_vec_named(&snapshot_minimal).unwrap(),
|
||||
);
|
||||
|
||||
// snapshot_full: version=14, tick=42, monologue + dialogue + inventory + POIs + KG dump
|
||||
let snapshot_full = ObserverSnapshot {
|
||||
version: PROTOCOL_VERSION,
|
||||
tick: 42,
|
||||
game_time: GameTime {
|
||||
day: 3,
|
||||
time_of_day: 840,
|
||||
day_phase: DayPhase::Evening,
|
||||
tick_rate: TickRate::Full,
|
||||
},
|
||||
player_facing: FacingDirection::East,
|
||||
player_stance: MovementStance::Walk,
|
||||
player_inventory: vec![InventoryItem {
|
||||
item_id: 7,
|
||||
name: "Forged Customs Cert".to_string(),
|
||||
slot: 0,
|
||||
}],
|
||||
entities: vec![VisibleEntity {
|
||||
entity_id: 1,
|
||||
x: 10.0,
|
||||
y: 10.0,
|
||||
z: 0,
|
||||
kind: EntityKind::Player,
|
||||
visibility: VisibilitySector::Forward,
|
||||
relationship: RelationshipState::Unknown,
|
||||
observation: EntityVisibility::Visible,
|
||||
tell_state: None,
|
||||
}],
|
||||
visible_tiles: vec![],
|
||||
nearby_interactions: vec![],
|
||||
current_monologue: Some(MonologueEvent {
|
||||
id: "test_monologue_001".to_string(),
|
||||
text: "Something feels off about this place.".to_string(),
|
||||
duration_seconds: 4.0,
|
||||
}),
|
||||
pending_recognitions: vec![PendingRecognitionWire {
|
||||
entity_id: 7,
|
||||
x: 12.0,
|
||||
y: 8.0,
|
||||
z: 0,
|
||||
remaining_ticks: 3,
|
||||
total_delay_ticks: 10,
|
||||
}],
|
||||
dialogue_response: Some(DialogueResponseEvent {
|
||||
line_id: "kael_d_001".to_string(),
|
||||
text: "We need to talk about the shipment.".to_string(),
|
||||
speaker_entity_id: 99,
|
||||
speaker_color_index: 2,
|
||||
speaker_name: "Kael".to_string(),
|
||||
}),
|
||||
blocked_entities: vec![5, 6],
|
||||
scan_events: vec![],
|
||||
sound_events: vec![],
|
||||
conversation_events: vec![],
|
||||
conversation_ended: vec![],
|
||||
follow_state: None,
|
||||
character_pressure: None,
|
||||
rng_seed: Some(0xDEADBEEF),
|
||||
poi_list: vec![PoiWire {
|
||||
poi_id: "docking_bay_7".to_string(),
|
||||
name: "Docking Bay 7".to_string(),
|
||||
x: 50,
|
||||
y: 30,
|
||||
z: 0,
|
||||
category: PoiCategory::Location,
|
||||
}],
|
||||
examine_result: Some(ExamineResultWire {
|
||||
entity_id: 42,
|
||||
text: "A smuggler, probably. The way they hold themselves.".to_string(),
|
||||
confidence: KnowledgeConfidence::KnowsOf,
|
||||
}),
|
||||
player_knowledge: Some(PlayerKnowledgeWire {
|
||||
entities: vec![KnownEntityWire {
|
||||
entity_id: 99,
|
||||
name: "Kael".to_string(),
|
||||
confidence: KnowledgeConfidence::KnowsDetails,
|
||||
source: "DirectObservation".to_string(),
|
||||
state: KnowledgeState::Active,
|
||||
relationship: RelationshipState::Known,
|
||||
last_observed_tick: 40,
|
||||
}],
|
||||
facts: vec![KnownFactWire {
|
||||
fact_id: "poi.docking_bay_7".to_string(),
|
||||
confidence: KnowledgeConfidence::KnowsOf,
|
||||
source: "DirectObservation".to_string(),
|
||||
state: KnowledgeState::Active,
|
||||
acquired_tick: 10,
|
||||
}],
|
||||
}),
|
||||
};
|
||||
write_fixture(
|
||||
"snapshot_full",
|
||||
&rmp_serde::to_vec_named(&snapshot_full).unwrap(),
|
||||
);
|
||||
|
||||
// player_input_move: tick=1, MoveNorth
|
||||
let input_move = PlayerInput {
|
||||
tick: 1,
|
||||
action: PlayerAction::MoveNorth,
|
||||
};
|
||||
write_fixture(
|
||||
"player_input_move",
|
||||
&rmp_serde::to_vec_named(&input_move).unwrap(),
|
||||
);
|
||||
|
||||
// player_input_interact: tick=2, Interact { target: 99, verb: "Talk" }
|
||||
let input_interact = PlayerInput {
|
||||
tick: 2,
|
||||
action: PlayerAction::Interact {
|
||||
target_entity_id: Some(99),
|
||||
verb: Some("Talk".to_string()),
|
||||
},
|
||||
};
|
||||
write_fixture(
|
||||
"player_input_interact",
|
||||
&rmp_serde::to_vec_named(&input_interact).unwrap(),
|
||||
);
|
||||
|
||||
// malformed: intentionally truncated bytes — tests error handling in both Rust and GDScript
|
||||
// 0x82 = fixmap with 2 entries, 0xa4 = fixstr of length 4 — incomplete map, no key/value follows
|
||||
write_fixture("malformed", &[0x82u8, 0xa4u8]);
|
||||
|
||||
// === Boundary value fixtures (#472) ===
|
||||
// 14 raw integer values at encoding format boundaries (Appendix C).
|
||||
// These are Rust-encoded MessagePack that GDScript must decode correctly.
|
||||
|
||||
@@ -30,7 +30,6 @@ 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![],
|
||||
@@ -174,13 +173,15 @@ fn all_fixtures_deserialize() {
|
||||
} else if name.starts_with("input_batch") {
|
||||
rmp_serde::from_slice::<Vec<PlayerInput>>(&bytes)
|
||||
.unwrap_or_else(|e| panic!("deserialize batch input fixture {}: {}", name, e));
|
||||
} else if name.starts_with("input") {
|
||||
} else if name.starts_with("input") || name.starts_with("player_input") {
|
||||
rmp_serde::from_slice::<PlayerInput>(&bytes)
|
||||
.unwrap_or_else(|e| panic!("deserialize input fixture {}: {}", name, e));
|
||||
} else if name.starts_with("boundary_raw") {
|
||||
// Raw integer boundary fixtures (#472): single u64 values
|
||||
rmp_serde::from_slice::<u64>(&bytes)
|
||||
.unwrap_or_else(|e| panic!("deserialize boundary raw fixture {}: {}", name, e));
|
||||
} else if name == "malformed" {
|
||||
// Intentionally truncated — skip deserialization check, error handling tested elsewhere
|
||||
} else {
|
||||
panic!(
|
||||
"unknown fixture naming convention: {} — add a deserialization branch for this prefix",
|
||||
@@ -280,7 +281,6 @@ 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![],
|
||||
@@ -384,7 +384,6 @@ 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![],
|
||||
@@ -1394,7 +1393,7 @@ fn serde_default_fields_fill_in_when_missing_from_wire() {
|
||||
// `tell_state`, `follow_state`, `rng_seed`, `zone_id`, `object_type`, etc.
|
||||
// are all `#[serde(default)]` — they must default to None/empty when absent.
|
||||
let minimal_json = serde_json::json!({
|
||||
"version": 14,
|
||||
"version": 13,
|
||||
"tick": 42,
|
||||
"game_time": {
|
||||
"day": 0,
|
||||
@@ -1642,3 +1641,142 @@ fn nearby_interaction_object_type_roundtrip() {
|
||||
Some(ObjectType::Container)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// #271: Named fixture validation tests (D-030 Layer 1)
|
||||
//
|
||||
// These tests read the committed .msgpack files and assert specific field
|
||||
// values. They serve as the Rust side of cross-language verification — the
|
||||
// same fixtures are decoded by client/tests/test_ipc_fixtures.gd.
|
||||
// ============================================================
|
||||
|
||||
fn read_named_fixture(name: &str) -> Vec<u8> {
|
||||
let path = format!("../client/tests/fixtures/msgpack/{}.msgpack", name);
|
||||
fs::read(&path).unwrap_or_else(|e| panic!("failed to read fixture '{}': {}", name, e))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_snapshot_minimal_fields() {
|
||||
let bytes = read_named_fixture("snapshot_minimal");
|
||||
let snap: ObserverSnapshot =
|
||||
rmp_serde::from_slice(&bytes).expect("deserialize snapshot_minimal");
|
||||
|
||||
assert_eq!(snap.version, PROTOCOL_VERSION, "protocol version mismatch");
|
||||
assert_eq!(snap.tick, 0, "tick should be 0");
|
||||
assert_eq!(snap.entities.len(), 1, "should have exactly 1 entity");
|
||||
assert_eq!(snap.entities[0].entity_id, 1);
|
||||
assert!(
|
||||
matches!(snap.entities[0].kind, EntityKind::Player),
|
||||
"entity should be Player kind"
|
||||
);
|
||||
assert!(snap.current_monologue.is_none(), "no monologue in minimal");
|
||||
assert!(snap.dialogue_response.is_none(), "no dialogue in minimal");
|
||||
assert!(snap.player_inventory.is_empty(), "no inventory in minimal");
|
||||
assert!(snap.poi_list.is_empty(), "no POIs in minimal");
|
||||
assert!(snap.player_knowledge.is_none(), "no KG in minimal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_snapshot_full_fields() {
|
||||
let bytes = read_named_fixture("snapshot_full");
|
||||
let snap: ObserverSnapshot =
|
||||
rmp_serde::from_slice(&bytes).expect("deserialize snapshot_full");
|
||||
|
||||
assert_eq!(snap.version, PROTOCOL_VERSION, "protocol version mismatch");
|
||||
assert_eq!(snap.tick, 42, "tick should be 42");
|
||||
|
||||
// Monologue
|
||||
let monologue = snap.current_monologue.as_ref().expect("monologue absent");
|
||||
assert_eq!(monologue.id, "test_monologue_001");
|
||||
assert_eq!(
|
||||
monologue.text,
|
||||
"Something feels off about this place."
|
||||
);
|
||||
|
||||
// Dialogue
|
||||
let dialogue = snap.dialogue_response.as_ref().expect("dialogue absent");
|
||||
assert_eq!(dialogue.speaker_entity_id, 99);
|
||||
assert_eq!(dialogue.speaker_name, "Kael");
|
||||
|
||||
// Inventory
|
||||
assert_eq!(snap.player_inventory.len(), 1);
|
||||
assert_eq!(snap.player_inventory[0].name, "Forged Customs Cert");
|
||||
|
||||
// POIs
|
||||
assert_eq!(snap.poi_list.len(), 1);
|
||||
assert_eq!(snap.poi_list[0].poi_id, "docking_bay_7");
|
||||
|
||||
// Examine result
|
||||
let examine = snap.examine_result.as_ref().expect("examine_result absent");
|
||||
assert_eq!(examine.entity_id, 42);
|
||||
|
||||
// Player knowledge
|
||||
let kg = snap.player_knowledge.as_ref().expect("player_knowledge absent");
|
||||
assert_eq!(kg.entities.len(), 1);
|
||||
assert_eq!(kg.entities[0].name, "Kael");
|
||||
assert_eq!(kg.facts.len(), 1);
|
||||
assert_eq!(kg.facts[0].fact_id, "poi.docking_bay_7");
|
||||
|
||||
// RNG seed
|
||||
assert_eq!(snap.rng_seed, Some(0xDEADBEEF));
|
||||
|
||||
// Pending recognitions
|
||||
assert_eq!(snap.pending_recognitions.len(), 1);
|
||||
assert_eq!(snap.pending_recognitions[0].entity_id, 7);
|
||||
|
||||
// Blocked entities
|
||||
assert_eq!(snap.blocked_entities, vec![5u64, 6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_player_input_move_fields() {
|
||||
let bytes = read_named_fixture("player_input_move");
|
||||
let input: PlayerInput =
|
||||
rmp_serde::from_slice(&bytes).expect("deserialize player_input_move");
|
||||
|
||||
assert_eq!(input.tick, 1, "tick should be 1");
|
||||
assert!(
|
||||
matches!(input.action, PlayerAction::MoveNorth),
|
||||
"action should be MoveNorth"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_player_input_interact_fields() {
|
||||
let bytes = read_named_fixture("player_input_interact");
|
||||
let input: PlayerInput =
|
||||
rmp_serde::from_slice(&bytes).expect("deserialize player_input_interact");
|
||||
|
||||
assert_eq!(input.tick, 2, "tick should be 2");
|
||||
match &input.action {
|
||||
PlayerAction::Interact {
|
||||
target_entity_id,
|
||||
verb,
|
||||
} => {
|
||||
assert_eq!(*target_entity_id, Some(99u64), "target_entity_id should be Some(99)");
|
||||
assert_eq!(
|
||||
verb.as_deref(),
|
||||
Some("Talk"),
|
||||
"verb should be Some(\"Talk\")"
|
||||
);
|
||||
}
|
||||
other => panic!("expected Interact, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_malformed_fails_deserialization() {
|
||||
let bytes = read_named_fixture("malformed");
|
||||
// Intentionally truncated — must NOT deserialize as ObserverSnapshot
|
||||
let result = rmp_serde::from_slice::<ObserverSnapshot>(&bytes);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"malformed fixture should fail to deserialize as ObserverSnapshot"
|
||||
);
|
||||
// Also must NOT deserialize as PlayerInput
|
||||
let result2 = rmp_serde::from_slice::<PlayerInput>(&bytes);
|
||||
assert!(
|
||||
result2.is_err(),
|
||||
"malformed fixture should fail to deserialize as PlayerInput"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user