621 lines
22 KiB
Python
621 lines
22 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
import pytest
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.routing import APIRoute
|
|
|
|
from k1link.simulation import (
|
|
AuthorityProfile,
|
|
ProviderPin,
|
|
QualificationArtifact,
|
|
QualificationRun,
|
|
QualificationRunStore,
|
|
ReproducibilityTier,
|
|
RunKind,
|
|
RunState,
|
|
)
|
|
from k1link.simulation.stock_rover import STOCK_ROVER_PROVIDER_PROFILE
|
|
from k1link.web.polygon_api import (
|
|
AckermannCommandRequest,
|
|
StartStockRoverRequest,
|
|
build_polygon_router,
|
|
)
|
|
|
|
SHA_A = "a" * 64
|
|
SHA_B = "b" * 64
|
|
SHA_C = "c" * 64
|
|
|
|
|
|
def _run(run_id: str = "s1b-ui0-001") -> QualificationRun:
|
|
return QualificationRun(
|
|
run_id=run_id,
|
|
episode_id=f"episode-{run_id}",
|
|
kind=RunKind.SIMULATION_CLOSED_LOOP,
|
|
state=RunState.ADMITTED,
|
|
scenario_generation="px4-v1.17.0-stock-rover-ackermann",
|
|
scenario_sha256=SHA_A,
|
|
profile_generation="stock-rover-lifecycle-v1",
|
|
profile_sha256=SHA_B,
|
|
mission_core_commit="6cb1495a1234567890abcdef1234567890abcdef",
|
|
providers=(
|
|
ProviderPin(
|
|
identifier="px4-autopilot",
|
|
version="v1.17.0",
|
|
revision="v1.17.0",
|
|
),
|
|
ProviderPin(
|
|
identifier="gazebo",
|
|
version="harmonic",
|
|
revision="8.9.0",
|
|
),
|
|
),
|
|
host_profile_id="mission-gpu-s0",
|
|
host_profile_sha256=SHA_C,
|
|
seed=42,
|
|
reproducibility_tier=ReproducibilityTier.R1,
|
|
authority=AuthorityProfile(
|
|
generation=1,
|
|
command_ttl_max_ns=250_000_000,
|
|
heartbeat_timeout_monotonic_ns=500_000_000,
|
|
),
|
|
clock_domain="gazebo:/clock",
|
|
created_at_utc="2026-07-24T15:35:00Z",
|
|
)
|
|
|
|
|
|
def _accepted_run(root: Path) -> QualificationRun:
|
|
store = QualificationRunStore(root)
|
|
admitted = store.create(_run())
|
|
starting = store.transition(
|
|
admitted.run_id,
|
|
RunState.STARTING,
|
|
expected_revision=0,
|
|
observed_at_utc="2026-07-24T15:35:01Z",
|
|
host_monotonic_ns=1,
|
|
)
|
|
running = store.transition(
|
|
admitted.run_id,
|
|
RunState.RUNNING,
|
|
expected_revision=starting.revision,
|
|
observed_at_utc="2026-07-24T15:35:02Z",
|
|
host_monotonic_ns=2,
|
|
sim_time_ns=0,
|
|
)
|
|
event = store.append_event(
|
|
admitted.run_id,
|
|
event_type="orchestrator.providers-started",
|
|
observed_at_utc="2026-07-24T15:35:03Z",
|
|
host_monotonic_ns=3,
|
|
sim_time_ns=1_000_000,
|
|
payload={"provider_ids": ["micro-xrce-dds-agent", "px4-gazebo-stock-rover"]},
|
|
expected_revision=running.revision,
|
|
)
|
|
store.register_artifact(
|
|
admitted.run_id,
|
|
QualificationArtifact(
|
|
artifact_id="provider-log-index",
|
|
kind="log-index",
|
|
relative_path="provider-runtime/processes.json",
|
|
sha256=SHA_A,
|
|
byte_length=512,
|
|
source_of_record=True,
|
|
),
|
|
)
|
|
stopping = store.transition(
|
|
admitted.run_id,
|
|
RunState.STOPPING,
|
|
expected_revision=event.sequence,
|
|
observed_at_utc="2026-07-24T15:35:04Z",
|
|
host_monotonic_ns=4,
|
|
sim_time_ns=2_000_000,
|
|
)
|
|
return store.transition(
|
|
admitted.run_id,
|
|
RunState.COMPLETED,
|
|
expected_revision=stopping.revision,
|
|
observed_at_utc="2026-07-24T15:35:05Z",
|
|
host_monotonic_ns=5,
|
|
sim_time_ns=2_000_000,
|
|
reason="operator-stop-clean",
|
|
)
|
|
|
|
|
|
def _endpoint(router: APIRouter, path: str, method: str) -> Callable[..., Any]:
|
|
for route in router.routes:
|
|
if isinstance(route, APIRoute) and route.path == path and method in route.methods:
|
|
return route.endpoint
|
|
raise AssertionError(f"{method} {path} route is missing")
|
|
|
|
|
|
def test_polygon_api_is_read_only_and_fails_closed_when_unconfigured() -> None:
|
|
router = build_polygon_router(root_provider=lambda: None)
|
|
routes = [route for route in router.routes if isinstance(route, APIRoute)]
|
|
|
|
archive_routes = [route for route in routes if "/worker" not in route.path]
|
|
assert all(route.methods <= {"GET", "HEAD"} for route in archive_routes)
|
|
with pytest.raises(HTTPException) as failure:
|
|
_endpoint(router, "/api/v1/polygon/runs", "GET")(limit=20)
|
|
assert failure.value.status_code == 503
|
|
assert "не настроен" in failure.value.detail
|
|
|
|
|
|
def test_polygon_api_publishes_bounded_path_free_qualification_evidence(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
root = tmp_path / "runs"
|
|
accepted = _accepted_run(root)
|
|
router = build_polygon_router(root_provider=lambda: root)
|
|
|
|
catalog = _endpoint(router, "/api/v1/polygon/runs", "GET")(limit=20)
|
|
detail = _endpoint(router, "/api/v1/polygon/runs/{run_id}", "GET")(
|
|
run_id=accepted.run_id,
|
|
event_limit=2,
|
|
)
|
|
|
|
assert catalog["schema_version"] == "missioncore.polygon-run-catalog/v1"
|
|
assert catalog["access"] == "read-only"
|
|
assert catalog["total"] == 1
|
|
assert catalog["items"][0]["state"] == "completed"
|
|
assert catalog["items"][0]["terminal_reason"] == "operator-stop-clean"
|
|
assert detail["schema_version"] == "missioncore.polygon-run-detail/v1"
|
|
assert detail["access"] == "read-only"
|
|
assert detail["run"]["run_id"] == accepted.run_id
|
|
assert detail["run"]["authority"]["actuator_authority"] is False
|
|
assert detail["commands"] == {"count": 0, "content_exposed": False}
|
|
assert detail["events_total"] == accepted.revision
|
|
assert detail["events_truncated"] is True
|
|
assert len(detail["events"]) == 2
|
|
assert detail["artifacts"][0]["relative_path"] == "provider-runtime/processes.json"
|
|
serialized = repr({"catalog": catalog, "detail": detail})
|
|
assert str(tmp_path) not in serialized
|
|
assert "command_ttl_max_ns" in serialized
|
|
assert all("http" not in artifact for artifact in detail["artifacts"])
|
|
|
|
|
|
def test_polygon_api_rejects_corrupt_evidence_without_partial_response(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
root = tmp_path / "runs"
|
|
accepted = _accepted_run(root)
|
|
event_path = root / accepted.run_id / "events" / "00000000000000000001.json"
|
|
event_path.write_text("{broken", encoding="utf-8")
|
|
router = build_polygon_router(root_provider=lambda: root)
|
|
|
|
with pytest.raises(HTTPException) as failure:
|
|
_endpoint(router, "/api/v1/polygon/runs", "GET")(limit=20)
|
|
assert failure.value.status_code == 500
|
|
assert "целостности" in failure.value.detail
|
|
|
|
|
|
def _qualification_run(root: Path) -> tuple[QualificationRun, str]:
|
|
store = QualificationRunStore(root)
|
|
admitted = store.create(_run("goose-ground-test"))
|
|
starting = store.transition(
|
|
admitted.run_id,
|
|
RunState.STARTING,
|
|
expected_revision=0,
|
|
observed_at_utc="2026-07-24T15:35:01Z",
|
|
host_monotonic_ns=1,
|
|
)
|
|
store.transition(
|
|
admitted.run_id,
|
|
RunState.RUNNING,
|
|
expected_revision=starting.revision,
|
|
observed_at_utc="2026-07-24T15:35:02Z",
|
|
host_monotonic_ns=2,
|
|
)
|
|
evidence = root / admitted.run_id / "evidence"
|
|
failures = evidence / "failures"
|
|
failures.mkdir(parents=True)
|
|
frame_id = "2022-07-22_flight__0071_1658494234334310308"
|
|
report = {
|
|
"schema_version": "missioncore.goose-ground-qualification-report/v1",
|
|
"identity_sha256": SHA_A,
|
|
"source_id": "goose-3d/v2025-08-22",
|
|
"split": "validation",
|
|
"frame_count": 961,
|
|
"aggregates": {"current": {}, "patchworkpp": {}},
|
|
"degradations": {},
|
|
"checks": [],
|
|
"worst_frames": [{"frame_id": frame_id}],
|
|
"decision": {"status": "shadow-candidate", "passed": True},
|
|
"safety": {"navigation_or_safety_accepted": False},
|
|
"frames": [{"private": "not-published"}],
|
|
}
|
|
preview = {
|
|
"schema_version": "missioncore.goose-ground-qualification-failure-preview/v1",
|
|
"source_id": "goose-3d/v2025-08-22",
|
|
"frame_id": frame_id,
|
|
"source_point_count": 2,
|
|
"point_count": 2,
|
|
"sampling": "deterministic-even-index",
|
|
"points_xyz_m": [[0, 0, 0], [1, 1, 1]],
|
|
"ground_truth_ground": [1, 0],
|
|
"evaluated": [1, 1],
|
|
"current_ground": [0, 0],
|
|
"patchwork_ground": [1, 0],
|
|
"current_disagreement": [1, 0],
|
|
"patchwork_disagreement": [0, 0],
|
|
"safety": {"navigation_or_safety_accepted": False},
|
|
}
|
|
for artifact_id, kind, path, value in (
|
|
(
|
|
"qualification-report",
|
|
"goose-ground-qualification-report",
|
|
evidence / "qualification.json",
|
|
report,
|
|
),
|
|
(
|
|
"failure-preview-01",
|
|
"goose-ground-qualification-failure-preview",
|
|
failures / f"{frame_id}.json",
|
|
preview,
|
|
),
|
|
):
|
|
path.write_text(json.dumps(value), encoding="utf-8")
|
|
store.register_artifact(
|
|
admitted.run_id,
|
|
QualificationArtifact(
|
|
artifact_id=artifact_id,
|
|
kind=kind,
|
|
relative_path=path.relative_to(root / admitted.run_id).as_posix(),
|
|
sha256=hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
byte_length=path.stat().st_size,
|
|
source_of_record=True,
|
|
),
|
|
)
|
|
current = store.load(admitted.run_id)
|
|
stopping = store.transition(
|
|
admitted.run_id,
|
|
RunState.STOPPING,
|
|
expected_revision=current.revision,
|
|
observed_at_utc="2026-07-24T15:35:04Z",
|
|
host_monotonic_ns=4,
|
|
)
|
|
completed = store.transition(
|
|
admitted.run_id,
|
|
RunState.COMPLETED,
|
|
expected_revision=stopping.revision,
|
|
observed_at_utc="2026-07-24T15:35:05Z",
|
|
host_monotonic_ns=5,
|
|
reason="qualification-evidence-sealed",
|
|
)
|
|
return completed, frame_id
|
|
|
|
|
|
def test_polygon_api_publishes_verified_ground_qualification(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
root = tmp_path / "runs"
|
|
run, frame_id = _qualification_run(root)
|
|
router = build_polygon_router(root_provider=lambda: root)
|
|
|
|
qualification = _endpoint(
|
|
router,
|
|
"/api/v1/polygon/runs/{run_id}/qualification",
|
|
"GET",
|
|
)(run_id=run.run_id)
|
|
preview = _endpoint(
|
|
router,
|
|
"/api/v1/polygon/runs/{run_id}/qualification/failures/{frame_id}",
|
|
"GET",
|
|
)(run_id=run.run_id, frame_id=frame_id)
|
|
|
|
assert qualification["schema_version"] == "missioncore.polygon-ground-qualification/v1"
|
|
assert qualification["frame_count"] == 961
|
|
assert "frames" not in qualification
|
|
assert preview["schema_version"] == "missioncore.polygon-ground-failure-preview/v1"
|
|
assert preview["frame_id"] == frame_id
|
|
assert str(tmp_path) not in repr({"qualification": qualification, "preview": preview})
|
|
|
|
|
|
def test_polygon_api_rejects_tampered_ground_qualification(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
root = tmp_path / "runs"
|
|
run, _ = _qualification_run(root)
|
|
(root / run.run_id / "evidence/qualification.json").write_text("{}", encoding="utf-8")
|
|
router = build_polygon_router(root_provider=lambda: root)
|
|
|
|
with pytest.raises(HTTPException) as failure:
|
|
_endpoint(
|
|
router,
|
|
"/api/v1/polygon/runs/{run_id}/qualification",
|
|
"GET",
|
|
)(run_id=run.run_id)
|
|
assert failure.value.status_code == 500
|
|
assert "SHA-256" in failure.value.detail or "файла" in failure.value.detail
|
|
|
|
|
|
def _review_pack(root: Path, run_id: str, frame_id: str) -> Path:
|
|
identity = "d" * 64
|
|
pack = root.parent / "polygon-review-packs" / run_id / f"review-{identity}"
|
|
frames = pack / "frames"
|
|
frames.mkdir(parents=True)
|
|
frame_path = frames / f"{frame_id}.npz"
|
|
np.savez_compressed(
|
|
frame_path,
|
|
schema=np.asarray(["missioncore.goose-ground-review-frame/v1"]),
|
|
frame_id=np.asarray([frame_id]),
|
|
source_point_count=np.asarray([3], dtype=np.int32),
|
|
xyz_cm=np.asarray([[0, 0, 0], [100, -100, 50]], dtype="<i2"),
|
|
remission_u8=np.asarray([12, 255], dtype=np.uint8),
|
|
flags=np.asarray([15, 1], dtype=np.uint8),
|
|
)
|
|
metric = {
|
|
"ground_iou": 0.5,
|
|
"natural_ground_recall": 0.6,
|
|
"obstacle_non_ground_recall": 0.95,
|
|
"latency_ms": 20.0,
|
|
}
|
|
manifest = {
|
|
"schema_version": "missioncore.goose-ground-review-pack/v1",
|
|
"source_run_id": run_id,
|
|
"identity_sha256": identity,
|
|
"source_id": "goose-3d/v2025-08-22",
|
|
"preview_points": 12_000,
|
|
"frame_count": 1,
|
|
"frames": [
|
|
{
|
|
"sequence": 0,
|
|
"frame_id": frame_id,
|
|
"source_point_count": 3,
|
|
"point_count": 2,
|
|
"relative_path": f"frames/{frame_id}.npz",
|
|
"sha256": hashlib.sha256(frame_path.read_bytes()).hexdigest(),
|
|
"byte_length": frame_path.stat().st_size,
|
|
"current": metric,
|
|
"patchworkpp": {**metric, "ground_iou": 0.7},
|
|
"ground_iou_delta": 0.2,
|
|
}
|
|
],
|
|
"safety": {
|
|
"visualization_only": True,
|
|
"actuator_authority": False,
|
|
"navigation_or_safety_accepted": False,
|
|
},
|
|
}
|
|
(pack / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
return frame_path
|
|
|
|
|
|
def test_polygon_api_publishes_all_frame_review_without_paths(tmp_path: Path) -> None:
|
|
root = tmp_path / "runs"
|
|
run, frame_id = _qualification_run(root)
|
|
_review_pack(root, run.run_id, frame_id)
|
|
router = build_polygon_router(root_provider=lambda: root)
|
|
|
|
review = _endpoint(
|
|
router,
|
|
"/api/v1/polygon/runs/{run_id}/qualification/review",
|
|
"GET",
|
|
)(run_id=run.run_id)
|
|
frame = _endpoint(
|
|
router,
|
|
"/api/v1/polygon/runs/{run_id}/qualification/review/frames/{frame_id}",
|
|
"GET",
|
|
)(run_id=run.run_id, frame_id=frame_id)
|
|
|
|
assert review["schema_version"] == "missioncore.polygon-ground-review/v2"
|
|
assert review["frame_count"] == 1
|
|
assert review["frames"][0]["ground_iou_delta"] == pytest.approx(0.2)
|
|
assert review["frames"][0]["dataset_sequence_id"] == "2022-07-22_flight"
|
|
assert review["frames"][0]["dataset_frame_number"] == 71
|
|
assert review["frames"][0]["sensor_timestamp_ns"] == "1658494234334310308"
|
|
assert "relative_path" not in review["frames"][0]
|
|
assert frame["schema_version"] == "missioncore.polygon-ground-review-frame/v1"
|
|
assert frame["points_xyz_m"] == [[0.0, 0.0, 0.0], [1.0, -1.0, 0.5]]
|
|
assert frame["intensity_0_255"] == [12, 255]
|
|
assert frame["ground_truth_ground"] == [1, 0]
|
|
assert frame["patchwork_ground"] == [1, 0]
|
|
assert str(tmp_path) not in repr({"review": review, "frame": frame})
|
|
|
|
|
|
def test_polygon_api_rejects_tampered_review_frame(tmp_path: Path) -> None:
|
|
root = tmp_path / "runs"
|
|
run, frame_id = _qualification_run(root)
|
|
frame_path = _review_pack(root, run.run_id, frame_id)
|
|
frame_path.write_bytes(b"tampered")
|
|
router = build_polygon_router(root_provider=lambda: root)
|
|
|
|
with pytest.raises(HTTPException) as failure:
|
|
_endpoint(
|
|
router,
|
|
"/api/v1/polygon/runs/{run_id}/qualification/review/frames/{frame_id}",
|
|
"GET",
|
|
)(run_id=run.run_id, frame_id=frame_id)
|
|
assert failure.value.status_code == 500
|
|
assert "SHA-256" in failure.value.detail or "файл" in failure.value.detail.lower()
|
|
|
|
|
|
class _FakeWorkerGateway:
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple[str, str]] = []
|
|
self.active_run_id: str | None = None
|
|
|
|
def status(self) -> dict[str, Any]:
|
|
return self._status()
|
|
|
|
def live(self) -> dict[str, Any]:
|
|
if self.active_run_id is None:
|
|
raise AssertionError("test worker has no active run")
|
|
return {
|
|
"schema_version": "missioncore.vehicle-state/v1",
|
|
"run_id": self.active_run_id,
|
|
"sequence": 1,
|
|
"observed_at_utc": "2026-07-24T18:00:00Z",
|
|
"host_monotonic_ns": 123,
|
|
"sim_time_ns": 456,
|
|
"frame_id": "map_enu",
|
|
"child_frame_id": "base_link_flu",
|
|
"pose": {
|
|
"position_m": {"x": 1.0, "y": 2.0, "z": 0.1},
|
|
"orientation_xyzw": {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0},
|
|
},
|
|
"source": {
|
|
"provider": "gazebo",
|
|
"topic": "/world/rover/dynamic_pose/info",
|
|
"signal": "ground-truth",
|
|
"quality": "diagnostic",
|
|
},
|
|
"safety": {
|
|
"scope": "virtual-only",
|
|
"actuator_authority": False,
|
|
"navigation_or_safety_accepted": False,
|
|
},
|
|
}
|
|
|
|
def start(
|
|
self,
|
|
*,
|
|
run_id: str,
|
|
mission_core_commit: str,
|
|
idempotency_key: str,
|
|
) -> dict[str, Any]:
|
|
self.calls.append(("start", idempotency_key))
|
|
assert mission_core_commit == "d" * 40
|
|
self.active_run_id = run_id
|
|
return self._status()
|
|
|
|
def stop(self, *, run_id: str, idempotency_key: str) -> dict[str, Any]:
|
|
self.calls.append(("stop", idempotency_key))
|
|
assert run_id == self.active_run_id
|
|
self.active_run_id = None
|
|
return self._status()
|
|
|
|
def command(
|
|
self,
|
|
*,
|
|
run_id: str,
|
|
speed_mps: float,
|
|
steering_normalized: float,
|
|
idempotency_key: str,
|
|
) -> dict[str, Any]:
|
|
self.calls.append(("command", idempotency_key))
|
|
assert run_id == self.active_run_id
|
|
return {
|
|
"schema_version": "missioncore.command-acceptance/v2",
|
|
"run_id": run_id,
|
|
"command_id": "cmd-test",
|
|
"sequence": 1,
|
|
"issued_at_sim_ns": 1_000_000,
|
|
"valid_until_sim_ns": 251_000_000,
|
|
"speed_mps": speed_mps,
|
|
"steering_normalized": steering_normalized,
|
|
"authority_scope": "virtual-only",
|
|
"delivery": {
|
|
"provider_id": "px4-ros2-offboard",
|
|
"control_profile": "rover-speed-steering/v1",
|
|
"accepted": True,
|
|
"controller_ready": True,
|
|
"ttl_expired_count": 0,
|
|
"diagnostics": {"armed": True, "offboard": True},
|
|
},
|
|
}
|
|
|
|
def _status(self) -> dict[str, Any]:
|
|
return {
|
|
"schema_version": "missioncore.simulation-worker-status/v2",
|
|
"worker_id": "mission-gpu-s1",
|
|
"transport": "unix",
|
|
"mode": "simulation",
|
|
"available": True,
|
|
"control_available": True,
|
|
"active_run_id": self.active_run_id,
|
|
"run_state": "running" if self.active_run_id else None,
|
|
"active_provider_ids": (["px4-gazebo-stock-rover"] if self.active_run_id else []),
|
|
"provider_profile": STOCK_ROVER_PROVIDER_PROFILE.to_dict(),
|
|
"isolation": {
|
|
"network": "loopback-only-netns",
|
|
"process_identity": "missioncore",
|
|
"artifact_policy": "d-only",
|
|
},
|
|
"authority": {
|
|
"scope": "virtual-only",
|
|
"actuator_authority": False,
|
|
"direct_actuator_setpoints_allowed": False,
|
|
},
|
|
}
|
|
|
|
|
|
def test_polygon_worker_api_fails_closed_then_proxies_virtual_only_lifecycle() -> None:
|
|
worker = _FakeWorkerGateway()
|
|
disabled = build_polygon_router(
|
|
root_provider=lambda: None,
|
|
worker_provider=lambda: worker,
|
|
control_provider=lambda: False,
|
|
commit_provider=lambda: "d" * 40,
|
|
)
|
|
status = _endpoint(disabled, "/api/v1/polygon/worker", "GET")()
|
|
assert status["available"] is True
|
|
assert status["control_available"] is False
|
|
with pytest.raises(HTTPException) as failure:
|
|
_endpoint(disabled, "/api/v1/polygon/worker/runs", "POST")(
|
|
request=StartStockRoverRequest(scenario_id="stock-rover-ackermann"),
|
|
idempotency_key="start-disabled",
|
|
)
|
|
assert failure.value.status_code == 403
|
|
|
|
enabled = build_polygon_router(
|
|
root_provider=lambda: None,
|
|
worker_provider=lambda: worker,
|
|
control_provider=lambda: True,
|
|
commit_provider=lambda: "d" * 40,
|
|
)
|
|
running = _endpoint(enabled, "/api/v1/polygon/worker/runs", "POST")(
|
|
request=StartStockRoverRequest(scenario_id="stock-rover-ackermann"),
|
|
idempotency_key="start-001",
|
|
)
|
|
assert running["active_run_id"].startswith("s1c-ddddddd-")
|
|
live = _endpoint(enabled, "/api/v1/polygon/worker/live", "GET")()
|
|
assert live["run_id"] == running["active_run_id"]
|
|
assert live["source"]["quality"] == "diagnostic"
|
|
command = _endpoint(
|
|
enabled,
|
|
"/api/v1/polygon/worker/runs/{run_id}/commands",
|
|
"POST",
|
|
)(
|
|
run_id=running["active_run_id"],
|
|
request=AckermannCommandRequest(speed_mps=1.0, steering_normalized=-0.25),
|
|
idempotency_key="command-001",
|
|
)
|
|
assert command["authority_scope"] == "virtual-only"
|
|
assert command["delivery"]["controller_ready"] is True
|
|
stopped = _endpoint(enabled, "/api/v1/polygon/worker/runs/{run_id}/stop", "POST")(
|
|
run_id=running["active_run_id"],
|
|
idempotency_key="stop-001",
|
|
)
|
|
assert stopped["active_run_id"] is None
|
|
assert worker.calls == [
|
|
("start", "start-001"),
|
|
("command", "command-001"),
|
|
("stop", "stop-001"),
|
|
]
|
|
|
|
|
|
def test_polygon_worker_command_rejects_values_outside_lab_envelope() -> None:
|
|
with pytest.raises(ValueError):
|
|
AckermannCommandRequest(speed_mps=1.51, steering_normalized=0)
|
|
with pytest.raises(ValueError):
|
|
AckermannCommandRequest(speed_mps=0, steering_normalized=-1.01)
|
|
|
|
|
|
def test_polygon_worker_status_is_explicitly_unavailable_when_not_registered() -> None:
|
|
router = build_polygon_router(
|
|
root_provider=lambda: None,
|
|
worker_provider=lambda: None,
|
|
)
|
|
status = _endpoint(router, "/api/v1/polygon/worker", "GET")()
|
|
assert status["schema_version"] == "missioncore.simulation-worker-status/v2"
|
|
assert status["available"] is False
|
|
assert status["control_available"] is False
|
|
assert status["provider_profile"] is None
|
|
assert status["authority"]["scope"] == "virtual-only"
|