test(simulation): sprint 8 test suite — pause guards, registry, boundary, determinism

Add 50+ tests: pause guard suite (movement, unpause, roundtrip, stance,
interact, batch, tick_rate), EntityRegistry lifecycle (stale mapping,
re-register, unknown unregister), boundary value encode/roundtrip (41
values), encoding asymmetry (GDScript signed→Rust unsigned), malformed
batch rejection, determinism gauntlet (20-tick replay), per-fix
determinism unit tests, and recognition monologue integration tests.
Fix pause guard to block all actions except Pause/Unpause while paused.
Fixes #461-463, #466-469, #471-473, #479.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 17:41:33 +01:00
co-authored by Claude Opus 4.6
parent 35f55cfa46
commit b1fdeabb7c
7 changed files with 911 additions and 2 deletions
+111
View File
@@ -1846,6 +1846,7 @@ fn pending_recognitions_appear_in_snapshot() {
position: TilePosition::new(16, 14, 0),
delay_until_tick: 110, // will complete at tick 110
trigger: RecognitionTrigger::Normal,
monologue_fired: false,
});
let player = world
@@ -1892,6 +1893,116 @@ fn pending_recognitions_appear_in_snapshot() {
assert_eq!(pending.z, expected_z);
}
// -----------------------------------------------------------------------
// Determinism regression tests (#456/#457 — Fix A + Fix B)
// -----------------------------------------------------------------------
#[test]
fn equidistant_npcs_produce_stable_snapshot_ordering() {
// Fix A (#456): visible_ids uses BTreeSet for deterministic iteration.
// Fix B (#457): entities sorted by entity_id in snapshot.
// Regression guard: equidistant NPCs must always appear in ascending
// entity_id order regardless of ECS internal iteration order.
let mut world = setup_world(32, 32);
let mut registry = EntityRegistry::new(0);
// Three NPCs equidistant from observer at (16,16) — all 2 tiles away.
// Spawn order: npc_a, npc_b, npc_c → ascending stable_ids.
let npc_a = world
.spawn((crate::npc::Npc, TilePosition::new(16, 14, 0)))
.id();
let npc_a_sid = registry.register(npc_a);
let npc_b = world
.spawn((crate::npc::Npc, TilePosition::new(14, 16, 0)))
.id();
let npc_b_sid = registry.register(npc_b);
let npc_c = world
.spawn((crate::npc::Npc, TilePosition::new(18, 16, 0)))
.id();
let npc_c_sid = registry.register(npc_c);
let player = world
.spawn((
PlayerCharacter,
TilePosition::new(16, 16, 0),
Facing(FacingDirection::North),
KnowledgeGraph::new(),
NearbyInteractionBuffer::default(),
MonologueBuffer::default(),
))
.id();
registry.register(player);
world.insert_resource(registry);
run_observer_pipeline(&mut world);
let buffer = world.resource::<SnapshotBuffer>();
let snapshot = buffer.snapshot.as_ref().unwrap();
let npc_ids: Vec<u64> = snapshot
.entities
.iter()
.filter(|e| matches!(e.kind, EntityKind::Npc))
.map(|e| e.entity_id)
.collect();
assert_eq!(npc_ids.len(), 3, "all three equidistant NPCs should be visible");
// Entity IDs must be in strictly ascending order (Fix B sort guarantee)
for i in 1..npc_ids.len() {
assert!(
npc_ids[i - 1] < npc_ids[i],
"snapshot entities not sorted by entity_id: {:?}",
npc_ids
);
}
// Verify the ordering matches the expected stable_id assignment order
assert_eq!(npc_ids[0], npc_a_sid.0);
assert_eq!(npc_ids[1], npc_b_sid.0);
assert_eq!(npc_ids[2], npc_c_sid.0);
}
#[test]
fn visible_tiles_sorted_by_coordinates() {
// Fix A (#456): visible_tiles sorted by (x, y) for deterministic snapshots.
let mut world = setup_world(32, 32);
world.spawn((
PlayerCharacter,
TilePosition::new(16, 16, 0),
Facing(FacingDirection::North),
KnowledgeGraph::new(),
NearbyInteractionBuffer::default(),
MonologueBuffer::default(),
));
run_observer_pipeline(&mut world);
let buffer = world.resource::<SnapshotBuffer>();
let snapshot = buffer.snapshot.as_ref().unwrap();
assert!(
!snapshot.visible_tiles.is_empty(),
"should have visible tiles"
);
// All tiles must be sorted by (x, y)
for i in 1..snapshot.visible_tiles.len() {
let prev = &snapshot.visible_tiles[i - 1];
let curr = &snapshot.visible_tiles[i];
assert!(
(prev.x, prev.y) <= (curr.x, curr.y),
"visible_tiles not sorted: ({},{}) > ({},{})",
prev.x,
prev.y,
curr.x,
curr.y,
);
}
}
#[test]
fn no_cognitive_delay_component_means_empty_pending_recognitions() {
// H11 complement: player WITHOUT CognitiveDelay should produce