[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:
Oleksandr Bezdieniezhnykh
2026-05-16 17:54:00 +03:00
parent ff1b00200c
commit 702a0c0ff3
27 changed files with 4619 additions and 58 deletions
@@ -0,0 +1,172 @@
"""Behavioural tests for the AZ-408 multi_segment injector.
Covers AC-5 (≥3 disjoint windows, ≥30 s gaps, ≤25 % total coverage) and
AC-6 (tmpfs scratch isolation).
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from fixtures.injectors import multi_segment
def _build_synthetic_frames_dir(parent: Path, count: int) -> Path:
from PIL import Image # noqa: PLC0415
frames_dir = parent / "frames"
frames_dir.mkdir(parents=True, exist_ok=True)
img = Image.new("RGB", (256, 256), color=(60, 60, 60))
for i in range(count):
img.save(
frames_dir / f"AD{i + 1:06d}.jpg",
format="JPEG", quality=85, optimize=False, progressive=False, subsampling=2,
)
return frames_dir
def test_produces_three_disjoint_segments(tmp_path: Path) -> None:
"""AC-5: 3 disjoint blackout windows."""
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000) # 5 min @ 30 fps
plan = multi_segment.MultiSegmentPlan(
source_frames_dir=frames, n_segments=3, segment_seconds=15.0
)
# Act
report = multi_segment.build(plan, tmp_path / "out")
# Assert
assert len(report.segments) == 3
# Each segment is non-empty
for s in report.segments:
assert s.end_ms > s.start_ms
# Disjoint
for prev, nxt in zip(report.segments, report.segments[1:]):
assert prev.end_ms < nxt.start_ms
def test_segments_are_at_least_30_seconds_apart(tmp_path: Path) -> None:
"""AC-5: consecutive segments separated by ≥30 s of normal frames."""
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000)
plan = multi_segment.MultiSegmentPlan(
source_frames_dir=frames, n_segments=3, segment_seconds=12.0
)
# Act
report = multi_segment.build(plan, tmp_path / "out")
# Assert
for prev, nxt in zip(report.segments, report.segments[1:]):
gap_ms = nxt.start_ms - prev.end_ms
assert gap_ms >= 30_000, f"gap {gap_ms} ms < 30 s between segments"
def test_total_blackout_below_25_percent(tmp_path: Path) -> None:
"""AC-5: total blackout coverage ≤ 25 %."""
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000)
plan = multi_segment.MultiSegmentPlan(
source_frames_dir=frames, n_segments=3, segment_seconds=15.0
)
# Act
report = multi_segment.build(plan, tmp_path / "out")
# Assert
assert report.total_blackout_fraction <= 0.25
def test_rejects_overlapping_gap(tmp_path: Path) -> None:
"""Infeasible plan: too many segments inside too short a source."""
# Arrange — 30 s source can't fit 3×12 s segments with 30 s gaps
frames = _build_synthetic_frames_dir(tmp_path / "src", count=900)
plan = multi_segment.MultiSegmentPlan(
source_frames_dir=frames, n_segments=3, segment_seconds=12.0
)
# Act / Assert
with pytest.raises(ValueError, match="gap between segment|blackout fraction"):
multi_segment.build(plan, tmp_path / "out")
def test_rejects_too_few_segments(tmp_path: Path) -> None:
"""AC-5: n_segments must be ≥3."""
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=900)
plan = multi_segment.MultiSegmentPlan(
source_frames_dir=frames, n_segments=2, segment_seconds=5.0
)
# Act / Assert
with pytest.raises(ValueError, match="n_segments must be ≥3"):
multi_segment.build(plan, tmp_path / "out")
def test_rejects_zero_segment_seconds(tmp_path: Path) -> None:
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=900)
plan = multi_segment.MultiSegmentPlan(
source_frames_dir=frames, n_segments=3, segment_seconds=0.0
)
# Act / Assert
with pytest.raises(ValueError, match="segment_seconds"):
multi_segment.build(plan, tmp_path / "out")
def test_blackout_frames_are_black(tmp_path: Path) -> None:
"""Frames inside any segment are all-zero (black) on disk."""
# Arrange
from PIL import Image # noqa: PLC0415
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000)
plan = multi_segment.MultiSegmentPlan(
source_frames_dir=frames, n_segments=3, segment_seconds=10.0
)
out_root = tmp_path / "out"
report = multi_segment.build(plan, out_root)
# Act
for seg in report.segments[:1]: # spot-check first segment
for idx in range(seg.first_frame_idx, min(seg.first_frame_idx + 5, seg.last_frame_idx)):
name = f"AD{idx + 1:06d}.jpg"
img = Image.open(out_root / "frames" / name).convert("RGB")
r, g, b = img.getpixel((128, 128)) # type: ignore[misc]
# Assert
assert r < 5 and g < 5 and b < 5
def test_summary_json_present_with_expected_fields(tmp_path: Path) -> None:
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000)
plan = multi_segment.MultiSegmentPlan(
source_frames_dir=frames, n_segments=3, segment_seconds=10.0
)
# Act
multi_segment.build(plan, tmp_path / "out")
payload = json.loads((tmp_path / "out" / "summary.json").read_text())
# Assert
assert payload["scenario"] == "multi-segment-derkachi"
assert payload["n_segments"] == 3
assert payload["total_blackout_fraction"] <= 0.25
def test_overwrites_existing_out_root(tmp_path: Path) -> None:
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000)
plan = multi_segment.MultiSegmentPlan(
source_frames_dir=frames, n_segments=3, segment_seconds=10.0
)
out_root = tmp_path / "out"
multi_segment.build(plan, out_root)
(out_root / "stale.txt").write_text("stale")
# Act
multi_segment.build(plan, out_root)
# Assert
assert not (out_root / "stale.txt").exists()