Hoshe (code quality): - Remove dead RoomMember component from reset.rs - Remove execute_reset (dual API trap); plan_reset is sole production path - .unwrap() → .expect() on reset_plate in setup_gauntlet boot path - Add 10s read timeout to TCP runtime test (prevents hangs) - Register player in EntityRegistry in runtime boot test - Document room_at z-range and corridor overlap assumptions - Derive entity count from EXPECTED_ENTITY_COUNT constant (was hardcoded 24) - Add reset plate (49-51) verification to stable_id_ranges_match_spec - Add debounce exact boundary test (tick 9 rejected, tick 10 accepted) Tyre (architecture): - Gate test_world rooms/constants/setup behind "gauntlet" feature (default-on); reset module stays always-compiled (production dependency via input system) - Document setup_gauntlet scheduler bypass for future tracking - Extract runtime TCP test to content_runtime.rs (separate failure modes) 507 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
151 lines
6.0 KiB
Rust
151 lines
6.0 KiB
Rust
//! Runtime validation: boot full plugin stack with real content, tick 10
|
|
//! times over TCP, assert valid ObserverSnapshot (#489).
|
|
//!
|
|
//! Separated from content_loading.rs (structural loading tests) per
|
|
//! architectural review — TCP runtime tests have different failure modes
|
|
//! and timeout characteristics.
|
|
|
|
use bevy_app::prelude::*;
|
|
use std::net::{TcpListener, TcpStream};
|
|
use std::path::PathBuf;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
use settled_reach_server::bridge::framing::{read_framed, write_framed};
|
|
use settled_reach_server::bridge::tcp::TcpBridge;
|
|
use settled_reach_server::bridge::types::*;
|
|
use settled_reach_server::bridge::{BridgePlugin, BridgeResource};
|
|
use settled_reach_server::content::{ContentConfig, ContentPlugin};
|
|
use settled_reach_server::knowledge::registry::EntityRegistry;
|
|
use settled_reach_server::knowledge::KnowledgeGraph;
|
|
use settled_reach_server::perception::cognitive_delay::CognitiveDelay;
|
|
use settled_reach_server::perception::vision_cone::Facing;
|
|
use settled_reach_server::simulation::interaction::NearbyInteractionBuffer;
|
|
use settled_reach_server::simulation::listening::ListeningFocus;
|
|
use settled_reach_server::simulation::monologue::{
|
|
MonologueBuffer, MonologueState, SprintAnomalyQueue,
|
|
};
|
|
use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
|
|
use settled_reach_server::simulation::stance::{MovementProfile, PlayerMoveCooldown};
|
|
use settled_reach_server::simulation::SimulationPlugin;
|
|
|
|
fn content_root() -> PathBuf {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
PathBuf::from(manifest_dir).join("../content")
|
|
}
|
|
|
|
/// Smoke test for production content. Boots the full plugin stack with real
|
|
/// content over TCP, ticks 10 times, and asserts a valid ObserverSnapshot.
|
|
/// Catches runtime panics from broken entity references, missing components,
|
|
/// or content schema issues that pass YAML validation but fail at tick time.
|
|
#[test]
|
|
fn content_runtime_boot_tick_10_snapshot() {
|
|
use std::io::{BufReader, BufWriter};
|
|
|
|
let root = content_root();
|
|
if !root.join("content.yaml").exists() {
|
|
eprintln!("Skipping: content directory not found at {:?}", root);
|
|
return;
|
|
}
|
|
|
|
let listener = TcpListener::bind("127.0.0.1:0").expect("bind listener");
|
|
let server_addr = listener.local_addr().expect("get local addr");
|
|
|
|
// Server thread: full plugin stack with real content
|
|
let server_handle = thread::spawn(move || {
|
|
let bridge = TcpBridge::accept_on(listener).expect("accept connection");
|
|
|
|
let mut app = App::new();
|
|
app.add_plugins(SimulationPlugin);
|
|
app.add_plugins(BridgePlugin);
|
|
app.add_plugins(settled_reach_server::knowledge::KnowledgePlugin);
|
|
app.add_plugins(settled_reach_server::npc::NpcPlugin);
|
|
app.insert_resource(ContentConfig {
|
|
content_root: root,
|
|
..Default::default()
|
|
});
|
|
app.add_plugins(ContentPlugin);
|
|
app.insert_resource(BridgeResource::new(bridge));
|
|
app.insert_resource(WalkabilityMap::new(32, 32, 1));
|
|
|
|
// Spawn player with all required observer pipeline components
|
|
let profile = MovementProfile::smuggler();
|
|
let mut registry = EntityRegistry::new(0);
|
|
let player = app
|
|
.world_mut()
|
|
.spawn((
|
|
PlayerCharacter,
|
|
TilePosition::new(16, 16, 0),
|
|
Facing::default(),
|
|
KnowledgeGraph::new(),
|
|
NearbyInteractionBuffer::default(),
|
|
MonologueState::default(),
|
|
MonologueBuffer::default(),
|
|
SprintAnomalyQueue::default(),
|
|
CognitiveDelay::default(),
|
|
ListeningFocus::new(TilePosition::new(16, 16, 0)),
|
|
profile,
|
|
profile.initial_stance(),
|
|
PlayerMoveCooldown::default(),
|
|
))
|
|
.id();
|
|
registry.register(player);
|
|
app.insert_resource(registry);
|
|
|
|
// Tick 10 times — any panic here means content has a runtime bug
|
|
for _ in 0..10 {
|
|
app.update();
|
|
}
|
|
});
|
|
|
|
// Client: connect with read timeout and receive 10 snapshots
|
|
let stream = TcpStream::connect(server_addr).expect("client connect");
|
|
stream
|
|
.set_read_timeout(Some(Duration::from_secs(10)))
|
|
.expect("set read timeout");
|
|
let mut reader = BufReader::new(stream.try_clone().expect("clone for reader"));
|
|
let mut writer = BufWriter::new(stream);
|
|
|
|
let mut last_snapshot = None;
|
|
for tick in 0..10 {
|
|
let payload = read_framed(&mut reader)
|
|
.unwrap_or_else(|e| panic!("read error at tick {}: {}", tick, e))
|
|
.unwrap_or_else(|| panic!("unexpected EOF at tick {}", tick));
|
|
|
|
let snapshot: ObserverSnapshot = rmp_serde::from_slice(&payload)
|
|
.unwrap_or_else(|e| panic!("deserialization error at tick {}: {}", tick, e));
|
|
|
|
last_snapshot = Some(snapshot);
|
|
|
|
// Send empty input for next tick
|
|
let empty: Vec<PlayerInput> = vec![];
|
|
let input_payload = rmp_serde::to_vec(&empty).expect("serialize empty input");
|
|
if write_framed(&mut writer, &input_payload).is_err() {
|
|
// Server may have shut down after tick 10 — that's fine
|
|
break;
|
|
}
|
|
}
|
|
|
|
drop(reader);
|
|
drop(writer);
|
|
|
|
// Server thread must not have panicked
|
|
server_handle
|
|
.join()
|
|
.expect("server thread panicked — content triggered a runtime error during tick processing");
|
|
|
|
// Validate final snapshot
|
|
let snapshot = last_snapshot.expect("should have received at least one snapshot");
|
|
assert_eq!(
|
|
snapshot.version, PROTOCOL_VERSION,
|
|
"snapshot protocol version mismatch"
|
|
);
|
|
// Content-spawned NPCs should be visible (they all spawn at 0,0,0 by default)
|
|
// The player is at 16,16 — content NPCs are far away but the player entity itself
|
|
// should always be in the snapshot
|
|
assert!(
|
|
!snapshot.entities.is_empty(),
|
|
"snapshot should contain at least the player entity"
|
|
);
|
|
}
|