Sprint 37 dead-code sweep closing out two stale supersession chains: #877 (D-167, 2026-03-24): Removes HeritageRoot type alias and ZonePaletteModifier::Heritage variant from server/src/simulation/ generator.rs. The 7 abstract heritage roots were retired in favour of the corridor cultural system; these two stubs were the only remaining references. #878 (D-032 + cascade rule): Strips the entire CharacterArchetype (Smuggler/Detective) trace from the server. Per lead direction 2026-04-21 and the development cascade (CLAUDE.md), character/NPC/ verb-differentiation/monologue code is Phase 6 detail that should not exist in code yet. The running archetype trace was pre-cascade filler, not production — production is only the client's character- creation UI and insert screens (client follow-up in #882). Deleted: - CharacterArchetype enum + StartupMessage.character_archetype field - archetype_verb_label() + archetype branch of apply_phase2_verb_filter (D-057 character-verb differentiation — marked superseded) - MonologueState.character partitioning - Gauntlet archetype plumbing (setup_gauntlet no longer takes an archetype) - server/content/schemas/drama_module.schema.yaml (zero Rust consumers) - server/content/modules/tier1/smuggling_ring_v0_1.yaml - server/tests/archetype_monologue.rs (regression guard for the removed system) - server/tests/v01_integration_playthrough.rs (archetype-dependent) Decision updates: - decisions/content.md D-032 supersession rewritten to cite the cascade (v0.2 drop invalidated the prior D-117 framing). - decisions/content.md D-035 tag taxonomy: `character` enum footnote updated; field noted as unused, do not reintroduce without a confirmed Phase 6 design. - decisions/perception.md D-057: archetype-verb differentiation marked superseded. Also bundles the types.rs version-field removal from #874 since the file was already touched here. Full trace audit in docs/architecture/sprint-37-878-audit.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
141 lines
5.4 KiB
Rust
141 lines
5.4 KiB
Rust
//! Layer 3 integration test: real subprocess IPC (D-030)
|
|
//!
|
|
//! Spawns the server binary as a child process with --test-mode --port 0,
|
|
//! parses the LISTENING:{port} handshake from stdout, connects via TCP,
|
|
//! sends a PlayerInput, and reads back an ObserverSnapshot.
|
|
//!
|
|
//! This is the highest-fidelity test layer: no mocks, no in-process bridge.
|
|
//! The server runs as a separate OS process, exactly as it does in production.
|
|
//!
|
|
//! Spec references: D-020 (subprocess IPC), D-030 (Layer 3 integration tests)
|
|
|
|
use settled_reach_server::bridge::framing::{read_framed, write_framed};
|
|
use settled_reach_server::bridge::types::*;
|
|
use std::io::{BufRead, BufReader, BufWriter};
|
|
use std::net::TcpStream;
|
|
use std::process::{Command, Stdio};
|
|
use std::time::{Duration, Instant};
|
|
|
|
/// Timeout for the server to emit LISTENING:{port} on stdout.
|
|
const LISTEN_TIMEOUT: Duration = Duration::from_secs(15);
|
|
|
|
/// Timeout for the client to receive a snapshot after sending input.
|
|
const SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(10);
|
|
|
|
#[test]
|
|
fn server_subprocess_sends_snapshot_on_connect() {
|
|
// 1. Spawn server binary with --test-mode --port 0
|
|
let server_bin = env!("CARGO_BIN_EXE_settled-reach-server");
|
|
let mut child = Command::new(server_bin)
|
|
.args(["--test-mode", "--port", "0"])
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.expect("failed to spawn server binary");
|
|
|
|
let stdout = child.stdout.take().expect("stdout not captured");
|
|
let mut stdout_reader = BufReader::new(stdout);
|
|
|
|
// 2. Parse LISTENING:{port} from stdout
|
|
let port = {
|
|
let deadline = Instant::now() + LISTEN_TIMEOUT;
|
|
let mut line = String::new();
|
|
loop {
|
|
line.clear();
|
|
match stdout_reader.read_line(&mut line) {
|
|
Ok(0) => panic!("server stdout closed before LISTENING signal"),
|
|
Ok(_) => {
|
|
let trimmed = line.trim();
|
|
if let Some(port_str) = trimmed.strip_prefix("LISTENING:") {
|
|
break port_str
|
|
.parse::<u16>()
|
|
.unwrap_or_else(|e| panic!("invalid port '{}': {}", port_str, e));
|
|
}
|
|
}
|
|
Err(e) => panic!("failed to read server stdout: {}", e),
|
|
}
|
|
assert!(
|
|
Instant::now() < deadline,
|
|
"timed out waiting for LISTENING signal"
|
|
);
|
|
}
|
|
};
|
|
|
|
// 3. Connect to the server via TCP
|
|
let addr = format!("127.0.0.1:{}", port);
|
|
let stream = TcpStream::connect(&addr)
|
|
.unwrap_or_else(|e| panic!("failed to connect to server at {}: {}", addr, e));
|
|
stream
|
|
.set_read_timeout(Some(SNAPSHOT_TIMEOUT))
|
|
.expect("set read timeout");
|
|
|
|
let mut reader = BufReader::new(stream.try_clone().expect("clone stream for reader"));
|
|
let mut writer = BufWriter::new(stream);
|
|
|
|
// 4. Read the protocol handshake (first framed message, #555)
|
|
let handshake_frame = read_framed(&mut reader)
|
|
.expect("read handshake frame")
|
|
.expect("server closed connection before sending handshake");
|
|
// D-192: HandshakeMessage carries no version field. Verify it deserialises cleanly.
|
|
let _handshake: HandshakeMessage =
|
|
rmp_serde::from_slice(&handshake_frame).expect("deserialize HandshakeMessage");
|
|
|
|
// 5. Send StartupMessage with world_seed (#175)
|
|
let startup = StartupMessage { world_seed: 42 };
|
|
let startup_payload = rmp_serde::to_vec_named(&startup).expect("serialize StartupMessage");
|
|
write_framed(&mut writer, &startup_payload).expect("send StartupMessage to server");
|
|
|
|
// 6. Send one PlayerInput (idle tick 0)
|
|
let inputs = vec![PlayerInput {
|
|
tick: 0,
|
|
action: PlayerAction::MoveNorth,
|
|
}];
|
|
let payload = rmp_serde::to_vec_named(&inputs).expect("serialize PlayerInput");
|
|
write_framed(&mut writer, &payload).expect("send PlayerInput to server");
|
|
|
|
// 7. Read one ObserverSnapshot
|
|
let response = read_framed(&mut reader)
|
|
.expect("read snapshot frame")
|
|
.expect("server closed connection before sending snapshot");
|
|
let snapshot: ObserverSnapshot =
|
|
rmp_serde::from_slice(&response).expect("deserialize ObserverSnapshot");
|
|
|
|
// 8. Assert protocol correctness (D-020)
|
|
assert!(
|
|
snapshot.entities.len() > 0,
|
|
"snapshot should contain at least one entity (the player), got 0"
|
|
);
|
|
|
|
// The proof room has a player + NPCs. Verify the player entity exists.
|
|
let has_player = snapshot
|
|
.entities
|
|
.iter()
|
|
.any(|e| matches!(e.kind, EntityKind::Player));
|
|
assert!(has_player, "snapshot must contain a Player entity");
|
|
|
|
// 9. Clean up: drop connection so the server exits its game loop
|
|
drop(reader);
|
|
drop(writer);
|
|
|
|
// Wait for child to exit (with timeout)
|
|
let exit_deadline = Instant::now() + Duration::from_secs(5);
|
|
loop {
|
|
match child.try_wait() {
|
|
Ok(Some(_status)) => break,
|
|
Ok(None) => {
|
|
if Instant::now() > exit_deadline {
|
|
child.kill().ok();
|
|
child.wait().ok();
|
|
break;
|
|
}
|
|
std::thread::sleep(Duration::from_millis(50));
|
|
}
|
|
Err(e) => {
|
|
eprintln!("error waiting for server process: {}", e);
|
|
child.kill().ok();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|