Phase 1: extend sitl_observer with cursor-based `wait_for_outbound` returning `OutboundMessage` from `outbound_messages_<fc_kind>_<host>.json` fixtures. Three outcomes: message, TimeoutError (null entries), or RuntimeError (missing/malformed). Fix FT-P-01 + FT-P-05 scenarios to use `fc_kind=` kwarg. Phase 2: FT-P-01 vertical-slice fixture builder under `e2e/fixtures/sitl_replay_builder/`. Reuses the production `gps-denied-replay` CLI + `ReplayInputAdapter`: encode 60 stills as 1 fps MP4 + synthetic stationary tlog (pymavlink); run replay; project FDR outbound estimates into the schema. Avoids the 13+ cp of SUT-side frame-ingestion that a live-SITL-capture path would have required. Live execution remains a manual operator step. +35 unit tests (664 total, up from 637). K=3 cumulative review for b76-b78 documents the offline-replay arc convergence. Co-authored-by: Cursor <cursoragent@cursor.com>
6.3 KiB
Batch 78 Report — FT-P-01 vertical slice (cycle 1, batch 12 of test phase)
Batch: 78
Date: 2026-05-17
Context: Test implementation (greenfield Step 10 — Implement Tests)
Tasks: AZ-598 (5 cp) — 1 task (FT-P-01 vertical slice)
Cycle: 1
Verdict: COMPLETE — PASS (self-reviewed + cumulative-reviewed; see reviews/batch_78_review.md + reviews/cumulative_76_78_review.md)
Summary
Two distinct concerns shipped under one ticket because they unblock each other:
- Observer extension —
sitl_observer._FdrReplayObserver.wait_for_outboundwas missing despite being called by FT-P-01 + FT-P-05. The b78 audit caught this; the implementation adds cursor-based replay fromoutbound_messages_<fc_kind>_<host>.jsonplus anOutboundMessagedataclass + the two scenario kwarg fixes (fc_adapter=→fc_kind=). - FT-P-01 fixture builder — a vertical-slice tool that produces
the two fixture files (
outbound_messages_*+observer_*) for the FT-P-01 scenario. Pivoted from the original "live SITL docker capture" design (would have needed ~13+ cp of new SUT-side frame-ingestion code) to a "drivegps-denied-replayagainst a 1 fps MP4 + synthetic stationary tlog" approach that reuses the existing productionReplayInputAdapter. No new SUT code; one subprocess call instead of a multi-container compose.
Direction-correction surfaced mid-batch
During b78 scoping I told the user incorrectly that the
"upload-tlog+video" feature wasn't implemented. Discovery during
scope analysis showed src/gps_denied_onboard/replay_input/ exists
exactly for that use case (CLI = gps-denied-replay, coordinator
= ReplayInputAdapter, auto-sync = AZ-405). I corrected the user
immediately, surfaced the direction options, and the user chose to
stay the course on b78 (FT-P-01 vertical slice). The discovery also
enabled the pivot from live-SITL-capture to "reuse the
gps-denied-replay CLI" — turning the impossible-in-one-batch
phase 2 into a tractable one.
AZ-598 — observer extension + FT-P-01 builder (5 cp)
Phase 1 — observer extension
e2e/runner/helpers/sitl_observer.py(extended):- New
OutboundMessage(lat_deg, lon_deg, image_id=None)frozen dataclass. _FdrReplayObserverunfrozen (cursor state is now meaningful);_outbound_cursor: int = 0+ lazy_outbound_messagescache.- New
wait_for_outbound(timeout_s: float | None = None)method with three outcomes:OutboundMessage/TimeoutError/RuntimeError. - New module-level
_load_outbound_messages(fc_kind, host)helper that validates the entiremessageslist at first read.
- New
e2e/tests/positive/test_ft_p_01_still_image_accuracy.py:get_observer(fc_adapter=...)→get_observer(fc_kind=...).e2e/tests/positive/test_ft_p_05_sat_anchor.py: same kwarg fix.e2e/_unit_tests/helpers/test_sitl_observer.py: +11 tests covering cursor advance, timeout, exhaustion, missing file, missing env, malformed schema (list/object/keys), optionalimage_id, and cursor independence between observers.
Phase 2 — FT-P-01 fixture builder
e2e/fixtures/sitl_replay_builder/__init__.py(new): minimal package docstring; deliberately no symbol re-exports (avoid thebuild_p01_fixturesfunction/submodule name-shadow pitfall — documented in the docstring).e2e/fixtures/sitl_replay_builder/build_p01_fixtures.py(new):BuilderConfigfrozen dataclass.encode_stills_to_mp4(image_paths, output, fps=1.0)— OpenCV; accepts_video_writer_factory/_imreadfor testability.generate_stationary_tlog(output, duration_s=120, hz=200)— pymavlink; writes zero-motionRAW_IMU+ATTITUDEpairs.run_gps_denied_replay(video, tlog, fdr_out, time_offset_ms=0, extra_args=...)— subprocess to the production CLI; bypasses auto-sync because the synthetic tlog has no take-off.parse_fdr_for_outbound_estimates(fdr_path, fdr_kind=..., lat_key=..., lon_key=...)— JSONL walk; configurable record-kind + field-key projection.write_outbound_messages_fixture(output, image_ids, estimates)— schema writer; preservesNone→ JSONnullfor timeouts.write_observer_fixture(output)— minimal observer config soget_observersucceeds.build_p01_fixtures(cfg, *, _runner=None, ...)— orchestrator._main(argv=None) -> int— argparse CLI entry point.
e2e/fixtures/sitl_replay_builder/README.md(new): strategy, usage, output structure, limitations.e2e/_unit_tests/fixtures/test_sitl_replay_builder.py(new): +24 tests — 3 forencode_stills_to_mp4, 4 forgenerate_stationary_tlog(incl. one real-pymavlink round-trip), 3 forrun_gps_denied_replay, 6 forparse_fdr_for_outbound_estimates, 3 forwrite_outbound_messages_fixture, 1 forwrite_observer_fixture, 4 for end-to-end orchestration.e2e/_unit_tests/test_directory_layout.py: registers the three new files in the layout invariant.
Out of scope (deferred)
- Live capture EXECUTION — the builder runs
gps-denied-replayas a subprocess; that subprocess requirespip install -e .at repo root plus access to the input images. Not executed in this batch; documented as a manual operator step. A future ticket can add a CI job that runs the live capture + commits the resulting fixtures. - Other scenarios (FT-P-02 through FT-N-04) — each needs its own fixture-builder flow (continuous video + IMU CSV replay, blackout/spoof setup, etc.).
- iNav adapter — only ArduPilot supported in this batch.
fc_kind↔fc_adapternaming convergence — kwarg-fix only; a future cleanup ticket should converge the vocabulary.
Test Results
- New unit tests: 35 (11
wait_for_outbound+ 24 builder). - Full
e2e/_unit_testssuite: 664 passed in 137 s (previous cumulative: 637 → +27 net). - No new linter errors.
grep raise NotImplementedErrorundere2e/tests/returns zero matches (b77 invariant preserved).
State
- Spec moved:
_docs/02_tasks/todo/AZ-598_ft_p_01_vertical_slice.md→_docs/02_tasks/done/. _docs/_autodev_state.mdadvanced tolast_completed_batch: 78,last_cumulative_review: batches_76-78(K=3 cumulative shipped alongside the batch review).