# 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.