Files
settled-reach/server/tests/layer3.rs
T
jpmschweitzerandClaude Opus 4.6 cae3d3ab85 refactor(simulation): strip archetype trace + HeritageRoot per cascade (#877, #878)
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>
2026-04-22 08:55:48 +02:00

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;
}
}
}
}