[AZ-597] Batch 77: replay_mode helpers + 13 scenario stub rewires

Add `runner/helpers/replay_mode.py` (NullFrameSink, NullFcInboundEmitter,
default_frame_period_ms, load_replay_json, resolve_replay_subdir,
imu_replay_noop) and rewire all 13 scenarios off their local
`_resolve_*` / `_drive_*` / `_push_*` NotImplementedError stubs.

Closes the offline FDR-replay execution path. `grep raise
NotImplementedError` under `e2e/tests/` now returns zero matches. +17
unit tests (626 total, up from 608). Unit-test behaviour unchanged
(scenarios still skip via b75 sitl_replay_ready gate when
E2E_SITL_REPLAY_DIR is unset).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-17 09:52:05 +03:00
parent 6554d568f1
commit f49d803252
22 changed files with 798 additions and 85 deletions
@@ -0,0 +1,102 @@
# 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}/<filename>`. 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}/<name>/`. 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}/`