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