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:
2026-02-25 12:13:03 +01:00
co-authored by Claude Opus 4.6
parent 0dd33690f7
commit 6ed8d11502
22 changed files with 2561 additions and 35 deletions
+11 -5
View File
@@ -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);