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
+92
View File
@@ -158,4 +158,96 @@ mod tests {
assert_eq!(registry.to_entity(&StableId(999)), None);
assert_eq!(registry.to_stable(e1), None);
}
// === EntityRegistry lifecycle edge cases (#469) ===
#[test]
fn stale_mapping_after_despawn() {
// #469: Registry returns stale Entity after world despawn.
// This documents the expected behavior — caller must unregister after despawn.
let mut world = World::new();
let e1 = world.spawn_empty().id();
let mut registry = EntityRegistry::new(0);
let id = registry.register(e1);
// Despawn from world — registry doesn't know
world.despawn(e1);
// Registry still maps the StableId to the (now stale) Entity
let stale_entity = registry.to_entity(&id);
assert!(
stale_entity.is_some(),
"Registry still holds mapping after world despawn"
);
// But the world no longer recognizes the entity
assert!(
world.get_entity(stale_entity.unwrap()).is_err(),
"World rejects stale entity — caller must call unregister()"
);
// After proper cleanup, mapping is gone
registry.unregister(e1);
assert_eq!(
registry.to_entity(&id),
None,
"Mapping gone after unregister"
);
}
#[test]
fn register_after_unregister_assigns_new_id() {
// #469: Re-registering the same entity after unregister gets a new StableId.
// StableId counter is monotonic — never recycles.
let mut world = World::new();
let e1 = world.spawn_empty().id();
let mut registry = EntityRegistry::new(0);
let id_first = registry.register(e1);
assert_eq!(id_first, StableId(0));
registry.unregister(e1);
let id_second = registry.register(e1);
assert_ne!(
id_first, id_second,
"Re-registration must assign a new StableId"
);
assert_eq!(
id_second,
StableId(1),
"Counter advances monotonically"
);
assert_eq!(registry.len(), 1);
// New mapping is bidirectionally correct
assert_eq!(registry.to_entity(&id_second), Some(e1));
assert_eq!(registry.to_stable(e1), Some(id_second));
// Old StableId no longer resolves
assert_eq!(
registry.to_entity(&id_first),
None,
"Old StableId must not resolve"
);
}
#[test]
fn unregister_unknown_entity_is_noop() {
// #469: Unregistering an entity that was never registered must not panic.
let mut world = World::new();
let e1 = world.spawn_empty().id();
let e2 = world.spawn_empty().id();
let mut registry = EntityRegistry::new(0);
registry.register(e1);
// Unregister e2 which was never registered — should be a no-op
registry.unregister(e2);
// e1's registration is unaffected
assert_eq!(registry.len(), 1);
assert_eq!(registry.to_stable(e1), Some(StableId(0)));
}
}