# Batch 77 Report — replay_mode helpers + 13 scenario stub rewires (cycle 1, batch 11 of test phase) **Batch**: 77 **Date**: 2026-05-17 **Context**: Test implementation (greenfield Step 10 — Implement Tests) **Tasks**: AZ-597 (3 cp) — 1 task (scenario stub cleanup bundle) **Cycle**: 1 **Verdict**: COMPLETE — PASS (self-reviewed; see `reviews/batch_77_review.md`) ## Summary Closes the offline FDR-replay path that AZ-594 (b74), AZ-595 (b75), and AZ-596 (b76) opened. After those three batches, the only remaining `NotImplementedError` stubs in the scenario suite were a grab-bag of local `_resolve_*` / `_drive_*` / `_push_*` helpers duplicated across 13 scenario files. They all reduced to the same FDR-replay pattern — either a no-op counter (frame sink, FC inbound emitter, IMU replay driver) or a JSON read from `${E2E_SITL_REPLAY_DIR}/` (per-frame GT, single-image observation, outage frames subdir). This batch bundles those into one shared `runner/helpers/replay_mode.py` module + rewires the 13 scenarios off their local stubs. After the batch: * `grep raise NotImplementedError` under `e2e/tests/` returns **zero** matches. * Once the SITL replay fixture builder lands (separate ticket), every scenario becomes runnable end-to-end with no further per-scenario edits. * Unit-test mode is unchanged — the b75 `sitl_replay_ready` skip gate keeps the loaders unreached when `E2E_SITL_REPLAY_DIR` is unset. ### AZ-597 — replay_mode helpers + 13 scenario rewires (3 cp) * **`runner/helpers/replay_mode.py`** (new): * `NullFrameSink` — counter-only `FrameSink` (`frames_written: int`). * `NullFcInboundEmitter` — counter-only `FcInboundEmitter` (`samples_emitted: int`). * `default_frame_period_ms() -> int` + `DEFAULT_FRAME_PERIOD_MS = 33` (30 fps). * `load_replay_json(filename)` — generic JSON loader. Raises `FileNotFoundError` (env-unset / file-missing) or `ValueError` (malformed, file pointer included). * `resolve_replay_subdir(name)` — directory loader. Raises `FileNotFoundError` (env-unset / subdir-missing). * `imu_replay_noop(csv_path)` — explicit no-op; signature mirrors `imu_replay.ImuReplayer.replay` for future live-mode parity. * Single shared `_resolve_replay_root_or_raise(reason)` enforces the `E2E_SITL_REPLAY_DIR` semantics exactly once. * **13 scenarios rewired** (all `_resolve_*` / `_drive_*` / `_push_*` stubs deleted): * `_resolve_frame_sink` → `NullFrameSink()` in: FT-P-01, FT-P-02, FT-P-04, FT-P-05, FT-P-07, FT-P-08, FT-P-09-AP, FT-P-09-iNav, FT-P-10, FT-P-11, FT-N-01, FT-N-02, FT-N-03, FT-N-04. * `_resolve_fc_inbound_emitter` → `NullFcInboundEmitter()` in: FT-P-02, FT-P-04, FT-P-10. * `_drive_imu_replay` → `imu_replay_noop(...)` in: FT-P-07, FT-N-02. * `_resolve_frame_period_ms` → `default_frame_period_ms()` in: FT-N-03, FT-N-04. * `_resolve_outage_injection_frames` → `resolve_replay_subdir("outage_frames")` in: FT-N-03. * `_resolve_gt_per_frame` → `load_replay_json("gt_per_frame.json")` + dataclass projection in: FT-N-01. * `_push_single_image_and_observe` → `load_replay_json("single_image_observation.json")` + tuple projection in: FT-P-03/14. * **`e2e/_unit_tests/test_directory_layout.py`** — registers the new `runner/helpers/replay_mode.py` path. ## Out of scope (deferred) * The actual SITL replay fixture builder (separate ticket — will populate `${E2E_SITL_REPLAY_DIR}/` with `gps_state.json`, `gt_per_frame.json`, `single_image_observation.json`, `outage_frames/`, `ekf_divergence_events.json`, etc.). * Live MAVLink router / pymavlink plumbing (separate live-mode infrastructure ticket). ## Test Results * New unit tests: **17** (2 null-sink, 2 null-emitter, 1 frame-period, 2 imu-replay-noop, 6 load_replay_json, 4 resolve_replay_subdir). * Full `e2e/_unit_tests` suite: **626 passed in 127 s** (previous cumulative: 608 → +18 net = +17 new replay_mode tests + 1 new directory-layout parametrize entry). * No new linter errors. * `grep raise NotImplementedError` under `e2e/tests/` returns **zero** matches. ## State * Spec moved: `_docs/02_tasks/todo/AZ-597_scenario_stub_cleanup.md` → `_docs/02_tasks/done/`. * `_docs/_autodev_state.md` advanced to `last_completed_batch: 77`. * `last_cumulative_review` remains `batches_73-75`; next K=3 cumulative review fires at the end of batch 78.