# Scenario stub cleanup (replay_mode helpers + 13 rewires) **Task**: AZ-597_scenario_stub_cleanup **Name**: Add `runner/helpers/replay_mode.py` + rewire 13 scenarios off local `_resolve_*` / `_drive_*` / `_push_*` stubs **Description**: After AZ-594/595/596 landed the three core harness helpers, sitl_observer, and the fc_proxy_runtime driver, the last unimplemented layer is a grab-bag of local per-scenario stubs that all share the same FDR-replay no-op pattern. Bundle them into one shared `runner/helpers/replay_mode.py` module so the offline FDR-replay path is end-to-end executable once the SITL replay fixture builder lands. **Complexity**: 3 points **Dependencies**: AZ-594, AZ-595, AZ-596 **Component**: Blackbox Tests / Test Infrastructure (epic AZ-262) **Tracker**: AZ-597 **Epic**: AZ-262 (E-BBT) ## Problem Despite the AZ-594/595/596 arc, 13 scenarios still carry local `_resolve_*` / `_drive_*` / `_push_*` `NotImplementedError` stubs: | Stub | Scenarios | |------|-----------| | `_resolve_frame_sink()` | 13 (FT-P-01/02/04/05/07/08/09-AP/09-iNav/10/11, FT-N-01/02/03/04) | | `_resolve_fc_inbound_emitter(fc_adapter[, host])` | 3 (FT-P-02/04/10) | | `_drive_imu_replay(csv_path)` | 2 (FT-P-07, FT-N-02) | | `_resolve_frame_period_ms()` | 2 (FT-N-03/04) | | `_resolve_outage_injection_frames()` | 1 (FT-N-03) | | `_resolve_gt_per_frame(report)` | 1 (FT-N-01) | | `_push_single_image_and_observe(...)` | 1 (FT-P-03/14) | These are unreachable today (the b75 `sitl_replay_ready` gate skips before they're called) so this cleanup can land safely under the unit-test regression gate. The value: once the SITL replay fixture builder ships, scenarios become runnable with no further per-scenario edits. ## Surfaces (`runner/helpers/replay_mode.py`) * `NullFrameSink` — implements `FrameSink` protocol. `write_frame` is a counter; exposes `frames_written: int`. * `NullFcInboundEmitter` — implements `FcInboundEmitter` protocol. `emit` is a counter; exposes `samples_emitted: int`. * `DEFAULT_FRAME_PERIOD_MS = 33` + `default_frame_period_ms() -> int`. * `load_replay_json(filename: str) -> dict | list` — reads `${E2E_SITL_REPLAY_DIR}/`. Raises `FileNotFoundError` when env var unset OR file missing; `ValueError` with file pointer on malformed JSON. * `resolve_replay_subdir(name: str) -> Path` — returns `${E2E_SITL_REPLAY_DIR}//`. Raises `FileNotFoundError` when env var unset OR directory missing. * `imu_replay_noop(csv_path: Path) -> None` — no-op stand-in for the per-scenario `_drive_imu_replay` (IMU is pre-baked into the FDR archive in replay mode; the CSV path is preserved as a parameter for diagnostic logging only). ## Per-scenario rewire pattern ```python # Before: def _resolve_frame_sink(): raise NotImplementedError(...) # After: from runner.helpers.replay_mode import NullFrameSink def _resolve_frame_sink(): return NullFrameSink() ``` Same shape for the other six helpers. For the two scenarios that need scenario-specific JSON (`_resolve_gt_per_frame`, `_push_single_image_and_observe`), they call `load_replay_json("gt_per_frame.json")` / `load_replay_json("single_image_observation.json")` and project the result into their scenario-local dataclass. ## Acceptance Criteria **AC-1**: `NullFrameSink.write_frame` and `NullFcInboundEmitter.emit` are pure counters. **AC-2**: `load_replay_json` raises `FileNotFoundError` (env unset or file missing) and `ValueError` (malformed JSON with file pointer). **AC-3**: `resolve_replay_subdir` raises `FileNotFoundError` (env unset or subdir missing). **AC-4**: `default_frame_period_ms()` returns 33. **AC-5**: All 13 scenarios have local `_resolve_*` / `_drive_*` / `_push_*` stubs deleted and import from `runner.helpers.replay_mode`. **AC-6**: ≥6 unit tests on `replay_mode.py`. **AC-7**: Full e2e unit-test suite passes (regression gate). ## Out of Scope * The actual SITL replay fixture builder. * Live MAVLink router / pymavlink plumbing. ## Files Touched * `e2e/runner/helpers/replay_mode.py` (new) * `e2e/_unit_tests/helpers/test_replay_mode.py` (new) * `e2e/_unit_tests/test_directory_layout.py` (register new module) * 13 scenario files under `e2e/tests/{positive,negative}/`