mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 20:11:15 +00:00
702a0c0ff3
AZ-408 (3pt) — Replace AZ-406 injector scaffolds with concrete generators: - outlier.py: deterministic stride + far-away tile replacement; AC-2 ≥350m offset - blackout_spoof.py: paired video blackout + FC GPS spoof with ≤40ms alignment; AC-4 realistic fix_type/hdop; AC-NEW-8 200-500m inter-spoof deltas - multi_segment.py: ≥3 disjoint windows, ≥30s gaps, ≤25% coverage - fc_proxy.py: timed-splice runtime proxy with pre-activate RuntimeError guard - _common.py: derive_rng + tile-manifest reader + tmpfs helpers - injector_fixtures.py: pytest fixtures wired via runner conftest AZ-410 (3pt) — FT-P-02 cumulative drift between satellite anchors: - anchor_pair_detector.py: AC-1 detection, AC-2/3 pass-fraction, AC-4 monotonicity check, CSV evidence - test_ft_p_02_derkachi_drift.py: scenario gated on upstream helper NotImplementedError (frame_source_replay / fdr_reader / imu_replay) AZ-411 (2pt) — FT-P-03 + FT-P-14 schema + WGS84: - estimate_schema.py: AC-1 schema completeness, AC-2 source-label set containment, AC-3 WGS84 range + int32 1e-7 decode - test_ft_p_03_14_schema_wgs84.py: shared single-image-push scenario Tests: 248 unit tests pass (+91 vs batch 68). Reports: batch_69_report.md, batch_69_review.md (PASS), cumulative_review_batches_67-69_cycle1_report.md (PASS). Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
4.3 KiB
Python
142 lines
4.3 KiB
Python
"""Public-surface contract tests for the AZ-408 injector dataclasses.
|
|
|
|
AZ-406 commits to module locations; AZ-408 owns the concrete dataclass
|
|
shapes. These tests assert the API surface (frozen dataclasses, public
|
|
``build()`` functions returning typed reports). Behavioural tests live
|
|
in their own files (``test_outlier.py``, ``test_blackout_spoof.py``,
|
|
``test_multi_segment.py``, ``test_fc_proxy.py``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from fixtures.injectors.blackout_spoof import BlackoutSpoofPlan, BlackoutSpoofReport
|
|
from fixtures.injectors.cold_boot import ColdBootFixture
|
|
from fixtures.injectors.cold_boot import load as load_cold_boot
|
|
from fixtures.injectors.fc_proxy import BlackoutSpoofProxy, SpoofGpsRecord
|
|
from fixtures.injectors.multi_segment import MultiSegmentPlan, MultiSegmentReport
|
|
from fixtures.injectors.outlier import OutlierInjectionPlan, OutlierInjectionReport
|
|
|
|
|
|
def test_outlier_plan_dataclass_is_frozen() -> None:
|
|
# Arrange
|
|
plan = OutlierInjectionPlan(
|
|
source_frames_dir=Path("/tmp/frames"),
|
|
tile_cache_dir=Path("/tmp/tile-cache"),
|
|
density="medium",
|
|
)
|
|
# Act / Assert
|
|
with pytest.raises(AttributeError):
|
|
plan.density = "heavy" # type: ignore[misc]
|
|
assert plan.min_offset_m == 350.0
|
|
|
|
|
|
def test_outlier_plan_density_literal_round_trip() -> None:
|
|
# Arrange / Act
|
|
for density in ("light", "medium", "heavy"):
|
|
plan = OutlierInjectionPlan(
|
|
source_frames_dir=Path("/tmp"),
|
|
tile_cache_dir=Path("/tmp"),
|
|
density=density, # type: ignore[arg-type]
|
|
)
|
|
# Assert
|
|
assert plan.density == density
|
|
|
|
|
|
def test_outlier_report_is_frozen_dataclass() -> None:
|
|
# Arrange
|
|
report = OutlierInjectionReport(
|
|
out_root=Path("/tmp/out"),
|
|
total_source_frames=100,
|
|
replaced_frame_count=10,
|
|
density="medium",
|
|
min_geodesic_offset_m=400.0,
|
|
max_geodesic_offset_m=900.0,
|
|
)
|
|
# Act / Assert
|
|
with pytest.raises(AttributeError):
|
|
report.replaced_frame_count = 20 # type: ignore[misc]
|
|
|
|
|
|
def test_blackout_spoof_plan_round_trip() -> None:
|
|
# Arrange / Act
|
|
plan = BlackoutSpoofPlan(
|
|
source_frames_dir=Path("/tmp/frames"),
|
|
blackout_seconds=35.0,
|
|
spoof_offset_m=120.0,
|
|
spoof_bearing_deg=90.0,
|
|
)
|
|
# Assert
|
|
assert plan.blackout_seconds == 35.0
|
|
assert plan.max_alignment_err_ms == 40.0 # default per AC-3
|
|
|
|
|
|
def test_blackout_spoof_report_is_frozen_dataclass() -> None:
|
|
# Arrange
|
|
proxy = BlackoutSpoofProxy(window_start_ms=0, window_end_ms=1000, spoof_gps=[])
|
|
# Assert that the report type is constructible (smoke check)
|
|
assert proxy.activation_report is None
|
|
|
|
|
|
def test_multi_segment_plan_defaults() -> None:
|
|
# Arrange / Act
|
|
plan = MultiSegmentPlan(source_frames_dir=Path("/tmp/frames"))
|
|
# Assert
|
|
assert plan.n_segments == 3
|
|
assert plan.segment_seconds == 12.0
|
|
|
|
|
|
def test_multi_segment_report_is_frozen_dataclass() -> None:
|
|
# Arrange
|
|
report = MultiSegmentReport(
|
|
out_root=Path("/tmp/out"),
|
|
segments=[],
|
|
source_duration_ms=300_000,
|
|
total_blackout_frames=300,
|
|
total_blackout_fraction=0.10,
|
|
)
|
|
# Act / Assert
|
|
with pytest.raises(AttributeError):
|
|
report.source_duration_ms = 0 # type: ignore[misc]
|
|
|
|
|
|
def test_spoof_gps_record_is_frozen_dataclass() -> None:
|
|
# Arrange
|
|
rec = SpoofGpsRecord(
|
|
monotonic_ms=1000,
|
|
lat_deg=50.1,
|
|
lon_deg=36.2,
|
|
alt_m=300.0,
|
|
fix_type=3,
|
|
hdop=1.0,
|
|
)
|
|
# Act / Assert
|
|
with pytest.raises(AttributeError):
|
|
rec.lat_deg = 0.0 # type: ignore[misc]
|
|
|
|
|
|
# Cold-boot tests are unchanged from AZ-406 — the cold-boot loader is
|
|
# still owned by AZ-419, not AZ-408.
|
|
|
|
|
|
def test_cold_boot_fixture_dataclass_is_frozen() -> None:
|
|
# Arrange
|
|
fx = ColdBootFixture(
|
|
lat_deg=50.0, lon_deg=30.0, alt_m=300.0, yaw_deg=180.0, last_valid_fix_age_s=2.5
|
|
)
|
|
# Act / Assert
|
|
with pytest.raises(AttributeError):
|
|
fx.alt_m = 999.0 # type: ignore[misc]
|
|
|
|
|
|
def test_cold_boot_load_raises_until_az419_lands(tmp_path: Path) -> None:
|
|
# Arrange
|
|
fixture_path = tmp_path / "cold_boot_fixture.json"
|
|
fixture_path.write_text("{}", encoding="utf-8")
|
|
# Act / Assert
|
|
with pytest.raises(NotImplementedError, match="AZ-419"):
|
|
load_cold_boot(fixture_path)
|