Blockers (4): - Wire cargo deny check into pre-pr-server (was dead config) (#1) - ConfirmBookmark idempotency guard: SimError ProtocolError on retry (#2) - D-080 amendment: transfer_npc_knowledge retained-but-dormant honest doc (#3) - SelectedBookmark v0.2 transient scope; save/load deferred to #863 (#4) Issues (8): - ConfirmBookmark validation tests: unknown id, invalid location, valid path, double-confirm guard (#5) - snapshot_with_bookmark_catalog fixture for client #618 decode tests (#6) - generate_brands: replace 5 raw .unwrap() with eprintln+exit pattern (#7) - npc_knowledge_transfer.rs: stale run_npc_conversations refs cleaned (#8) - monologue.rs: residual D-078 "overheard conversations" doc removed (#9) - 5 test files: orphan blank lines from removed conversation_* fields (#10) - BookmarkCatalog: add PartialEq, Eq derives (matches sibling) (#11) Nits (2): - culture_tag doc: describe BookmarkRegistry::build_catalog behavior, remove "until #679 lands" placeholder (#13) - generate_brands seed=1 canonical comment (#14) Follow-ups filed: - #862 — BookmarkPlugin::new(registry) injection (#12 deferred) - #863 — Wire SelectedBookmark into SaveState (Sprint 37) Pre-pr-server: fmt clean, clippy clean, deny clean, build clean. nextest save_io failures pre-existing parallelism issue (sequential cargo test --lib passes 20/20). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
168 lines
5.4 KiB
Rust
168 lines
5.4 KiB
Rust
//! Integration tests for LocalBridge over Unix sockets (D-030 Layer 2: IPC roundtrip).
|
|
|
|
use settled_reach_server::bridge::framing::{read_framed, write_framed};
|
|
use settled_reach_server::bridge::local::LocalBridge;
|
|
use settled_reach_server::bridge::types::*;
|
|
use settled_reach_server::bridge::SimBridge;
|
|
use settled_reach_server::simulation::time::{DayPhase, TickRate};
|
|
use std::os::unix::net::UnixStream;
|
|
use std::path::PathBuf;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
/// Generate unique socket path for test isolation
|
|
fn test_socket_path(test_name: &str) -> PathBuf {
|
|
let pid = std::process::id();
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_millis();
|
|
PathBuf::from(format!(
|
|
"/tmp/sr-test-{}-{}-{}.sock",
|
|
test_name, pid, timestamp
|
|
))
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_roundtrip_over_unix_socket() {
|
|
let socket_path = test_socket_path("snapshot");
|
|
|
|
// Server thread: accept connection and send snapshot
|
|
let server_path = socket_path.clone();
|
|
let server_handle = thread::spawn(move || {
|
|
let bridge = LocalBridge::accept(&server_path).expect("failed to accept");
|
|
|
|
let snapshot = ObserverSnapshot {
|
|
version: PROTOCOL_VERSION,
|
|
tick: 42,
|
|
game_time: GameTime {
|
|
day: 0,
|
|
time_of_day: 0,
|
|
day_phase: DayPhase::Morning,
|
|
tick_rate: TickRate::Full,
|
|
},
|
|
player_facing: FacingDirection::North,
|
|
player_stance: MovementStance::default(),
|
|
player_inventory: vec![],
|
|
entities: vec![VisibleEntity {
|
|
entity_id: 100,
|
|
x: 10.5,
|
|
y: 20.3,
|
|
z: 0,
|
|
kind: EntityKind::Npc,
|
|
visibility: VisibilitySector::Forward,
|
|
relationship: RelationshipState::Unknown,
|
|
observation: EntityVisibility::Visible,
|
|
tell_state: None,
|
|
}],
|
|
visible_tiles: vec![],
|
|
nearby_interactions: vec![],
|
|
current_monologue: None,
|
|
pending_recognitions: vec![],
|
|
dialogue_response: None,
|
|
blocked_entities: vec![],
|
|
scan_events: vec![],
|
|
sound_events: vec![],
|
|
follow_state: None,
|
|
character_pressure: None,
|
|
rng_seed: None,
|
|
poi_list: vec![],
|
|
examine_result: None,
|
|
player_knowledge: None,
|
|
save_result: None,
|
|
triangle_crisis_events: vec![],
|
|
state_hash: None,
|
|
debug_response: None,
|
|
sim_errors: vec![],
|
|
current_ticker: None,
|
|
settings_response: None,
|
|
economy_snapshot: None,
|
|
bookmark_catalog: None,
|
|
};
|
|
|
|
bridge
|
|
.send_snapshot(&snapshot)
|
|
.expect("failed to send snapshot");
|
|
});
|
|
|
|
// Give server time to bind
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
// Client: connect and receive snapshot
|
|
let stream = UnixStream::connect(&socket_path).expect("failed to connect");
|
|
let mut reader = std::io::BufReader::new(stream);
|
|
|
|
let payload = read_framed(&mut reader)
|
|
.expect("failed to read frame")
|
|
.expect("unexpected EOF");
|
|
|
|
let snapshot: ObserverSnapshot =
|
|
rmp_serde::from_slice(&payload).expect("failed to deserialize");
|
|
|
|
assert_eq!(snapshot.tick, 42);
|
|
assert_eq!(snapshot.entities.len(), 1);
|
|
assert_eq!(snapshot.entities[0].entity_id, 100);
|
|
assert_eq!(snapshot.entities[0].x, 10.5);
|
|
assert_eq!(snapshot.entities[0].y, 20.3);
|
|
|
|
server_handle.join().expect("server thread panicked");
|
|
}
|
|
|
|
#[test]
|
|
fn input_roundtrip_over_unix_socket() {
|
|
let socket_path = test_socket_path("input");
|
|
|
|
// Server thread: accept connection and receive inputs
|
|
let server_path = socket_path.clone();
|
|
let server_handle = thread::spawn(move || {
|
|
let bridge = LocalBridge::accept(&server_path).expect("failed to accept");
|
|
|
|
let inputs = bridge.receive_inputs().expect("failed to receive inputs");
|
|
|
|
assert_eq!(inputs.len(), 2);
|
|
assert_eq!(inputs[0].tick, 10);
|
|
assert_eq!(inputs[1].tick, 11);
|
|
|
|
inputs
|
|
});
|
|
|
|
// Give server time to bind
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
// Client: connect and send inputs
|
|
let stream = UnixStream::connect(&socket_path).expect("failed to connect");
|
|
let mut writer = std::io::BufWriter::new(stream);
|
|
|
|
let inputs = vec![
|
|
PlayerInput {
|
|
tick: 10,
|
|
action: PlayerAction::MoveNorth,
|
|
},
|
|
PlayerInput {
|
|
tick: 11,
|
|
action: PlayerAction::Interact {
|
|
target_entity_id: None,
|
|
verb: None,
|
|
},
|
|
},
|
|
];
|
|
|
|
let payload = rmp_serde::to_vec_named(&inputs).expect("failed to serialize");
|
|
write_framed(&mut writer, &payload).expect("failed to write frame");
|
|
|
|
// Drop writer to close connection and signal EOF to server
|
|
drop(writer);
|
|
|
|
let received_inputs = server_handle.join().expect("server thread panicked");
|
|
|
|
// Verify actions survived the round-trip
|
|
match &received_inputs[0].action {
|
|
PlayerAction::MoveNorth => {}
|
|
_ => panic!("expected MoveNorth action"),
|
|
}
|
|
match &received_inputs[1].action {
|
|
PlayerAction::Interact { .. } => {}
|
|
_ => panic!("expected Interact action"),
|
|
}
|
|
}
|