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