[AZ-598] Batch 78: sitl_observer.wait_for_outbound + FT-P-01 fixture builder

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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-17 12:08:02 +03:00
parent f49d803252
commit 47ad43f913
14 changed files with 1940 additions and 8 deletions
@@ -0,0 +1,117 @@
# FT-P-01 vertical slice: observer wait_for_outbound + SITL capture builder
**Task**: AZ-598_ft_p_01_vertical_slice
**Complexity**: 5 points
**Dependencies**: AZ-594, AZ-595, AZ-596, AZ-597
**Component**: Blackbox Tests / Test Infrastructure (epic AZ-262)
**Tracker**: AZ-598
**Epic**: AZ-262 (E-BBT)
## Problem
Batch 78 was scoped as "build the SITL replay fixture builder, vertical
slice for FT-P-01". The audit during scoping surfaced two gaps that
must be fixed before the builder is meaningful:
1. FT-P-01 + FT-P-05 call `observer.wait_for_outbound(timeout_s=...)`
but `wait_for_outbound` does not exist on `_FdrReplayObserver`.
Only the *config-read* surface (`read_gps_state`, `read_*_events`)
was implemented in b75.
2. FT-P-01 + FT-P-05 call `get_observer(fc_adapter=..., host=...)`
but the signature is `get_observer(fc_kind, host)`. Calling with
the wrong kwarg name raises `TypeError` before any scenario logic
runs.
Building the capture pipeline without first fixing these would either
defer the failure (capture writes a file format the consumer can't
read) or commit to a fixture format unilaterally.
## Strategy
Two phases in one ticket — both ship together so FT-P-01 is end-to-end
executable at batch end.
### Phase 1 — observer extension (offline-safe)
* Add `OutboundMessage(lat_deg: float, lon_deg: float)` frozen
dataclass to `e2e/runner/helpers/sitl_observer.py`.
* Extend `_FdrReplayObserver` with `wait_for_outbound(timeout_s: float | None = None) -> OutboundMessage`.
* Replay-mode semantics: cursor-based read from
`${E2E_SITL_REPLAY_DIR}/outbound_messages_<fc_kind>_<host>.json`.
* Each call advances the cursor by one entry.
* `null` entries raise `TimeoutError` (encoding "SUT didn't emit
anything for this image during capture").
* Cursor past list length raises `RuntimeError`.
* `timeout_s` accepted for live-mode parity; ignored in replay.
* Fix call sites: `get_observer(fc_adapter=...)``get_observer(fc_kind=...)`.
### Phase 2 — SITL capture builder (FT-P-01)
* New `e2e/fixtures/sitl_replay_builder/build_p01_fixtures.py`:
* Stand up the existing `e2e/docker/docker-compose.test.yml` stack.
* For each `AD0000NN.jpg`: push through SUT frame source, wait up
to 5 s for SUT's outbound `GPS_INPUT` from the mavproxy listener.
* Persist `outbound_messages_<fc_kind>_<host>.json` and
`observer_<fc_kind>_<host>.json` to `--output` directory.
* Optional docker compose override `docker-compose.sitl-builder.yml`.
* Unit tests with mocked docker / mavproxy layer.
## Fixture Format (`outbound_messages_<fc_kind>_<host>.json`)
```json
{
"messages": [
{"image_id": "AD000001.jpg", "lat_deg": 48.275292, "lon_deg": 37.385220},
null,
{"image_id": "AD000003.jpg", "lat_deg": 48.275001, "lon_deg": 37.382922}
]
}
```
* `image_id` is optional metadata (diagnostics only).
* `null` = timeout (no message captured for this image).
* Entries map 1:1 to scenario `wait_for_outbound` calls in order.
## Acceptance Criteria
**AC-1**: `wait_for_outbound()` returns `OutboundMessage(lat_deg, lon_deg)`
from the cursor entry.
**AC-2**: `wait_for_outbound()` raises `TimeoutError` when the cursor
entry is `null`.
**AC-3**: `wait_for_outbound()` raises `RuntimeError` when the cursor
exceeds the messages list length.
**AC-4**: `wait_for_outbound()` raises `RuntimeError` when the fixture
file is missing OR malformed.
**AC-5**: FT-P-01 + FT-P-05 use `get_observer(fc_kind=fc_adapter, ...)`.
**AC-6**: `build_p01_fixtures.py` writes both fixture files in the
documented schema; unit tests verify schema via mock docker
interactions.
**AC-7**: Full e2e unit-test suite passes (regression gate).
FT-P-01 + FT-P-05 still skip via `sitl_replay_ready` when env unset.
## Out of Scope
* Live capture EXECUTION — will ask user approval before running
(docker-heavy).
* Other scenarios (FT-P-02 through FT-N-04).
* iNav adapter for capture — AP first.
## Files Touched
* `e2e/runner/helpers/sitl_observer.py` (extend)
* `e2e/_unit_tests/helpers/test_sitl_observer.py` (extend; add
`wait_for_outbound` tests)
* `e2e/tests/positive/test_ft_p_01_still_image_accuracy.py` (kwarg fix)
* `e2e/tests/positive/test_ft_p_05_sat_anchor.py` (kwarg fix)
* `e2e/fixtures/sitl_replay_builder/__init__.py` (new)
* `e2e/fixtures/sitl_replay_builder/build_p01_fixtures.py` (new)
* `e2e/fixtures/sitl_replay_builder/README.md` (new)
* `e2e/_unit_tests/fixtures/test_sitl_replay_builder.py` (new)
* `e2e/_unit_tests/test_directory_layout.py` (register new paths)
* `e2e/docker/docker-compose.sitl-builder.yml` (new, if needed)