[AZ-960] render-map: dispatch --truth loader on extension (CSV+tlog)

load_ground_truth_track now dispatches on truth_path.suffix:
- .csv → load_csv_ground_truth (AZ-894)
- else (.tlog, .bin, no ext) → load_tlog_ground_truth (AZ-697)

Removes the AZ-959 short-circuit in SubprocessReplayRunner.
_maybe_render_map so CSV-path replay jobs ship with the same
map.html artefact as tlog jobs. Both ground-truth DTOs expose
row-aligned (lat_deg, lon_deg) records so the renderer needs no
other changes.

Touches:
- src/gps_denied_onboard/cli/render_map.py: dispatch +
  source-agnostic tooltip + --truth CLI help expanded
- src/gps_denied_onboard/replay_api/app.py: workaround removed,
  truth_path resolution picks whichever input was uploaded

Tests: 44/44 green across test_az700_render_map.py +
test_az701_replay_api.py:
- 17 pre-existing render-map tests pass unchanged (AC-2)
- New test_load_ground_truth_track_dispatches_to_csv_loader (AC-1)
- New test_load_ground_truth_track_csv_propagates_schema_error
  (AC-4: malformed CSV raises ReplayInputAdapterError)
- New test_cli_renders_map_with_csv_truth (AC-1 end-to-end)
- AZ-959 test_post_replay_csv_path_returns_200... extended to
  assert map_html_url is now present (AC-3)

Bookkeeping: AZ-960 spec moved todo/ → done/, dep-table preamble
seventh bump documents the landing + AC coverage, state.md records
batch 6 complete with AZ-961 as next.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-29 12:53:17 +03:00
parent 363c235264
commit 7f590582cc
7 changed files with 128 additions and 20 deletions
+89
View File
@@ -371,3 +371,92 @@ def test_load_ground_truth_track_returns_lat_lon_pairs(tmp_path: Path) -> None:
for lat, lon in track:
assert 49.99 < lat < 50.01
assert 29.99 < lon < 30.01
# ---------------------------------------------------------------------
# AZ-960 — CSV-path dispatch for --truth
def _write_minimal_csv(path: Path) -> None:
"""Write a tiny AZ-896-schema CSV with 3 GPS rows for AZ-960 tests."""
header = (
"timestamp(ms),Time,"
"SCALED_IMU2.xacc,SCALED_IMU2.yacc,SCALED_IMU2.zacc,"
"SCALED_IMU2.xgyro,SCALED_IMU2.ygyro,SCALED_IMU2.zgyro,"
"GLOBAL_POSITION_INT.lat,GLOBAL_POSITION_INT.lon,"
"GLOBAL_POSITION_INT.alt,GLOBAL_POSITION_INT.vx,"
"GLOBAL_POSITION_INT.vy,GLOBAL_POSITION_INT.vz,"
"GLOBAL_POSITION_INT.hdg"
)
rows = [
"0,0.0,21,-3,-984,52,32,-5,50.000,30.000,141290,0,0,0,35041",
"100,0.1,-68,-9,-995,58,-17,1,50.0005,30.0005,141360,0,0,0,35042",
"200,0.2,9,108,-988,69,-65,13,50.001,30.001,141410,0,0,0,35048",
]
path.write_text(header + "\n" + "\n".join(rows) + "\n")
def test_load_ground_truth_track_dispatches_to_csv_loader(
tmp_path: Path,
) -> None:
# Arrange — AC-1: csv extension routes through load_csv_ground_truth
csv_path = tmp_path / "data_imu.csv"
_write_minimal_csv(csv_path)
# Act
track = load_ground_truth_track(csv_path)
# Assert
assert len(track) == 3
for lat, lon in track:
assert 49.999 < lat < 50.002
assert 29.999 < lon < 30.002
def test_load_ground_truth_track_csv_propagates_schema_error(
tmp_path: Path,
) -> None:
# Arrange — AC-4: malformed CSV fails fast via ReplayInputAdapterError
csv_path = tmp_path / "bad.csv"
csv_path.write_text(
"timestamp(ms),SCALED_IMU2.xacc,GLOBAL_POSITION_INT.lat\n"
"0,0,50.0\n"
)
from gps_denied_onboard.replay_input.errors import ReplayInputAdapterError
# Act / Assert
with pytest.raises(ReplayInputAdapterError, match="Time"):
load_ground_truth_track(csv_path)
def test_cli_renders_map_with_csv_truth(tmp_path: Path) -> None:
# Arrange — AC-1 end-to-end: CLI accepts --truth foo.csv
csv_path = tmp_path / "data_imu.csv"
_write_minimal_csv(csv_path)
estimated = tmp_path / "estimator.jsonl"
_write_jsonl(
estimated,
[
{"position_wgs84": {"lat_deg": 50.0, "lon_deg": 30.0, "alt_m": 100}},
{"position_wgs84": {"lat_deg": 50.001, "lon_deg": 30.001, "alt_m": 101}},
],
)
output = tmp_path / "map.html"
# Act
rc = main(
[
"--estimated", str(estimated),
"--truth", str(csv_path),
"--output", str(output),
]
)
# Assert
assert rc == 0
assert output.is_file()
body = output.read_text()
assert body.startswith("<!DOCTYPE html>")
assert "L.polyline" in body
assert '"color": "red"' in body
assert '"color": "blue"' in body