Standardized YAML frontmatter on all 16 files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
36 KiB
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:
pathfindingcrate (crates.io) — mature, has A* and Dijkstra, works on arbitrary graphs. ~10 lines to integrate with tile grid. Recommended.- Custom A* — it's a well-known algorithm, easy to implement. But why reinvent it?
- 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):
- 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.
- 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.
- Time display. Player needs to know what time it is (insert HUD element). Diegetic — it's on their neural insert.
- 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.pngor 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:
-
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. -
Custom serialization: Define a
SaveStatestruct 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. -
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:
SaveStatedata 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?
- Main menu (new game / load game / settings / quit)
- New game → character selection (smuggler or detective)
- World generation (station district instantiation, NPC generation, template placement)
- Game starts — player character placed in starting location
What happens when the player stops playing?
- Pause → save and quit
- Quit without saving → warning prompt
- 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:
- Godot 4 binary must be installed and on PATH (or referenced by absolute path)
- The project must be initialized (
.godot/directory with imported resources) - First run may need
godot --headless --importto build the import cache - 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?
-
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.
-
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.
-
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
MockBridgethat 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
pubjust so tests can access them - NO
pub(crate)solely for test visibility
What IS acceptable:
-
#[cfg(test)] mod testsat 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. -
Public API boundaries as test seams. The
SimulationBuilderpattern 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).
-
Trait-based abstraction for external dependencies. The
SimBridgetrait (D-020) is a production abstraction — it exists because we need LocalBridge and NetworkBridge. Tests can create aTestBridgethat records inputs and replays snapshots. This isn't test pollution; it's the natural consequence of good architecture. -
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.
-
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:
- No conditional compilation that changes production behavior
- No
pubvisibility escalation for tests - No test-specific parameters on production functions
- Public API is the test surface — if something can't be tested through the public API, the API boundary is wrong
- 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
--interactiveflags)
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 modulestests/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-nextestfor 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:
- Tile collision system — nothing moves correctly without this
- Pathfinding + NPC movement — NPCs can't follow routines without this
- Time system (promote Q-009 / #25) — routines depend on knowing what time it is
- Interaction dispatcher — player can't talk to NPCs without this
Soft blockers needing attention:
- Audio manager — sound events need a client-side player
- Placeholder art spec — #133 needs specifics before implementation
- Save/load — required for any real play session, shares design with #96
Testability priorities:
- Confirm the 6 decisions above — unblocks #215, #216, #217
- Set up
cargo nextestand test/run- scripts* — first developer experience - 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