[AZ-596] Batch 76: fc_proxy_runtime driver (FDR-replay mode)

Add `runner/helpers/fc_proxy_runtime.py` wrapping the existing
`BlackoutSpoofProxy` (AZ-406) with a scenario-facing `drive_fc_proxy`
entry point. FDR-replay mode only: loads `schedule.json`, optionally
activates the proxy against a caller clock for alignment verification,
and writes a `proxy_drive_report.json` audit record into
`${E2E_SITL_REPLAY_DIR}` for downstream evaluators.

Replaces the local `_drive_fc_proxy` stub in FT-N-04. Adds 3
@property accessors on `BlackoutSpoofProxy` so the wrapper does not
reach into private attributes. +11 unit tests (608 total, up from
596). Live-mode router wiring remains out of scope (future ticket).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-17 09:08:48 +03:00
parent 43fdef1aac
commit 6554d568f1
9 changed files with 667 additions and 5 deletions
@@ -0,0 +1,91 @@
# Batch 76 Report — fc_proxy_runtime driver (cycle 1, batch 10 of test phase)
**Batch**: 76
**Date**: 2026-05-17
**Context**: Test implementation (greenfield Step 10 — Implement Tests)
**Tasks**: AZ-596 (2 cp) — 1 task (fc_proxy_runtime driver, FDR-replay mode)
**Cycle**: 1
**Verdict**: COMPLETE — PASS (self-reviewed; see `reviews/batch_76_review.md`)
## Summary
Final piece of the harness-stubs arc that started in batch 74. The
FT-N-04 (`test_ft_n_04_blackout_spoof`) scenario called a local
`_drive_fc_proxy` stub that raised
`NotImplementedError("FC-inbound spoof proxy driver is owned by
runner.helpers.fc_proxy_runtime")`. That module didn't exist. The
`BlackoutSpoofProxy` state machine (load schedule, activate, replace
inbound GPS frames inside the window) was already fully implemented
under AZ-406 in `fixtures/injectors/fc_proxy.py` — what was missing
was the scenario-facing wrapper.
This batch adds `runner/helpers/fc_proxy_runtime.py` (one function +
one dataclass) using the same **FDR-replay strategy** as AZ-595:
the runtime driver does not plumb into a live MAVLink router. It
loads the schedule, optionally activates the proxy against a
caller-supplied clock, and writes a small audit JSON
(`proxy_drive_report.json`) into `${E2E_SITL_REPLAY_DIR}` so the
downstream FDR evaluators can correlate. Live-mode driving (real
router + real FC) is explicitly a separate live-mode infrastructure
ticket.
### AZ-596 — fc_proxy_runtime driver (2 cp)
* **`runner/helpers/fc_proxy_runtime.py`** —
`drive_fc_proxy(schedule_path, *, now_ms_provider=None, replay_dir=None)`:
* Loads the schedule via
`BlackoutSpoofProxy.from_schedule_file(schedule_path)`.
* Wraps `json.JSONDecodeError` as `ValueError` with a file pointer
(consistent with the rest of `e2e/runner/helpers/`).
* When `now_ms_provider` is supplied, activates the proxy and
records the resulting `alignment_err_ms`. When absent, sets
`was_replay_mode=True`.
* Resolves the write directory in this order: explicit
`replay_dir` argument > `${E2E_SITL_REPLAY_DIR}` env var > no
write. The chosen directory is created if missing.
* Returns `ProxyDriveReport` (frozen dataclass with
`schedule_path, window_start_ms, window_end_ms,
spoof_frame_count, alignment_err_ms, was_replay_mode`).
* **`fixtures/injectors/fc_proxy.py`** — added three additive
`@property` accessors (`window_start_ms`, `window_end_ms`,
`spoof_frame_count`) so the runtime wrapper does NOT reach into
private attributes. Existing callers unaffected.
* **FT-N-04 scenario** — local `_drive_fc_proxy` stub replaced
with `from runner.helpers.fc_proxy_runtime import drive_fc_proxy;
drive_fc_proxy(schedule_path)`. The scenario's b75
`sitl_replay_ready` skip gate continues to govern when this
code path actually runs.
* **Directory layout test** — registered the new
`runner/helpers/fc_proxy_runtime.py` path.
## Out of scope (deferred)
* **Live MAVLink router + FC inbound transport** — the runtime
driver currently does not wire `proxy.process_inbound_message`
into a real router. A live-mode follow-up ticket will own the
docker-compose-bound MAVLink router that plumbs in the
per-message replace.
* **Other per-scenario `_resolve_*` / `_drive_*` stubs**
(`_resolve_frame_sink`, `_resolve_fc_inbound_emitter`,
`_resolve_outage_injection_frames`, `_resolve_gt_per_frame`,
`_drive_imu_replay`, `_resolve_frame_period_ms`) — each will get
its own follow-up ticket. They remain `NotImplementedError`
stubs in their respective scenario files; the `sitl_replay_ready`
skip gate ensures they're never reached in unit-test mode.
## Test Results
* New unit tests: **11** (3 schedule load/error, 2 activation, 6
replay-dir write).
* Full `e2e/_unit_tests` suite: **608 passed in 124 s** (previous
cumulative: 596 → +12 net = +11 new fc_proxy_runtime tests + 1
new directory-layout parametrize entry).
* No new linter errors.
## State
* Spec moved: `_docs/02_tasks/todo/AZ-596_fc_proxy_runtime.md`
`_docs/02_tasks/done/`.
* `_docs/_autodev_state.md` advanced to `last_completed_batch: 76`.
* `last_cumulative_review` remains `batches_73-75`; next K=3
cumulative review fires at the end of batch 78.