mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 09:51:13 +00:00
[AZ-408] [AZ-410] [AZ-411] Batch 69: synth injectors + FT-P-02/03/14
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>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
"""Unit tests for the injector public surfaces.
|
||||
"""Public-surface contract tests for the AZ-408 injector dataclasses.
|
||||
|
||||
AZ-406 commits to the type signatures + the NotImplementedError pointer.
|
||||
AZ-408 will replace each NotImplementedError with a real generator; these
|
||||
tests will then be updated alongside the implementation.
|
||||
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
|
||||
@@ -11,52 +13,129 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures.injectors.blackout_spoof import BlackoutSpoofPlan
|
||||
from fixtures.injectors.blackout_spoof import build as build_blackout_spoof
|
||||
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.multi_segment import MultiSegmentPlan
|
||||
from fixtures.injectors.multi_segment import build as build_multi_segment
|
||||
from fixtures.injectors.outlier import OutlierInjectionPlan
|
||||
from fixtures.injectors.outlier import build as build_outlier
|
||||
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:
|
||||
plan = OutlierInjectionPlan(target_segment_seconds=(0.0, 5.0))
|
||||
# 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.max_offset_m = 999.0 # type: ignore[misc]
|
||||
assert plan.max_offset_m == 350.0
|
||||
plan.density = "heavy" # type: ignore[misc]
|
||||
assert plan.min_offset_m == 350.0
|
||||
|
||||
|
||||
def test_outlier_build_raises_until_az408_lands() -> None:
|
||||
with pytest.raises(NotImplementedError, match="AZ-408"):
|
||||
build_outlier(OutlierInjectionPlan(target_segment_seconds=(0.0, 5.0)), Path("/tmp"))
|
||||
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:
|
||||
plan = BlackoutSpoofPlan(blackout_seconds=35.0, spoof_offset_m=120.0, spoof_bearing_deg=90.0)
|
||||
# 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
|
||||
with pytest.raises(NotImplementedError, match="AZ-408"):
|
||||
build_blackout_spoof(plan, Path("/tmp"))
|
||||
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:
|
||||
plan = MultiSegmentPlan()
|
||||
# Arrange / Act
|
||||
plan = MultiSegmentPlan(source_frames_dir=Path("/tmp/frames"))
|
||||
# Assert
|
||||
assert plan.n_segments == 3
|
||||
with pytest.raises(NotImplementedError, match="AZ-408"):
|
||||
build_multi_segment(plan, Path("/tmp"))
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user