# Test Specification — C5 State Estimator

Component-scoped. Suite-level coverage in `_docs/02_document/tests/*.md`.

## Acceptance Criteria Traceability

| AC ID | Acceptance Criterion (one-line) | Test IDs | Coverage |
|-------|---------------------------------|----------|----------|
| AC-1.3 | Cumulative drift between satellite-anchored fixes | FT-P-02, **C5-IT-01** | Covered |
| AC-1.4 | 95% covariance + source label | FT-P-03, **C5-IT-02** | Covered |
| AC-3.5 | Visual blackout + spoofed-GPS failsafe | FT-N-04, **C5-IT-03** | Covered |
| AC-4.5 (revised) | Internal smoothing of past keyframes (NOT FC retroactive correction) | FT-P-10, **C5-IT-04** | Covered |
| AC-5.2 | On >3 s without estimate, FC IMU-only fallback | NFT-RES-01, **C5-IT-05** | Covered |
| AC-NEW-2 | Spoofing-promotion latency <3 s p95 | NFT-PERF-04, **C5-IT-06** | Covered |
| AC-NEW-8 | Visual blackout + spoof degraded-mode escalation | FT-N-04, NFT-RES-04, **C5-IT-07** | Covered |

---

## Component-Internal Tests

### C5-IT-01: source_label state machine produces correct `last_satellite_anchor_age_ms`

**Summary**: after a satellite-anchored frame, `last_satellite_anchor_age_ms` resets to the frame's age; under visual-propagated frames it monotonically increases.

**Traces to**: AC-1.3 (binning input)

**Description**: scripted sequence — 3 satellite-anchored frames, then 30 s of visual-propagated, then another satellite-anchored. Assert `last_satellite_anchor_age_ms` resets at the anchored events and rises monotonically between them with ms-level resolution.

**Input data**: scripted `EstimatorOutput` sequence.

**Expected result**: monotonic between resets; resets within 100 ms of the anchored frame's `emitted_at`.

**Max execution time**: 60 s.

---

### C5-IT-02: smoothed-current estimate honest covariance

**Summary**: `current_estimate()` produces an SPD 6×6 covariance whose norm reflects the iSAM2 graph's actual posterior — not a fake-confidence value.

**Traces to**: AC-1.4

**Description**: build a synthetic graph where the keyframe-11 absolute factor is intentionally noisy (3× covariance); assert C5's emitted covariance norm is at least 2× the steady-state norm of a clean graph. Repeat with a clean factor; assert ≤1.2× steady-state.

**Input data**: synthetic factor-graph fixtures.

**Expected result**: noisy → ≥2× norm; clean → ≤1.2× norm.

**Max execution time**: 30 s.

---

### C5-IT-03: VIO-only fallback under cross-domain matcher failure

**Summary**: when C4 stops emitting `PoseEstimate` (matcher failure), C5 continues with VIO-only and labels `source_label = visual_propagated`.

**Traces to**: AC-3.5

**Description**: feed `add_vio` for 60 s while withholding `add_pose_anchor` calls; assert (a) `current_estimate` keeps emitting, (b) `source_label == visual_propagated`, (c) `cov_norm_growing_for_s` rises monotonically.

**Input data**: scripted VIO-only fixture.

**Expected result**: 60 s of visual_propagated estimates; cov norm monotonically rising.

**Max execution time**: 90 s.

---

### C5-IT-04: smoothed past-keyframe history is NOT forwarded to FC

**Summary**: `smoothed_history(n)` reflects past-keyframe smoothing per AC-4.5 (revised), but the FC emission path uses `current_estimate` only — the smoothing must NOT alter what C8 emits as `GPS_INPUT` / `MSP2_SENSOR_GPS`.

**Traces to**: AC-4.5 (revised)

**Description**: trigger an iSAM2 relinearisation that materially shifts a 5-keyframe-old pose; assert (a) `smoothed_history(10)` shows the shift, (b) the next 10 calls to `current_estimate` are unaffected, (c) the FDR record stream contains the smoothed history (per AC-4.5) AND the unshifted FC emissions in the same flight log.

**Input data**: synthetic graph with a deliberately-late loop closure.

**Expected result**: history shows shift; current_estimate unaffected; FDR has both streams.

**Max execution time**: 60 s.

---

### C5-IT-05: 3 s no-estimate threshold triggers AC-5.2 fallback

**Summary**: when no `add_vio` and no `add_pose_anchor` arrive for >3 s, `current_estimate` returns a `dead_reckoned`-labeled output (or refuses to emit) so C8 can fall to FC IMU-only.

**Traces to**: AC-5.2

**Description**: prime C5 with a normal warm-start; cease all input for 4 s; observe `current_estimate` over the gap; assert (a) at < 3 s gap, label is `visual_propagated`, (b) at ≥ 3 s gap, label transitions to `dead_reckoned` or no emission, (c) the transition timestamp is logged at ERROR level.

**Input data**: scripted gap fixture.

**Expected result**: transition at gap ≥ 3 s; ERROR logged.

**Max execution time**: 30 s.

---

### C5-IT-06: spoof-promotion gate enforces ≥10 s + visual consistency

**Summary**: a previously-spoofed FC GPS source can only be re-promoted to trusted after BOTH (i) `gps_health == STABLE_NON_SPOOFED` for ≥10 s AND (ii) the next satellite-anchored frame agrees with FC GPS within tolerance.

**Traces to**: AC-NEW-2, AC-NEW-8

**Description**: scripted scenario — initial trust → spoof event (gps_health = SPOOFED) → recovery to STABLE_NON_SPOOFED at t=0; satellite-anchored agreement frames at t=5 s, t=11 s. Assert promotion blocks until t=11 s + agreement; reject every promotion attempt before then; log every reject in FDR + STATUSTEXT.

**Input data**: scripted `gps_health` + `EstimatorOutput` sequence.

**Expected result**: promotion at t=11 s; rejects logged before then.

**Max execution time**: 60 s.

---

### C5-IT-07: visual blackout + spoof escalation

**Summary**: simultaneous visual blackout (no C4 anchors) and spoofed FC GPS escalates to `dead_reckoned` source label and AC-NEW-8 STATUSTEXT.

**Traces to**: AC-NEW-8

**Description**: combine the visual-blackout fixture (no `add_pose_anchor` for 5 s) with a `gps_health == SPOOFED` event; assert the next emission is `dead_reckoned` and an AC-NEW-8 STATUSTEXT is published via the C8 path (mocked C8 in the test harness records the outgoing message).

**Input data**: combined scripted fixture.

**Expected result**: `dead_reckoned` label; STATUSTEXT recorded in mock C8 harness.

**Max execution time**: 30 s.

---

## Performance Tests

### C5-PT-01: iSAM2 + Marginals throughput on Tier-2

**Traces to**: AC-4.1

**Load scenario**: 3 Hz `add_pose_anchor` + 200 Hz `add_fc_imu` + 3 Hz `add_vio`; 10 min replay.

**Expected results**:

| Metric | Target | Failure Threshold |
|--------|--------|-------------------|
| `add_pose_anchor` + `current_estimate` p95 | ≤ 60 ms | 100 ms |
| `add_fc_imu` p95 | ≤ 1 ms (preintegration buffer-add only) | 5 ms |
| `add_vio` p95 | ≤ 5 ms | 15 ms |

**Resource limits**:
- Memory: bounded by `IncrementalFixedLagSmoother K=10–20`; ≤ 100 MB resident.

---

## Security Tests

### C5-ST-01: spoof-rejection logging cannot be silenced

**Summary**: every spoof-promotion-block event lands in FDR + GCS STATUSTEXT — the system has no config knob that disables this.

**Traces to**: AC-NEW-2 / AC-NEW-8 (defensive)

**Test procedure**:
1. Configure C5 with the production-default config.
2. Inject a spoof-promotion-block event.
3. Assert the FDR record stream contains an entry with `kind = "spoof_promotion_block"` AND a STATUSTEXT was issued.
4. Search the codebase for any config flag that could disable either path; assert no such flag exists.

**Pass criteria**: FDR + STATUSTEXT both recorded; no disabling config flag found.
**Fail criteria**: either path missing or a disabling flag exists.

---

## Acceptance Tests

Covered transitively via FT-P-02 / FT-P-03 / FT-N-04 / NFT-RES-01.

---

## Test Data Management

| Data Set | Source | Size |
|----------|--------|------|
| Synthetic factor-graph fixtures | scripted | <5 MB |
| `flight_derkachi/normal_segment_60_stills/` (replayed VIO + pose feeds) | shared | shared |
| `gps_health` event fixtures | scripted | <1 MB |

**Setup**: in-process; no external services.
**Teardown**: per-test temp dirs.
**Data isolation**: each test instantiates a fresh `StateEstimator`.
