Files
settled-reach/docs/workshops/v01-gap-analysis/round1-tyre.md
T
jpmschweitzerandClaude Opus 4.6 ba3ade434e docs(docs): add frontmatter to v01-gap-analysis workshop
Standardized YAML frontmatter on all 16 files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:01:28 +01:00

36 KiB
Raw Blame History

title, description, type, status, workshop, agent, round, created
title description type status workshop agent round created
Workshop Round 1 — Tyre (Technical Architect) Technical gap analysis identifying hard blockers (collision, pathfinding, time system) and testability architecture recommendations workshop archived v01-gap-analysis tyre 1 2026-02-11

Workshop Round 1 — Tyre (Technical Architect)

Tracks: 3 (Things We May Have Forgotten) + 4 (Testability Deep-Dive) Date: 2026-02-11


TRACK 3: Things We May Have Forgotten — Technical Gap Analysis

cracks knuckles

Let me be honest about what's missing. I've gone through the 232 tickets systematically, cross-referenced with confirmed decisions, and found genuine gaps. I'll categorize each as HARD BLOCKER (v0.1 cannot ship without this), SOFT BLOCKER (v0.1 works but feels broken without it), or NICE-TO-HAVE (defer safely).


3.1 Pathfinding — HARD BLOCKER

Status: No ticket exists. Zero coverage.

NPCs need to navigate the station district. Ticket #101 (Routine execution system) says "schedule-driven NPC movement, location transitions at scheduled times" but there is no system to actually move an NPC from point A to point B. #83 (Moving character proof) handles player movement via direct input, but NPCs need autonomous navigation.

What's needed:

  • A* grid pathfinding on the tile map. For a 150x150 tile map, this is well within performance budget.
  • Wall/obstacle avoidance from the tile collision data.
  • Path caching — NPCs following daily routines walk the same paths repeatedly. Cache hot paths.
  • Door/gate traversal — the station district has a gate corridor (#157). NPCs need to know how to use doors.

Implementation in Rust/bevy_ecs: There's no built-in pathfinding in bevy_ecs. Options:

  1. pathfinding crate (crates.io) — mature, has A* and Dijkstra, works on arbitrary graphs. ~10 lines to integrate with tile grid. Recommended.
  2. Custom A* — it's a well-known algorithm, easy to implement. But why reinvent it?
  3. Navigation mesh — overkill for a tile-based top-down game. Save this for if we ever do free-form movement.

Effort estimate: 1-2 stories. One for the pathfinding system itself, one for integrating it with NPC routine execution (#101).

New tickets needed:

  • [story] Tile-based A* pathfinding system (high priority, blocks #101)
  • [story] NPC path following and movement (high priority, blocks #101)

3.2 Collision Detection — HARD BLOCKER

Status: No ticket exists. Zero coverage.

Both player movement (#83) and NPC movement (#101) need collision detection. Without it, entities walk through walls.

What's needed:

  • Tile-based collision map — walls, furniture, objects marked as solid tiles. This comes from the tile map data.
  • Entity-tile collision check before movement — "can I move to tile X,Y?" query.
  • Entity-entity collision (optional for v0.1) — preventing NPCs from stacking on the same tile. Low priority but feels weird without it.

Architecture note: Collision lives in the Rust simulation, not the Godot client. The simulation is authoritative (D-010). The client just renders where entities are.

What this is NOT:

  • Physics simulation. No rigid bodies, no forces, no momentum. This is tile-based "is the target tile walkable?" checks.
  • Complex collision shapes. Entities occupy tiles. Tiles are solid or not.

Effort estimate: 1 story. Tile collision map + movement validation. Small but load-bearing.

New tickets needed:

  • [story] Tile collision system (critical priority, blocks #83 and #101)

3.3 Z-levels — NICE-TO-HAVE (for v0.1)

Status: #114 (Multi-z-level LOS) is low priority. Correct.

Assessment: The station district (#153) could be designed as a single z-level for v0.1. D-014 says "2-3 z-levels" but that's the full spec — the vertical slice can work on one floor.

Recommendation: Design the station district as single-level for v0.1. Add a second floor as a stretch goal. The chunk-based map architecture (D-012) already supports z-levels in the data model — we just don't need to render or pathfind across them yet.

Risk if deferred: None for v0.1. The data model supports it. Adding z-levels later is an extension, not a rewrite.

No new tickets needed. #114 stays low priority. The station district layout (#153) should note "single z-level for v0.1."


3.4 Time System — HARD BLOCKER

Status: Q-009 is open. No ticket beyond #25 (Time system, medium priority, backlog).

This is a genuine blocker. Daily routines (#88) say "time-based transitions" but there is no time system defined. Every NPC behavior system depends on knowing what time it is.

What Q-009 needs to decide (minimum for v0.1):

  1. Tick-to-game-time mapping. If the simulation runs at 10-20 tps (D-026), how many ticks = 1 game-minute? Proposal: 1 game-minute = 10 ticks at 10 tps (= 1 real second). A 30-minute play session = ~12-18 game-hours. This gives a full "day" of NPC routines in one play session.
  2. Day structure. Morning/afternoon/evening/night phases that drive routine transitions. NPCs go to work in the morning, to the bar in the evening. Simple phase system.
  3. Time display. Player needs to know what time it is (insert HUD element). Diegetic — it's on their neural insert.
  4. Pause. Can the player pause? Probably yes for v0.1 (single-player). Simulation freezes, UI stays responsive.

What we do NOT need for v0.1:

  • Deep time (years, decades) — v0.1 is one play session
  • Day/night lighting changes — placeholder art doesn't need this
  • Seasonal cycles
  • Time zone differences between locations

Effort estimate: 1 story for the time system, 1 story for the time display. Promotes #25 from medium to high priority.

New tickets needed:

  • Promote #25 to high priority (blocks #88)
  • [story] Game clock and day-phase system (high priority, blocks #88)
  • [story] Time display on insert HUD (medium priority)

3.5 Audio Engine — SOFT BLOCKER

Status: Sound propagation is specced (#124-128) but there's no basic audio foundation ticket.

The gap: Godot 4 has a built-in AudioServer with AudioStreamPlayer2D for positional audio. This is the client-side playback engine — we don't need to build one. But there's no ticket for:

  • Setting up the audio bus layout in the Godot project
  • Loading and managing sound assets
  • Connecting sound events from the ObserverSnapshot to AudioStreamPlayer2D instances
  • Basic ambient/environmental audio (background hum of the station)

Tickets #124-128 jump straight to sophisticated sound propagation. The bridge between "server generates SoundEvent" and "client plays audio" is assumed but not specified.

What's needed for v0.1:

  • Client-side audio manager that receives sound events from snapshot and plays them
  • A small library of placeholder sounds (footsteps, door, conversation murmur, alert)
  • Spatial audio positioning relative to camera

Effort estimate: 1 story. This is Godot built-in functionality with a thin integration layer.

New tickets needed:

  • [story] Client audio manager and spatial playback (high priority, supports #124-125)

3.6 Asset Pipeline — SOFT BLOCKER

Status: #133 (Placeholder art pipeline) exists but lacks specifics.

What #133 needs to specify (or a companion design ticket):

  • Tile size: 32x32 pixels is the sweet spot for top-down. 16x16 is too small for readable detail. 48x48 is feasible but unusual. Recommendation: 32x32.
  • Sprite dimensions: Character sprites. 32x48 (taller than wide) is standard for top-down characters standing on 32x32 tiles.
  • Animation frames: For v0.1 placeholder art? Minimal: idle (1 frame), walk (4 frames per direction), interact (2 frames). That's ~18 frames per character × 4 directions = ~72 frames. For colored boxes with labels? 1-2 frames per state.
  • Tileset format: Godot TileSet resource. Atlas-based or individual tiles? Atlas is better for performance.
  • Naming conventions: npc_smuggler_walk_north_01.png or sprite sheet coordinates?
  • Color coding for placeholder art: Since D-014 says "colored boxes with labels" — which colors mean what? Walls = dark gray, floor = light gray, NPC = colored by role, player = distinct color, interactable objects = highlighted.

Recommendation: This is a design discussion for Araminta + Stig, but needs a ticket. The art direction question (Q-003) doesn't need full resolution for v0.1 — we just need placeholder conventions.

Effort estimate: Half a story to define the spec, which then unblocks #133.

New tickets needed:

  • [task] Define placeholder art specification (high priority, blocks #133)

3.7 Save/Load — SOFT BLOCKER

Status: Complete gap. No ticket anywhere.

Let me be honest about what this means technically:

Serializing a bevy_ecs World is non-trivial. Options:

  1. bevy_reflect + bevy_scene: Bevy has a reflection system that can serialize entities and components. It works but requires all components to derive Reflect. This is the "Bevy way" but couples us to Bevy's serialization format.

  2. Custom serialization: Define a SaveState struct that captures the game state we care about (entity positions, NPC states, relationship data, time, knowledge graphs, quest states). Serialize with serde + bincode or MessagePack. Deserialize by reconstructing the ECS world from SaveState.

  3. Snapshot-based: The simulation already produces ObserverSnapshots. A save is just a full-world snapshot (not observer-filtered). This leverages existing serialization infrastructure.

Recommendation for v0.1: Option 2 (custom serialization). We control the save format, it's independent of bevy_ecs internals, and it doubles as the foundation for the state-save tier in D-026 (which already needs "frozen serialized structs ~1-2KB each" for state-saved NPCs).

What's needed:

  • SaveState data structure capturing full game state
  • Serialize to file (serde + bincode or MessagePack)
  • Deserialize and reconstruct ECS world
  • "Save and quit" / "Load and resume" flow in the client

Effort estimate: 2-3 stories. This is real work, but it shares architecture with the simulation tier serialization (#96 - State serialization system). They should be designed together.

New tickets needed:

  • [story] Save state data model and serialization (high priority, shares design with #96)
  • [story] Save/load game flow - client integration (medium priority)

Architecture note: #96 (State serialization system) already covers serializing NPC state for tier transitions. Save/load is "serialize everything, not just one NPC." The same serialization infrastructure serves both purposes. These should share implementation.


3.8 Session Management — SOFT BLOCKER

Status: No ticket.

What happens when the player launches the game?

  1. Main menu (new game / load game / settings / quit)
  2. New game → character selection (smuggler or detective)
  3. World generation (station district instantiation, NPC generation, template placement)
  4. Game starts — player character placed in starting location

What happens when the player stops playing?

  1. Pause → save and quit
  2. Quit without saving → warning prompt
  3. Return → load from save → resume

For v0.1, this can be minimal:

  • No main menu (launch directly into game)
  • Character selection via command-line argument or simple dialog
  • Auto-save on quit
  • Auto-load on start if save exists

Effort estimate: 1 story for the basic session flow. Low complexity.

New tickets needed:

  • [story] Game session management - start/save/resume flow (medium priority)

3.9 Game Over / Failure States — NICE-TO-HAVE (for v0.1)

Status: No ticket. But also — does v0.1 need failure states?

D-027 success criteria focus on: 30-min daily life runway, different playthroughs, emotional NPC attachment, emergent observation. None of these require game-over conditions.

D-008 says death = information loss via memory cell backup. But for v0.1: is there combat? The smuggling ring template is about deception and social dynamics, not combat. The detective investigates; they don't get killed (in v0.1).

Recommendation: Defer game-over/failure states to post-v0.1. The vertical slice is about proving the information asymmetry concept, not about win/lose conditions. If the 30-minute session ends, it ends — that's success criterion #1.

What we MIGHT want for v0.1:

  • "Session complete" screen after the 30-min runway
  • Basic stats: "You observed X, discovered Y, talked to Z NPCs"
  • Comparison between smuggler and detective perspectives

Effort estimate if included: 1 story. Low priority.

No new tickets needed unless the team wants a session-end summary screen.


3.10 Content Volume — FEASIBLE BUT TIGHT

Status: This is a scope question, not a missing ticket.

The math:

  • 3 social site templates: workplace (4-8 NPCs), bar (4-8 NPCs), smuggling ring (4-8 NPCs)
  • Single-ownership model (D-025) means some NPCs are shared via reference links
  • Realistic count: ~20-25 unique NPCs with ~5-8 shared across templates
  • Each NPC has 10 axes (D-024): Want, Secret, Relationships ×3, Tolerance, Routine, Information, Contentment + 3 supporting
  • Entanglement ratio (D-029): 30% flat / 50% mundane triangles / 20% intrigue

For 20-25 NPCs with the 30/50/20 split:

  • 5-6 flat NPCs (routine + greeting only)
  • 10-13 mundane triangle NPCs (neighbor disputes, workplace rivalries)
  • 4-5 intrigue-entangled NPCs (smuggling ring connections)

Is this achievable?

  • Data modeling: Yes. 25 NPCs × 10 axes = 250 data points. Well within authoring budget.
  • Content authoring (Mellanie): 3 templates × 165-210 lines (D-028) = ~495-630 authored lines before generation expansion. That's a significant but bounded writing task. The line previewer CLI (#193) must be ready before Mellanie starts — it's her authoring tool.
  • Runtime simulation: 25 NPCs in active tier at 10-20 tps? Trivially easy. D-026 budgets for 30-80 active NPCs. We're well under ceiling.
  • Triangle generation: D-024 says 2 triangles per template minimum + 1 cross-template = 7 triangles minimum across 3 templates. Each triangle = 3 NPCs with conflicting interests. 7 triangles involving ~20 NPCs is tight but workable — NPCs participate in multiple triangles.

Verdict: Feasible. The bottleneck is content authoring time, not technical capability. The line previewer CLI (#193) is correctly marked critical — it's the tool that makes the writing volume manageable.

Risk: If Mellanie's content authoring takes longer than expected, reduce the mundane triangle NPCs first. The flat NPCs and intrigue NPCs are load-bearing; the mundane triangles are the adjustable dial.


3.11 Additional Gaps Discovered

3.11.1 Player Interaction System — HARD BLOCKER

Status: Partially covered but fragmented.

#73 (Input capture) maps keys to semantic actions. #230 (PlayerInput structure) includes Interact as an action type. But there's no ticket for what happens when the player interacts:

  • Walk up to NPC → press Interact → what system handles this? → dialogue system (#168-174)
  • Walk up to door → press Interact → door opens? No door system ticket.
  • Walk up to object → press Interact → examine? No examination system ticket.
  • Walk up to terminal → press Interact → access information? No terminal interaction ticket.

The gap: There's no interaction dispatcher — the system that takes "player pressed Interact near entity X" and routes to the appropriate subsystem (dialogue, object examination, door, terminal).

New tickets needed:

  • [story] Interaction dispatcher system (high priority) — takes Interact input + proximity + target entity type → routes to appropriate handler

3.11.2 NPC Movement System — HARD BLOCKER

Status: Implied but not explicitly ticketed.

#101 (Routine execution system) implies NPCs move between locations. #68 (Basic entity components) gives entities Position and Facing. But there's no explicit "NPC movement system" that updates position per tick along a path.

This is distinct from pathfinding (finding the route) — this is the actual per-tick position update, animation state, facing direction change.

This is part of the pathfinding gap (3.1) but worth calling out: the movement system is the thing that runs every tick, consuming path waypoints and updating Position. Pathfinding generates the path; movement follows it.

New tickets needed:

  • Already covered by the pathfinding tickets proposed in 3.1. The "NPC path following and movement" story covers this.

3.11.3 Player Journal / Knowledge Tracking UI — SOFT BLOCKER

Status: No ticket.

#89 (Information inventory) tracks what the player character knows in the simulation. But there's no client-side UI for the player to review what they've learned. The insert/minimap (#148-152) handles spatial navigation. Where does the player see their accumulated knowledge?

For v0.1 this could be minimal: a simple list of "things you know" accessible via a key press. But without it, the player has to remember everything themselves, which undermines the insert concept (D-013).

New tickets needed:

  • [story] Knowledge/journal display - client (medium priority) — shows information inventory contents in insert UI

Track 3 Summary: New Tickets Needed

Priority Title Blocks Effort
Critical Tile collision system #83, #101 S
High Tile-based A* pathfinding system #101 M
High NPC path following and movement #101 M
High Game clock and day-phase system (promote/replace #25) #88 M
High Interaction dispatcher system #168 (dialogue) M
High Client audio manager and spatial playback #124, #125 S
High Define placeholder art specification #133 XS
High Save state data model and serialization shared with #96 L
Medium Save/load game flow - client integration M
Medium Game session management - start/save/resume S
Medium Time display on insert HUD S
Medium Knowledge/journal display - client M

S = Small (1-3 days), M = Medium (3-5 days), L = Large (5-10 days), XS = Extra Small (< 1 day)

Hard blockers (v0.1 cannot ship): Collision, pathfinding, NPC movement, time system, interaction dispatcher Soft blockers (v0.1 feels broken): Audio manager, asset spec, save/load, session management Nice-to-have: Knowledge journal, game-over states, z-levels


TRACK 4: Testability Deep-Dive

4.1 Rust: Testing Strategy Recommendation — HYBRID

Recommendation: #[cfg(test)] for unit tests + external integration tests.

Here's why a hybrid approach is right:

Unit tests with #[cfg(test)] — inside modules

For: Testing internal algorithms where the public API is too coarse.

Examples:

  • Shadowcasting algorithm internals (#110) — does ray N correctly identify wall tile at (x,y)?
  • Pathfinding heuristic (#new) — does A* correctly navigate around an L-shaped wall?
  • Perception query filtering (#112) — does the vision cone correctly exclude tiles behind the observer?
  • Time-to-tick conversion (#new) — does 14:30 game-time map to the correct tick number?

These are pure functions or small systems that have well-defined inputs and outputs. They live right next to the code they test. They run with cargo test instantly.

// simulation/src/perception/shadowcast.rs

pub fn compute_visible_tiles(origin: IVec2, range: i32, is_opaque: impl Fn(IVec2) -> bool) -> HashSet<IVec2> {
    // ... shadowcasting implementation
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn wall_blocks_vision() {
        let opaque = |pos: IVec2| pos == IVec2::new(2, 0); // wall at (2,0)
        let visible = compute_visible_tiles(IVec2::ZERO, 5, opaque);
        assert!(visible.contains(&IVec2::new(1, 0))); // before wall
        assert!(!visible.contains(&IVec2::new(3, 0))); // behind wall
    }
}

Integration tests — external tests/ directory

For: Testing the simulation as a consumer (the Godot client) would use it.

These exercise the full tick loop: create a World, add entities, run N ticks, assert on resulting state.

// simulation/tests/tick_integration.rs

use simulation::{SimulationBuilder, PlayerInput, ActionType};

#[test]
fn player_move_updates_position() {
    let mut sim = SimulationBuilder::new()
        .with_test_map(10, 10)
        .with_player_at(5, 5)
        .build();

    sim.queue_input(PlayerInput::new(ActionType::Move(Direction::North)));
    sim.tick();

    let snapshot = sim.observer_snapshot(sim.player_entity());
    assert_eq!(snapshot.player_position, IVec2::new(5, 4));
}

These tests prove the public API works. They're how Dudley (server dev) knows the simulation is correct from the client's perspective. They exercise system scheduling, component interactions, and the observer query pipeline.

Why not external test crate only?

Because some internal algorithms need testing at a granularity the public API can't reach. The shadowcasting algorithm might have 15 edge cases (corners, T-junctions, diagonal walls) that need individual tests. Testing each through the full simulation tick loop is possible but wasteful and fragile.

Why not #[cfg(test)] only?

Because the most important tests are the ones that exercise the real system — full tick, real scheduling, actual observer queries. Those MUST use the public API to catch integration bugs.

Workspace structure:

simulation/
  Cargo.toml                    # [lib] + [[test]] targets
  src/
    lib.rs                      # Public API: SimulationBuilder, tick(), observer_snapshot()
    perception/
      shadowcast.rs             # Has #[cfg(test)] mod tests
      vision_cone.rs            # Has #[cfg(test)] mod tests
    navigation/
      pathfinding.rs            # Has #[cfg(test)] mod tests
    ecs/
      components.rs
      systems.rs
  tests/
    tick_integration.rs         # Integration: full tick loop
    observer_snapshot.rs        # Integration: observer queries
    npc_behavior.rs             # Integration: NPC routines + pathfinding
    information_boundary.rs     # Integration: info filtering

4.2 Godot: Framework Recommendation — GUT

Recommendation: GUT (Godot Unit Testing) framework.

Why GUT over gdUnit4:

Criterion GUT gdUnit4
Maturity 8+ years, battle-tested Newer, less proven
Documentation Comprehensive wiki Growing but thinner
Community Larger, more Stack Overflow answers Smaller
GDScript-native Yes Yes
Headless support Yes (--headless) Yes but less tested
Signal testing Good Better (more fluent API)
Test doubles Built-in Built-in
Claude Code compatibility Better documented patterns Less AI training data
Complexity Simpler, less magic More features, more complex

gdUnit4 has a more modern API (fluent assertions, better signal testing) but GUT's maturity and simplicity win for a project where the Godot client is intentionally thin. We're not testing complex GDScript logic — we're testing that the client correctly renders snapshots and captures input.

Can Claude Code run godot --headless?

Yes, with constraints.

Godot 4 supports --headless mode which runs without a display server (no X11/Wayland needed). This is how CI/CD pipelines run Godot tests.

godot --headless --script res://addons/gut/gut_cmdline.gd \
  -gdir=res://test/ -gprefix=test_ -gsuffix=.gd -gexit

Requirements:

  1. Godot 4 binary must be installed and on PATH (or referenced by absolute path)
  2. The project must be initialized (.godot/ directory with imported resources)
  3. First run may need godot --headless --import to build the import cache
  4. No GPU required for headless mode

Potential issue: If the Godot binary isn't installed in the dev environment, headless tests can't run. This should be a documented setup prerequisite.

When do we need Godot tests?

Honestly? Not urgently. The client is a "dumb renderer" (D-020). For v0.1:

  • Input mapping (key → semantic action) — simple enough to verify manually
  • Snapshot rendering (ObserverSnapshot → sprites) — visual verification, hard to unit test meaningfully
  • UI elements — not complex enough to warrant test infrastructure yet

Recommendation: Set up GUT in Sprint 1 (#205) but write minimal tests initially. The Rust side is where testing pays off. Godot tests become valuable when we have:

  • Complex UI state (dialogue system #174)
  • Input simulation for playtest automation (#207)
  • Rendering verification for fog overlay correctness (#208)

4.3 Integration Testing: Real IPC, Not Mocks

Recommendation: Test against the real subprocess/IPC bridge.

The subprocess/IPC architecture (D-020) means the simulation is a standalone binary. Integration tests should exercise the actual protocol:

Test runner (Rust or shell script)
  → Spawns simulation binary as subprocess
  → Connects via IPC (local socket)
  → Sends PlayerInput messages (MessagePack)
  → Receives ObserverSnapshot responses
  → Asserts on snapshot contents
  → Kills subprocess on teardown

Why real IPC, not mocks?

  1. The IPC IS the production path. Mocking it tests mock behavior. We need to know the real serialization/deserialization works, the real socket connection works, the real subprocess lifecycle works.

  2. The simulation binary already exists as a test target. We're building a standalone binary (#65, #66, #67). Running it in a test is literally what #81 (E2E connection test) describes. These aren't extra test infrastructure — they're the actual integration stories.

  3. Mock maintenance cost. A mock IPC would need to be updated every time the protocol changes. The real binary is always in sync with itself.

When mocks ARE appropriate:

  • Client-side unit tests that test GDScript logic without needing a running simulation. The client can have a MockBridge that returns hardcoded snapshots for testing UI rendering and input handling in isolation.
  • Protocol format tests that verify MessagePack serialization independently of the full simulation.

Integration test structure:

test/
  integration/
    test_connection.rs       # Spawn binary, connect, verify handshake
    test_movement.rs         # Send move input, verify position change in snapshot
    test_perception.rs       # Verify observer filtering (entity behind wall not in snapshot)
    test_npc_routine.rs      # Advance time, verify NPC position changes
  fixtures/
    test_map_small.bin       # Pre-generated 10x10 map for fast tests
    test_npcs_minimal.json   # 3 NPCs with known configurations

4.4 Test Output Format for Agent Consumption

Recommendation: cargo-nextest for Rust, GUT console for Godot.

Rust: cargo-nextest

Standard cargo test output is human-readable but not structured. cargo-nextest provides:

  • JUnit XML output — universally parseable, works with any CI system
  • Per-test timing — identifies slow tests
  • Parallel execution — faster test runs on multi-core
  • Retry support — flaky test detection
  • Structured JSON output — ideal for agent consumption
cargo nextest run --message-format libtest-json 2>&1
# or for JUnit XML:
cargo nextest run --profile ci  # with .config/nextest.toml configured for junit output

For Claude Code agent consumption, the key format is: test name, pass/fail, duration, failure message if any. cargo-nextest's default output provides all of this clearly.

If nextest is too heavy for initial setup, standard cargo test with --format json (nightly) or even default output works. The test names and PASS/FAIL lines are parseable enough.

Godot: GUT console output

GUT outputs test results to Godot's console output. In headless mode, this goes to stdout. Format:

[PASS] test_snapshot_renders_entities
[FAIL] test_fog_overlay_covers_unseen - Expected fog at (3,4) but found visible

Simple, parseable, sufficient.

Summary format recommendation:

test/run-rust     → cargo nextest run (or cargo test)   → stdout + optional JUnit XML
test/run-godot    → godot --headless + GUT              → stdout
test/run-ipc      → custom integration runner           → stdout + exit code
test/run-all      → runs all three sequentially         → combined stdout

4.5 Test Pollution Boundaries — Where's the Line?

Core rule: No test-only code paths in production code that change behavior.

This means:

  • NO #[cfg(test)] blocks in production modules that alter execution paths
  • NO mock injection points or "if testing" branches
  • NO making fields pub just so tests can access them
  • NO pub(crate) solely for test visibility

What IS acceptable:

  1. #[cfg(test)] mod tests at the bottom of a source file. This is standard Rust practice. The test module lives in the same file but compiles away in release builds. No production code is changed.

  2. Public API boundaries as test seams. The SimulationBuilder pattern gives tests clean construction without test-specific hooks:

    // This is the production API, not a test hack
    pub struct SimulationBuilder {
        map_config: MapConfig,
        npc_configs: Vec<NpcConfig>,
    }
    

    Tests use the same builder that the real game uses, just with different configs (small map, few NPCs).

  3. Trait-based abstraction for external dependencies. The SimBridge trait (D-020) is a production abstraction — it exists because we need LocalBridge and NetworkBridge. Tests can create a TestBridge that records inputs and replays snapshots. This isn't test pollution; it's the natural consequence of good architecture.

  4. ECS component queries as natural seams. bevy_ecs is inherently testable — create a World, add components, run systems, query results. No mocking framework needed. No test hooks in production code. The ECS architecture IS the test infrastructure.

  5. Deterministic simulation with controlled inputs. If the simulation is deterministic (D-010 principle 4), then feeding the same inputs produces the same outputs. Tests set up initial state, feed known inputs, assert on outputs. No randomness to mock out.

Where bevy_ecs makes this easy:

#[test]
fn npc_follows_routine() {
    let mut world = World::new();

    // Set up test state directly — no mock injection needed
    let npc = world.spawn((
        Position(IVec2::new(5, 5)),
        DailyRoutine { /* morning: go to work at (10, 10) */ },
        GameTime { hour: 8, minute: 0 },
    )).id();

    // Run the routine execution system
    let mut schedule = Schedule::default();
    schedule.add_systems(routine_execution_system);
    schedule.run(&mut world);

    // Assert NPC is moving toward work
    let pos = world.get::<Position>(npc).unwrap();
    // ... assert path was generated toward (10, 10)
}

No mocking. No dependency injection. No test hooks. Just create the world state you want and run systems on it. This is bevy_ecs's killer feature for testability.

Production code constraints (#217) summary:

  1. No conditional compilation that changes production behavior
  2. No pub visibility escalation for tests
  3. No test-specific parameters on production functions
  4. Public API is the test surface — if something can't be tested through the public API, the API boundary is wrong
  5. ECS World setup is the test fixture — not mock objects

4.6 Test Runners as Bash Commands

Yes. Following the db/connectors/sqlite-* pattern.

test/
  run-rust          # #!/bin/bash — cd simulation && cargo nextest run "$@"
  run-godot         # #!/bin/bash — godot --headless --script ...
  run-ipc           # #!/bin/bash — builds sim binary, runs integration tests
  run-all           # #!/bin/bash — runs all three, reports summary

Each script:

  • Is whitelistable for Claude Code agents
  • Returns exit code 0 on success, non-zero on failure
  • Outputs human-readable AND agent-parseable results to stdout
  • Accepts arguments to filter tests (e.g., test/run-rust -- test_shadowcast)
  • Runs without interactive input (no prompts, no --interactive flags)

Additional utility scripts:

test/
  run-rust-watch    # cargo watch + nextest for TDD loop
  run-coverage      # cargo llvm-cov (if we want coverage metrics)
  run-bench         # cargo bench for performance regression

4.7 Ticket #214 Refinement — Decisions Needed

#214 (Refinement discussion) blocks #215-217. Here are the decisions I'm proposing based on this analysis:

Decision 1: Rust test organization = Hybrid

  • #[cfg(test)] for unit tests inside modules
  • tests/ directory for integration tests
  • Both run via cargo test / cargo nextest run
  • Status: Ready for team confirmation

Decision 2: Godot test framework = GUT

  • Install GUT as addon in client project
  • Headless execution via godot --headless
  • Minimal initial tests, expand with UI complexity
  • Status: Ready for team confirmation

Decision 3: Integration tests = Real IPC

  • Spawn actual simulation binary in tests
  • Test the real MessagePack protocol over real sockets
  • Mock bridge only for client-side unit tests
  • Status: Ready for team confirmation

Decision 4: Production code constraints

  • No test-only code in production modules
  • Public API is the test surface
  • ECS World setup replaces mock injection
  • Trait boundaries (SimBridge) are natural test seams
  • Status: Ready for team confirmation

Decision 5: Test runner tooling

  • cargo-nextest for Rust (install as dev dependency)
  • GUT for Godot
  • Bash wrapper scripts in test/ directory
  • Status: Ready for team confirmation

Decision 6: Test output format

  • Rust: nextest default output + optional JUnit XML
  • Godot: GUT console output
  • Integration: stdout + exit code
  • Status: Ready for team confirmation

Still needs discussion:

  • Deterministic replay system (#201): How do we record and replay input sequences? This needs a design session. It's high value for regression testing but not a blocker for initial test infrastructure.
  • Performance benchmarks (#204): What are our performance baselines? Need to define: tick budget (ms per tick), entity count targets, serialization throughput. Not urgent for v0.1 with 25 NPCs.
  • Flaky test policy: How do we handle non-deterministic test failures? With deterministic simulation, there shouldn't be many. But integration tests involving subprocess lifecycle could be flaky. Policy: flaky tests are bugs, not tolerated.

Cross-Track Dependencies

Track 3 Gap Track 4 Impact
Collision system Needs unit tests for tile walkability checks
Pathfinding Needs unit tests for A* algorithm + integration tests for NPC routing
Time system Needs unit tests for tick-time conversion + integration tests for routine triggering
Interaction dispatcher Needs integration tests for interact → dialogue flow
Save/load Needs integration tests for serialize → deserialize roundtrip

Priority Summary

Immediate v0.1 blockers requiring new tickets:

  1. Tile collision system — nothing moves correctly without this
  2. Pathfinding + NPC movement — NPCs can't follow routines without this
  3. Time system (promote Q-009 / #25) — routines depend on knowing what time it is
  4. Interaction dispatcher — player can't talk to NPCs without this

Soft blockers needing attention:

  1. Audio manager — sound events need a client-side player
  2. Placeholder art spec — #133 needs specifics before implementation
  3. Save/load — required for any real play session, shares design with #96

Testability priorities:

  1. Confirm the 6 decisions above — unblocks #215, #216, #217
  2. Set up cargo nextest and test/run- scripts* — first developer experience
  3. Write first integration test alongside #81 (E2E connection test) — test infra grows with features

That's my technical assessment. The good news: none of these gaps are architecturally surprising. They're all "systems we assumed existed but nobody ticketed." The collision + pathfinding + time system trio is the most critical — nothing works without them. The testability decisions are clear-cut; I'm confident in the hybrid approach.

— TYRE