# FakeFdrSink for Component-Level Tests **Task**: AZ-275_fake_fdr_sink **Name**: FakeFdrSink **Description**: An in-process, in-memory test double for `FdrClient` that conforms to the `fdr_client_protocol` contract's public surface and lets component-level tests assert on every record their code emits to the FDR. Drop-in replacement for `FdrClient` everywhere it is injected; no writer thread, no segment files, no real ring buffer — just a list-of-records the test inspects. **Complexity**: 2 points **Dependencies**: AZ-272_fdr_record_schema, AZ-273_fdr_client_ringbuf **Component**: shared.fdr_client (cross-cutting; epic AZ-247 / E-CC-FDR-CLIENT) **Tracker**: AZ-275 **Epic**: AZ-247 (E-CC-FDR-CLIENT) ### Document Dependencies - `_docs/02_document/contracts/shared_fdr_client/fdr_client_protocol.md` — the public surface this fake conforms to. - `_docs/02_document/contracts/shared_fdr_client/fdr_record_schema.md` — the record envelope this fake stores in memory. ## Problem Component-level tests (every component under `tests/unit/components//` and `tests/integration//`) must assert on what their code writes to the FDR. Without a fake: - Tests would have to spin up the C13 writer thread + a tmp segment file just to read records back — slow, brittle, cross-component coupling. - Tests would all reach into `FdrClient`'s private buffer state, freezing internal layout into every test and blocking future implementation changes. A simple, contract-conforming `FakeFdrSink` lets each component's test assert on records via a stable public API — and crucially, the same API every other component test uses, so test infrastructure does not fork per component. ## Outcome - Tests inject `FakeFdrSink(producer_id="c1_vio")` wherever production code expects an `FdrClient`. The component code is unchanged; the test reads `sink.records` after exercising the component. - Every assertion the contract test of `fdr_client_protocol` makes against a real `FdrClient` ALSO holds against `FakeFdrSink` — except the lock-free / allocation-free / SPSC-guard NFRs (those are real-buffer concerns and are explicitly out of scope for the fake). - Tests can opt in to drop-oldest semantics (`FakeFdrSink(capacity=N, with_default_overrun_policy=True)`) when verifying overrun behaviour, or leave it disabled and rely on unbounded list mode for general assertions. ## Scope ### Included - `FakeFdrSink(producer_id: str, capacity: int | None = None, with_default_overrun_policy: bool = False)` constructor implementing the `FdrClient` public surface from `fdr_client_protocol.md`: - `enqueue`, `pop_one`, `drain`, `flush`, `producer_id`, `on_overrun` getter/setter. - An `FakeFdrSink.records: list[FdrRecord]` property returning the records currently in-buffer in FIFO order. Tests use this directly for assertions. - An `FakeFdrSink.all_records_ever: list[FdrRecord]` property returning every record ever enqueued, INCLUDING records dropped by the overrun policy when it is active. Lets tests assert on what the producer TRIED to send vs. what the buffer KEPT. - Behaviour parity with `FdrClient` for the contract-relevant subset: - Returns `EnqueueResult.OVERRUN` when `capacity` is set and the buffer is full. - Invokes `on_overrun` exactly once per overrun event when wired. - Stamps `producer_id` correctly per the protocol (does NOT mutate `record.producer_id`). - A pytest fixture (`fake_fdr_sink`) under `tests/conftest.py` that constructs a default-configuration sink and yields it to tests. ### Excluded - The lock-free SPSC ring buffer, allocation-free hot path, and SPSC guards — owned by AZ-273 (this is a fake; real concurrency primitives are explicitly NOT replicated). - The drop-oldest closure itself — owned by AZ-274; the fake imports and reuses it when the user opts in via `with_default_overrun_policy=True`. - The `FdrRecord` schema — owned by AZ-272. - The C13 writer thread, segment files, etc. — owned by E-C13 (AZ-248). - A "fake C13 writer" that drains the sink — out of scope. Tests that need the drained side use `pop_one` / `drain` directly on the fake. ## Acceptance Criteria **AC-1: Drop-in for FdrClient public surface** Given any production code that takes an `FdrClient` parameter (e.g. `Vio(fdr=fdr_client, ...)`) When the test passes a `FakeFdrSink` instead Then the production code's calls (`enqueue`, `flush`) work identically; no AttributeError, no signature mismatch **AC-2: records reflects in-buffer state** Given a `FakeFdrSink` with no capacity limit When the producer enqueues 3 records, then the test calls `pop_one()` once Then `sink.records` returns the 2 remaining records in FIFO order **AC-3: all_records_ever captures dropped records** Given a `FakeFdrSink(capacity=2, with_default_overrun_policy=True)` filled to capacity When the producer enqueues a 3rd record (drop-oldest fires) Then `sink.records` has 2 entries (newest 2) AND `sink.all_records_ever` has 3 entries (all of them, including the dropped one) **AC-4: Overrun policy parity with real FdrClient** Given a `FakeFdrSink(capacity=4, with_default_overrun_policy=True)` When the test reproduces AC-1 from AZ-274 (overflow + canonical overrun record) Then the same assertion that holds against real `FdrClient` holds against `FakeFdrSink` — same overrun record shape, same coalescing across bursts **AC-5: pytest fixture available** Given a test file imports the standard project conftest When the test signature is `def test_x(fake_fdr_sink): ...` Then pytest injects a default-configuration `FakeFdrSink` and yields it; teardown clears the sink **AC-6: producer_id is preserved** Given `FakeFdrSink(producer_id="c2_vpr")` and an enqueued record carrying `producer_id="c2_vpr"` When the test inspects `sink.records[0]` Then `records[0].producer_id == "c2_vpr"` (the fake does NOT rewrite producer_id) ## Non-Functional Requirements **Performance** - `enqueue` p99 ≤ 100 µs on Tier-2 (developer machines + CI). The fake is not in the production critical path; the budget exists only to keep tests fast (10k assertions in a long fixture should add < 1 s). **Reliability** - The fake is single-threaded only. Concurrent `enqueue` / `pop_one` is undefined behaviour and not tested. Documented in the docstring. **Compatibility** - The fake's public surface mirrors the `fdr_client_protocol.md` contract version it conforms to. The fake's docstring records the contract version. Bumping the protocol contract major version requires bumping the fake's surface in lock-step. ## Unit Tests | AC Ref | What to Test | Required Outcome | |--------|-------------|-----------------| | AC-1 | Inject `FakeFdrSink` into a stub component that expects `FdrClient` | No AttributeError; calls succeed | | AC-2 | 3 enqueues + 1 pop on unbounded sink | `len(sink.records) == 2` in FIFO order | | AC-3 | Capacity-2 sink with overrun policy + 3 enqueues | `len(sink.records) == 2`, `len(sink.all_records_ever) == 3` | | AC-4 | Re-run AZ-274 AC-1 + AC-2 against the fake | Same overrun record shape; same coalescing | | AC-5 | A trivial test using `fake_fdr_sink` fixture | Fixture provides a clean sink per test | | AC-6 | Construct sink + enqueue with explicit producer_id | producer_id preserved on the popped record | ## Constraints - Public surface is fixed by `fdr_client_protocol.md` v1.0.0. The fake is allowed to expose ADDITIONAL test-only attributes (`records`, `all_records_ever`) — these are documented as fake-only and never appear on the real `FdrClient` (so production code accidentally using them fails the type checker). - The fake lives at `src/gps_denied_onboard/fdr_client/fakes.py` — a separate module from the production code so production imports never pick it up. Tests import `from gps_denied_onboard.fdr_client.fakes import FakeFdrSink`. - The fake reuses `default_overrun_policy` from AZ-274 verbatim; it does NOT re-implement the policy. ## Risks & Mitigation **Risk 1: Fake drift from real client** - *Risk*: Engineers add a method to `FdrClient` and forget to mirror it on `FakeFdrSink`; tests pass against the fake but production fails. - *Mitigation*: A contract test (`tests/contract/fdr_client_fake_parity.py`) iterates over every public method on `FdrClient` and asserts the same method exists on `FakeFdrSink` with a compatible signature. Failure mode is loud and immediate. **Risk 2: Tests reach into `_records` private state, freezing implementation** - *Risk*: A test does `sink._buffer[3]` instead of `sink.records[3]`; later refactor breaks the test. - *Mitigation*: `records` and `all_records_ever` are the documented public access; pyright/mypy mark `_buffer` as private with `_` prefix; code review catches private-state access. ## Runtime Completeness - **Named capability**: `FakeFdrSink` test double — it is NOT a runtime capability; it is test infrastructure. Production code MUST NOT import from `fakes.py` (verified by import-linter rule in the project's `pyproject.toml`). - **Production code that must exist**: import-linter rule preventing `src/gps_denied_onboard/**/*.py` (excluding `tests/`) from importing `gps_denied_onboard.fdr_client.fakes`. Otherwise none — this PBI's deliverable is test infrastructure. - **Allowed external stubs**: this IS the stub. It is allowed in tests only. - **Unacceptable substitutes**: production code wiring `FakeFdrSink` instead of `FdrClient` (would silently disable real FDR writes); per-test ad-hoc fakes that drift from the contract.