From 0157d1fa385c172a787c7b969a1f129369003e78 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 12 Feb 2026 22:52:59 +0100 Subject: [PATCH] test(server): add Entity::to_bits roundtrip and TickRate switch tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Entity::to_bits() roundtrip test guards against bevy version changes silently breaking wire IDs (Hoshe #1) - PROTOCOL_VERSION constant used in test helpers instead of hardcoded 4 - TickRate switch mid-accumulation test verifies Half→Full→Paused→Half transitions preserve accumulator state correctly (Tyre N3) - POI mid-range test now asserts priority=1 (Hoshe #4) Co-Authored-By: Claude Opus 4.6 --- server/src/simulation/time.rs | 31 +++++++++++++++++++++++++++++++ server/tests/serialization.rs | 31 ++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/server/src/simulation/time.rs b/server/src/simulation/time.rs index 76d9dc554..b743cda98 100644 --- a/server/src/simulation/time.rs +++ b/server/src/simulation/time.rs @@ -199,6 +199,37 @@ mod tests { assert_eq!(time.day(), 3); } + #[test] + fn tick_rate_switch_mid_accumulation() { + // Half->Full with 0.5 remainder: Full should tick immediately (0.5 + 1.0 >= 1.0) + let mut world = bevy_ecs::world::World::new(); + world.insert_resource(SimulationTime { tick_rate: TickRate::Half, ..Default::default() }); + let mut schedule = bevy_ecs::schedule::Schedule::default(); + schedule.add_systems(advance_tick); + + // Frame 1: Half rate, accumulate 0.5, no tick + schedule.run(&mut world); + assert_eq!(world.resource::().tick, 0); + + // Switch to Full mid-accumulation (0.5 remainder) + world.resource_mut::().tick_rate = TickRate::Full; + + // Frame 2: Full rate adds 1.0 to 0.5 remainder → tick fires + schedule.run(&mut world); + assert_eq!(world.resource::().tick, 1); + + // Switch to Paused: no advance regardless of accumulator + world.resource_mut::().tick_rate = TickRate::Paused; + schedule.run(&mut world); + assert_eq!(world.resource::().tick, 1); + + // Switch back to Half: accumulator still has 0.5 from overshoot + world.resource_mut::().tick_rate = TickRate::Half; + schedule.run(&mut world); + // 0.5 (leftover) + 0.5 (Half) = 1.0 → tick fires + assert_eq!(world.resource::().tick, 2); + } + #[test] fn half_rate_no_drift_over_10000_frames() { let mut world = bevy_ecs::world::World::new(); diff --git a/server/tests/serialization.rs b/server/tests/serialization.rs index 80fbfb819..4b5f2ca91 100644 --- a/server/tests/serialization.rs +++ b/server/tests/serialization.rs @@ -7,7 +7,7 @@ use std::fs; /// Helper to create a minimal v2 snapshot for tests fn test_snapshot(tick: u64, entities: Vec) -> ObserverSnapshot { ObserverSnapshot { - version: 4, + version: PROTOCOL_VERSION, tick, game_time: GameTime { day: 0, @@ -231,6 +231,35 @@ fn snapshot_v2_fields_roundtrip() { assert_eq!(decoded.entities[0].visibility, VisibilitySector::Forward); } +/// Entity::to_bits() must roundtrip through from_bits() — guards against +/// bevy version changes silently breaking wire IDs (Hoshe #12). +#[test] +fn entity_to_bits_roundtrip() { + use bevy_ecs::entity::Entity; + // Create entities via a World so we get valid index+generation pairs + let mut world = bevy_ecs::world::World::new(); + let e1 = world.spawn_empty().id(); + let e2 = world.spawn_empty().id(); + let e3 = world.spawn_empty().id(); + // Despawn and respawn to get a higher generation + world.despawn(e2); + let e4 = world.spawn_empty().id(); + + for entity in [e1, e2, e3, e4] { + let bits = entity.to_bits(); + let restored = Entity::from_bits(bits); + assert_eq!(entity, restored, "Entity::to_bits() roundtrip failed for {:?}", entity); + } +} + +/// PROTOCOL_VERSION constant matches snapshot version field +#[test] +fn protocol_version_constant_matches_snapshot() { + let snapshot = test_snapshot(0, vec![]); + assert_eq!(snapshot.version, PROTOCOL_VERSION); + assert_eq!(PROTOCOL_VERSION, 4, "bump this assertion when protocol version changes"); +} + /// All FacingDirection variants round-trip #[test] fn all_facing_direction_variants_roundtrip() {