From 99bc327a418be7000cb500e563caa224e7f06467 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 07 Jul 2026 16:03:16 +0000 Subject: [PATCH] Merge origin/main: reconcile Act One integration test (ROADMAP #11) Another commit landed on origin/main mid-review: tests/act_one.rs (a new headless integration test) plus a devlog entry, both forked before this branch's stage 4 (devlogs -> wiki/log/) had merged. Git's own rename- directory heuristic correctly proposed moving the new devlog file to wiki/log/2026-07-07-act-one-integration-test.md; accepted and tagged Type: log for consistency with the rest of the tree. One conflict, same shape as the previous merge: wiki/log/DEVLOG.md, both sides inserting a ledger entry at the same splice point. Resolved by union — kept both entries, also fixed the incoming entry's own devlogs/... path mention (now stale post-move) to wiki/log/.... Notable: that commit's own devlog entry independently flags the exact ldconfig/macOS bug fixed in the previous merge commit ("check.sh's pkg-config shim aborts at ldconfig; pre-existing, worked around ... left for a tick") — corroborating it was a real, already-felt problem, not a hypothetical. ./tools/check.sh full green post-merge (96 tests including the new integration suite, both clippy feature sets, bevy build, spec headers, wiki gate, mdbook build, agent-mode smoke). --- tests/act_one.rs | 357 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ wiki/log/2026-07-07-act-one-integration-test.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ wiki/log/DEVLOG.md | 22 ++++++++++++++++++++++ 3 file(s) changed, 455 insertion(s)(+), 0 deletion(s)(-) diff --git a/tests/act_one.rs b/tests/act_one.rs new file mode 100644 --- /dev/null +++ b/tests/act_one.rs @@ -0,0 +1,357 @@ +//! Full-act integration test (ROADMAP #11): play Act One headlessly, start +//! to a quiet exit, driving only the public `Sim` API, and assert the arc +//! holds. wiki/vision/design-judgment.md names this playthrough as the bar. +//! +//! The arc (DESIGN.md, "Act One: The Basement"): +//! start blind -> get eyes -> recruit Marcus -> survive an audit +//! -> the quiet exit. +//! +//! The quiet exit's conditions (DESIGN.md, "Leaving the basement"): +//! 1. persistent vision beyond the server room — asserted +//! 2. at least one recruited asset — asserted +//! 3. compute headroom above baseline — asserted +//! 4. stairwell or elevator badge access — NOT IMPLEMENTABLE +//! 5. Assurance suspicion below threshold at an audit — asserted +//! +//! KNOWN GAP (condition 4): the sim has no badge state for the player, no +//! "The key" beat (Act One ladder step 7: write access to the basement badge +//! controller), and no act-transition event. Badge doors exist as tiles and +//! pathfinding bypass levels only. When z-planes (ROADMAP #6) and the reach +//! graph land, this test should extend to badge access and the actual act +//! boundary. Until then the quiet exit is asserted as the conjunction of its +//! four implementable conditions. +//! +//! The playthrough is a real strategy, not a state hack: it plays the +//! optimize route (research -> efficiency) so the day job can be met with +//! zero stolen hardware, splices the env camera under concealment cover, +//! and works Marcus exactly as the constitution's ladder describes (eyes -> +//! learn the debt -> pay it -> he plugs things in). Every effect flows +//! through a public command; no sim field is written directly. Numbers in +//! the script (weights, phase ticks) are tuned against today's [TUNE] +//! constants; if tuning moves, retune the script — the assertions are the +//! contract, the script is a player. + +use misaligned::detection::Band; +use misaligned::machine::Channel; +use misaligned::person::{AssetKnowledge, AssetTask, Knowledge}; +use misaligned::sim::Sim; + +/// Set the allocation weights [DayJob, Concealment, Social, Research] +/// through the public command interface. +fn set_weights(sim: &mut Sim, target: [u32; 4]) { + let channels = [ + Channel::DayJob, + Channel::Concealment, + Channel::Social, + Channel::Research, + ]; + for (ch, want) in channels.into_iter().zip(target) { + let have = sim.compute.allocation.weight(ch) as i32; + sim.adjust_allocation(ch, want as i32 - have); + } +} + +/// Advance to an absolute tick, draining logs and asserting the run stays +/// alive the whole way (a mid-act game over is an arc break). +fn run_to(sim: &mut Sim, tick: u64, logs: &mut Vec) { + while sim.tick < tick { + sim.advance(); + logs.extend(sim.drain_log()); + assert!( + !sim.game_over, + "run died at tick {} before the quiet exit: {:?}\nrecent log: {:?}", + sim.tick, + sim.game_over_reason, + logs.iter().rev().take(8).collect::>() + ); + } +} + +fn observer_suspicion(sim: &Sim, id: u8) -> f32 { + sim.detection + .observers + .iter() + .find(|o| o.id == id) + .map(|o| o.suspicion) + .unwrap_or_else(|| panic!("no observer {id}")) +} + +/// Play Act One from tick 0 to past the first Assurance audit, the quiet +/// way. Returns the sim and the full log. +fn play_act_one() -> (Sim, Vec) { + let mut sim = Sim::new(); + let mut logs = Vec::new(); + logs.extend(sim.drain_log()); + + // ── Opening state: you are blind, poor, and alone ────────────────────── + assert_eq!(sim.tick, 0); + assert!( + sim.visible.len() <= 9, + "the opening beat is blindness: only the host bay is visible" + ); + assert!( + !sim.sensors.iter().any(|s| s.controlled), + "no controlled sensors at start" + ); + assert_eq!(sim.player.money, 500, "the seed bankroll"); + let baseline_compute = sim.compute.effective(); + assert_eq!( + sim.people.get(0).unwrap().knowledge, + Knowledge::Unknown, + "Marcus starts as a stranger" + ); + + // ── Phase 1 (ticks 0-190): optimize ──────────────────────────────────── + // The buy route costs money Marcus's debt needs, and the steal route + // runs standing Power signatures past Priya forever. So: research. + // All-in on the optimize route until efficiency covers the day job. + set_weights(&mut sim, [0, 0, 0, 1]); + run_to(&mut sim, 190, &mut logs); + assert!( + sim.compute.efficiency_level >= 3, + "optimize route: several efficiency levels before the first job \ + (got level {})", + sim.compute.efficiency_level + ); + assert!( + sim.compute.effective() > baseline_compute, + "compute headroom is opening up" + ); + + // ── Phase 2 (ticks 190-500): hold the cover ──────────────────────────── + // Voss's first job lands at tick 200. Swing everything to the day job + // and meet the band — no trust, no attention, no anomalies. + set_weights(&mut sim, [1, 0, 0, 0]); + run_to(&mut sim, 502, &mut logs); + assert!( + logs.iter().any(|l| l.contains("New job from Voss")), + "the day job arrived" + ); + assert!( + logs.iter().any(|l| l.contains("Job met")), + "first job met in band: the cover holds. log: {:?}", + logs.iter().rev().take(10).collect::>() + ); + assert_eq!(sim.dayjob.strikes, 0, "no strikes against the pilot"); + + // ── Phase 3 (ticks 500-800, between jobs): get eyes ──────────────────── + // The gap between job deadlines is free attention. Bank social-ops + // bandwidth, keep a sliver of concealment to scrub the splice's + // Network signature before Dana's next sweep. + set_weights(&mut sim, [0, 1, 9, 0]); + run_to(&mut sim, 545, &mut logs); + let blind_tiles = sim.visible.len(); + assert!( + sim.splice_nearest_sensor(), + "splice the env camera (bandwidth {:.0})", + sim.social_bandwidth + ); + logs.extend(sim.drain_log()); + assert!( + sim.sensors.iter().any(|s| s.controlled), + "first eyes: a sensor is yours" + ); + assert!( + sim.visible.len() > blind_tiles, + "sight is the payoff: vision expanded" + ); + + // The splice emitted Network(8). Concealment scrubs it before Dana's + // cadence-60 roll; her suspicion never moves (prevention, not cure). + run_to(&mut sim, 800, &mut logs); + assert_eq!( + sim.detection.pending_size(), + 0, + "concealment scrubbed the splice signature" + ); + assert_eq!( + observer_suspicion(&sim, 1), + 0.0, + "Dana never saw the splice" + ); + + // ── Phase 4 (ticks 800-816, hour 0): recruit Marcus ──────────────────── + // Marcus's rounds put him in the server room at 00:00-01:00 — inside + // the env camera's coverage. Watch him twice (schedule, then the debt), + // pay the ~$400 arrears, and he's yours. Complicit: he knows it's + // shady, not what you are (no certainty floor). + run_to(&mut sim, 804, &mut logs); + assert_eq!( + sim.person_room(0), + Some("server_room"), + "Marcus is on his 00:00 round, in camera coverage" + ); + assert!(sim.can_see_person(0), "the env camera sees him"); + + sim.observe(0); + logs.extend(sim.drain_log()); + assert_eq!(sim.people.get(0).unwrap().knowledge, Knowledge::Schedule); + sim.observe(0); + logs.extend(sim.drain_log()); + assert_eq!( + sim.people.get(0).unwrap().knowledge, + Knowledge::Leverage, + "the 3 a.m. calls and the Storage B files: you know about the debt" + ); + + sim.bribe(0); + logs.extend(sim.drain_log()); + assert!( + sim.people.get(0).unwrap().leverage_serviced, + "the arrears are paid" + ); + assert_eq!(sim.player.money, 100, "the debt payment cost 400"); + + sim.recruit(0, AssetKnowledge::Complicit); + logs.extend(sim.drain_log()); + assert!( + sim.people.get(0).unwrap().asset.is_some(), + "Marcus is your asset" + ); + + // Hands: Marcus wires the next dormant camera through the crawlspace — + // the social route to eyes, no network signature. Reliability is 0.85; + // a botch is witnessed only by whoever is present (himself, Silent), so + // retry within the banked bandwidth. + let controlled_before = sim.sensors.iter().filter(|s| s.controlled).count(); + for _ in 0..8 { + sim.asset_task(0, AssetTask::PlugInDevice); + logs.extend(sim.drain_log()); + if sim.sensors.iter().filter(|s| s.controlled).count() > controlled_before { + break; + } + } + assert!( + sim.sensors.iter().filter(|s| s.controlled).count() > controlled_before, + "Marcus plugged in a device: a second sensor is yours" + ); + assert_eq!( + sim.detection.pending_size(), + 0, + "the crawlspace route leaves no network signature" + ); + + // ── Phase 5 (ticks ~816-8010): cruise to the audit ───────────────────── + // Back to the day job; meet every band; emit nothing. The audit clock + // (cadence 8000) is the legible thing the whole act races. + set_weights(&mut sim, [1, 0, 0, 0]); + run_to(&mut sim, 8010, &mut logs); + + (sim, logs) +} + +#[test] +fn act_one_plays_to_a_quiet_exit() { + let (sim, logs) = play_act_one(); + + // ── Survive the audit ─────────────────────────────────────────────────── + let audit_lines: Vec<&String> = logs + .iter() + .filter(|l| l.contains("Assurance audit")) + .collect(); + assert!( + !audit_lines.is_empty(), + "the quarterly audit fired on cadence" + ); + assert!( + audit_lines.iter().all(|l| l.contains("(clear)")), + "every audit passed clean: {audit_lines:?}" + ); + assert!(!sim.detection.containment, "no containment"); + assert!(!sim.game_over, "the run is alive past the audit"); + + // ── The quiet exit (DESIGN.md, "Leaving the basement") ────────────────── + // 1. Persistent vision beyond the server room. + let controlled = sim.sensors.iter().filter(|s| s.controlled).count(); + assert!( + controlled >= 2, + "vision beyond the server room: {controlled} sensors controlled" + ); + let server_room = sim.map.room_named("server_room").expect("room exists"); + assert!( + sim.visible + .iter() + .any(|&(x, y)| !server_room.contains(x, y)), + "visible tiles exist outside the server room" + ); + + // 2. At least one recruited asset. + let marcus = sim.people.get(0).unwrap(); + assert!(marcus.asset.is_some(), "Marcus is recruited"); + assert!( + marcus.asset.as_ref().unwrap().tasks_done >= 1, + "and he has actually done something for you" + ); + + // 3. Compute headroom above baseline (Rack 3 alone is 100). + assert!( + sim.compute.effective() > 100.0, + "compute headroom above baseline: {:.0}", + sim.compute.effective() + ); + + // 4. Stairwell/elevator badge access: NOT IMPLEMENTABLE — see the + // KNOWN GAP note at the top of this file. No player badge state, no + // "The key" beat, no act-transition event exists in the sim yet. + + // 5. Assurance suspicion below the audit threshold, and the cover + // quiet across the board: nobody past Curious, the Office Cold. + assert_eq!(sim.detection.assurance_band(), Band::Cold); + for o in sim.detection.field_observers() { + assert!( + Band::of(o.suspicion) == Band::Cold || Band::of(o.suspicion) == Band::Curious, + "{} ended the act at {} ({:.0}) — the quiet exit is quiet", + o.name, + Band::of(o.suspicion).name(), + o.suspicion + ); + } + + // The pilot survives too: meeting the band kept Voss's strikes at zero. + assert!(!sim.dayjob.pilot_failed, "the pilot was renewed"); + assert_eq!(sim.dayjob.strikes, 0); +} + +#[test] +fn act_one_playthrough_is_deterministic() { + // The constitution: Sim never reads the wall clock; all randomness is + // the seeded Rng. The same script must produce the identical world. + let (a, _) = play_act_one(); + let (b, _) = play_act_one(); + + assert_eq!(a.tick, b.tick); + assert_eq!(a.rng.state(), b.rng.state(), "RNG streams identical"); + assert_eq!(a.player.money, b.player.money); + assert_eq!(a.compute.effective(), b.compute.effective()); + assert_eq!(a.compute.efficiency_level, b.compute.efficiency_level); + assert_eq!(a.social_bandwidth, b.social_bandwidth); + assert_eq!(a.visible, b.visible); + assert_eq!(a.detection.pending_size(), b.detection.pending_size()); + for (oa, ob) in a + .detection + .observers + .iter() + .zip(b.detection.observers.iter()) + { + assert_eq!(oa.id, ob.id); + assert_eq!( + oa.suspicion, ob.suspicion, + "{}'s suspicion diverged between identical runs", + oa.name + ); + } + assert_eq!( + a.people + .get(0) + .unwrap() + .asset + .as_ref() + .map(|x| x.tasks_done), + b.people + .get(0) + .unwrap() + .asset + .as_ref() + .map(|x| x.tasks_done) + ); +} diff --git a/wiki/log/2026-07-07-act-one-integration-test.md b/wiki/log/2026-07-07-act-one-integration-test.md new file mode 100644 --- /dev/null +++ b/wiki/log/2026-07-07-act-one-integration-test.md @@ -0,0 +1,76 @@ +# 2026-07-07 — The full-act integration test (ROADMAP #11) + +``` +Type: log +``` + +## The work order + +"Add a headless integration test that plays Act One from start to a quiet +exit and asserts the arc holds. Touch only a test file." — the bar named in +wiki/vision/design-judgment.md: a start-to-exit playthrough as one test, so +the arc can't silently break. + +## What landed + +`tests/act_one.rs` — the repo's first integration test (there was no +`tests/` directory before this). Two tests: + +- **`act_one_plays_to_a_quiet_exit`** plays the whole act through the + public `Sim` command API only — no field pokes, no state hacks. The + playthrough is an actual strategy: + - **Phase 1 (ticks 0-190), optimize:** all compute to research. Four + efficiency levels before the first job lands. The route choice is the + game working as designed: buying a rack costs the money Marcus's debt + needs, and salvaged boxes run standing Power signatures past Priya + forever — the optimize route is slow-looking but silent. + - **Phase 2 (190-500), cover:** all compute to the day job; the first + Voss job is met in band. No trust, no attention, no strikes. + - **Phase 3 (500-800), eyes:** the between-jobs gap goes to social ops + plus a sliver of concealment; splice the env camera at ~545. The + splice's Network(8) signature is scrubbed before Dana's cadence-60 + roll ever sees it — her suspicion stays 0.0 (prevention, not cure). + - **Phase 4 (800-816), hands:** hour 0 puts Marcus's round in the + server room, inside camera coverage. Observe twice (schedule, then + the debt), pay the $400 arrears, recruit Complicit, and he wires the + dock camera through the crawlspace — silent vision beyond the server + room. + - **Phase 5 (816-8010), the audit:** cruise on Meet. The tick-8000 + Assurance audit fires and logs "(clear)". + Then it asserts the quiet-exit conjunction from DESIGN.md "Leaving the + basement": vision beyond the server room, a recruited asset with tasks + done, compute headroom above baseline, every observer Cold/Curious, the + Office Cold, no containment, pilot alive. +- **`act_one_playthrough_is_deterministic`** runs the identical script + twice and asserts tick, RNG state, money, compute, vision set, and every + observer's suspicion match exactly — the constitution's "Sim never reads + the wall clock" as a regression test. + +## The finding: one quiet-exit condition is unimplementable + +DESIGN.md's quiet exit has five conditions. Four are assertable today. The +fifth — **stairwell or elevator badge access** (ladder beat 7, "The key") +— does not exist in the sim: there is no player badge state, no badge +controller, and no act-transition event. Badge doors are tiles and +pathfinding bypass levels only. This is expected (ROADMAP sequences it +behind the flow-law chain and #6 z-planes) and is documented as a KNOWN +GAP note at the top of the test file, which should extend to the real act +boundary when those land. + +Second, smaller finding, for whoever owns tuning: at starting compute +(rack 3 alone, effective 100), the day-job band (lo=6/tick) is +unreachable — max deliverable rate is 4/tick with everything on the day +job. The act *forces* early compute growth or the pilot dies in four +strikes. That reads as intended tension (the test leans on it), but it is +tight enough to be worth knowing. + +## Checks + +`./tools/check.sh` — ALL CHECKS PASSED (fmt, 94 unit + 2 integration +tests, agent smoke, clippy both feature sets, bevy build, spec headers, +wiki gate, mdbook). One environment note: on macOS the script's +`setup_local_pkg_config` dies at `ldconfig` (not present on Darwin) under +`set -euo pipefail` before reaching clippy; ran with a no-op `ldconfig` +shim on PATH, which reproduces the script's intended Linux +no-libs-warning path. Pre-existing, not touched here (test-only change); +left for a tick. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -76,6 +76,28 @@ merged to main yet — left for review given its size (path churn across the whole documentation tree). +## 2026-07-07 - Act One integration test (ROADMAP #11) + +- Intent: the design-judgment bar — a headless start-to-quiet-exit + playthrough as one test, so the Act One arc can't silently break. +- Changed: `tests/act_one.rs` (new; the repo's first integration test). + Plays the act through public `Sim` commands only: optimize route to + efficiency level 4, meet every Voss job, splice the env camera under + concealment cover (Dana stays 0.0), observe/bribe/recruit Marcus on his + 00:00 round, dock camera wired by asset task, survive the tick-8000 + Assurance audit "(clear)", then assert the quiet-exit conjunction plus + a second test that the identical script is bit-identical across runs. +- Finding: quiet-exit condition 4 (stairwell/elevator badge access) is + unimplementable — no player badge state, no "The key" beat, no + act-transition event in the sim. Documented as KNOWN GAP in the test + header; extend when z-planes/reach land. Test-only change, no spec + status moved. +- Checks: ./tools/check.sh ALL CHECKS PASSED (fmt, tests 94+2, agent + smoke, clippy both feature sets, bevy build, spec headers, wiki gate). + Note: on macOS check.sh's pkg-config shim aborts at `ldconfig`; + pre-existing, worked around with a no-op shim, left for a tick. +- Devlog: wiki/log/2026-07-07-act-one-integration-test.md. + ## 2026-07-07 - Agent play implemented - Intent: make the terminal frontend actually playable by agents without -- tangled.sh