feat(simulation): add provider-neutral worker profile
This commit is contained in:
@@ -18,6 +18,7 @@ from k1link.simulation import (
|
||||
RunKind,
|
||||
RunState,
|
||||
)
|
||||
from k1link.simulation.stock_rover import STOCK_ROVER_PROVIDER_PROFILE
|
||||
from k1link.web.polygon_api import (
|
||||
AckermannCommandRequest,
|
||||
StartStockRoverRequest,
|
||||
@@ -256,7 +257,7 @@ class _FakeWorkerGateway:
|
||||
self.calls.append(("command", idempotency_key))
|
||||
assert run_id == self.active_run_id
|
||||
return {
|
||||
"schema_version": "missioncore.command-acceptance/v1",
|
||||
"schema_version": "missioncore.command-acceptance/v2",
|
||||
"run_id": run_id,
|
||||
"command_id": "cmd-test",
|
||||
"sequence": 1,
|
||||
@@ -266,17 +267,18 @@ class _FakeWorkerGateway:
|
||||
"steering_normalized": steering_normalized,
|
||||
"authority_scope": "virtual-only",
|
||||
"delivery": {
|
||||
"provider": "px4-ros2-offboard",
|
||||
"mode": "speed-steering",
|
||||
"armed": True,
|
||||
"offboard": True,
|
||||
"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/v1",
|
||||
"schema_version": "missioncore.simulation-worker-status/v2",
|
||||
"worker_id": "mission-gpu-s1",
|
||||
"transport": "unix",
|
||||
"mode": "simulation",
|
||||
@@ -284,7 +286,10 @@ class _FakeWorkerGateway:
|
||||
"control_available": True,
|
||||
"active_run_id": self.active_run_id,
|
||||
"run_state": "running" if self.active_run_id else None,
|
||||
"provider_ids": ["px4-gazebo-stock-rover"] if self.active_run_id else [],
|
||||
"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",
|
||||
@@ -340,7 +345,7 @@ def test_polygon_worker_api_fails_closed_then_proxies_virtual_only_lifecycle() -
|
||||
idempotency_key="command-001",
|
||||
)
|
||||
assert command["authority_scope"] == "virtual-only"
|
||||
assert command["delivery"]["offboard"] is True
|
||||
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",
|
||||
@@ -366,7 +371,8 @@ def test_polygon_worker_status_is_explicitly_unavailable_when_not_registered() -
|
||||
worker_provider=lambda: None,
|
||||
)
|
||||
status = _endpoint(router, "/api/v1/polygon/worker", "GET")()
|
||||
assert status["schema_version"] == "missioncore.simulation-worker-status/v1"
|
||||
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"
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.simulation.contracts import ControlProfile
|
||||
from k1link.simulation.provider_contract import (
|
||||
ProviderRole,
|
||||
SimulationClockDescriptor,
|
||||
SimulationProviderContractError,
|
||||
SimulationProviderDescriptor,
|
||||
SimulationProviderProfile,
|
||||
)
|
||||
from k1link.simulation.stock_rover import STOCK_ROVER_PROVIDER_PROFILE
|
||||
|
||||
|
||||
def _unreal_profile() -> SimulationProviderProfile:
|
||||
return SimulationProviderProfile(
|
||||
profile_id="unreal-native-rover-v1",
|
||||
providers=(
|
||||
SimulationProviderDescriptor(
|
||||
provider_id="unreal-native",
|
||||
roles=(
|
||||
ProviderRole.WORLD,
|
||||
ProviderRole.PHYSICS,
|
||||
ProviderRole.STATE,
|
||||
ProviderRole.SENSOR,
|
||||
),
|
||||
capabilities=(
|
||||
"clock.simulation",
|
||||
"state.vehicle-pose",
|
||||
"truth.ground-truth",
|
||||
"sensor.virtual",
|
||||
),
|
||||
),
|
||||
SimulationProviderDescriptor(
|
||||
provider_id="unreal-direct-control",
|
||||
roles=(ProviderRole.CONTROLLER,),
|
||||
capabilities=("command.rover-speed-steering/v1",),
|
||||
),
|
||||
),
|
||||
clock=SimulationClockDescriptor(
|
||||
provider_id="unreal-native",
|
||||
domain="unreal:fixed-step",
|
||||
),
|
||||
control_profiles=(ControlProfile.ROVER_SPEED_STEERING_V1,),
|
||||
)
|
||||
|
||||
|
||||
def test_stock_rover_provider_profile_round_trips_exactly() -> None:
|
||||
restored = SimulationProviderProfile.from_dict(STOCK_ROVER_PROVIDER_PROFILE.to_dict())
|
||||
|
||||
assert restored == STOCK_ROVER_PROVIDER_PROFILE
|
||||
assert restored.clock.domain == "gazebo:/clock"
|
||||
assert any(ProviderRole.CONTROLLER in provider.roles for provider in restored.providers)
|
||||
|
||||
|
||||
def test_provider_contract_accepts_unreal_without_changing_canonical_frames() -> None:
|
||||
profile = SimulationProviderProfile.from_dict(_unreal_profile().to_dict())
|
||||
|
||||
assert profile.clock.domain == "unreal:fixed-step"
|
||||
assert profile.world_frame == "map_enu"
|
||||
assert profile.body_frame == "base_link_flu"
|
||||
|
||||
|
||||
def test_provider_contract_rejects_missing_clock_capability() -> None:
|
||||
profile = _unreal_profile()
|
||||
world = profile.providers[0]
|
||||
|
||||
with pytest.raises(SimulationProviderContractError, match="clock.simulation"):
|
||||
replace(
|
||||
profile,
|
||||
providers=(
|
||||
replace(
|
||||
world,
|
||||
capabilities=tuple(
|
||||
capability
|
||||
for capability in world.capabilities
|
||||
if capability != "clock.simulation"
|
||||
),
|
||||
),
|
||||
profile.providers[1],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_provider_contract_rejects_undeclared_command_capability() -> None:
|
||||
profile = _unreal_profile()
|
||||
controller = profile.providers[1]
|
||||
|
||||
with pytest.raises(SimulationProviderContractError, match="control profiles"):
|
||||
replace(
|
||||
profile,
|
||||
providers=(
|
||||
profile.providers[0],
|
||||
replace(controller, capabilities=("transport.custom",)),
|
||||
),
|
||||
)
|
||||
@@ -89,6 +89,14 @@ def _start(store: QualificationRunStore, run_id: str = "run-s1-001") -> Qualific
|
||||
)
|
||||
|
||||
|
||||
def test_closed_loop_run_accepts_provider_neutral_simulation_clock() -> None:
|
||||
run = replace(_run(), clock_domain="unreal:fixed-step")
|
||||
|
||||
assert run.clock_domain == "unreal:fixed-step"
|
||||
with pytest.raises(SimulationContractError, match="clock domain"):
|
||||
replace(run, clock_domain="../../host-clock")
|
||||
|
||||
|
||||
def test_run_manifest_is_immutable_and_runtime_replays_from_events(tmp_path: Path) -> None:
|
||||
store = QualificationRunStore(tmp_path / "runs")
|
||||
created = store.create(_run())
|
||||
|
||||
@@ -144,8 +144,9 @@ def test_worker_agent_journals_and_idempotently_delivers_virtual_command(
|
||||
|
||||
assert accepted == repeated
|
||||
assert accepted["authority_scope"] == "virtual-only"
|
||||
assert accepted["delivery"]["provider"] == "px4-ros2-offboard"
|
||||
assert accepted["delivery"]["armed"] is True
|
||||
assert accepted["delivery"]["provider_id"] == "px4-ros2-offboard"
|
||||
assert accepted["delivery"]["controller_ready"] is True
|
||||
assert accepted["delivery"]["diagnostics"]["armed"] is True
|
||||
commands = agent.store.list_commands("s1c-command-test")
|
||||
assert len(commands) == 1
|
||||
assert commands[0].valid_until_sim_ns - commands[0].issued_at_sim_ns == 250_000_000
|
||||
|
||||
@@ -9,6 +9,7 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.simulation.stock_rover import STOCK_ROVER_PROVIDER_PROFILE
|
||||
from k1link.simulation.worker_gateway import (
|
||||
REQUEST_SCHEMA,
|
||||
RESPONSE_SCHEMA,
|
||||
@@ -20,7 +21,7 @@ from k1link.simulation.worker_gateway import (
|
||||
|
||||
def _status() -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": "missioncore.simulation-worker-status/v1",
|
||||
"schema_version": "missioncore.simulation-worker-status/v2",
|
||||
"worker_id": "mission-gpu-s1",
|
||||
"transport": "unix",
|
||||
"mode": "simulation",
|
||||
@@ -28,7 +29,8 @@ def _status() -> dict[str, Any]:
|
||||
"control_available": True,
|
||||
"active_run_id": None,
|
||||
"run_state": None,
|
||||
"provider_ids": [],
|
||||
"active_provider_ids": [],
|
||||
"provider_profile": STOCK_ROVER_PROVIDER_PROFILE.to_dict(),
|
||||
"isolation": {
|
||||
"network": "loopback-only-netns",
|
||||
"process_identity": "missioncore",
|
||||
@@ -44,7 +46,7 @@ def _status() -> dict[str, Any]:
|
||||
|
||||
def _command_acceptance() -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": "missioncore.command-acceptance/v1",
|
||||
"schema_version": "missioncore.command-acceptance/v2",
|
||||
"run_id": "s1c-command-test",
|
||||
"command_id": "cmd-001",
|
||||
"sequence": 1,
|
||||
@@ -54,11 +56,40 @@ def _command_acceptance() -> dict[str, Any]:
|
||||
"steering_normalized": -0.25,
|
||||
"authority_scope": "virtual-only",
|
||||
"delivery": {
|
||||
"provider": "px4-ros2-offboard",
|
||||
"mode": "speed-steering",
|
||||
"armed": True,
|
||||
"offboard": True,
|
||||
"provider_id": "unreal-direct-control",
|
||||
"control_profile": "rover-speed-steering/v1",
|
||||
"accepted": True,
|
||||
"controller_ready": True,
|
||||
"ttl_expired_count": 0,
|
||||
"diagnostics": {"armed": True, "offboard": True},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _vehicle_state() -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": "missioncore.vehicle-state/v1",
|
||||
"run_id": "s1c-command-test",
|
||||
"sequence": 1,
|
||||
"observed_at_utc": "2026-07-25T10:00:00Z",
|
||||
"host_monotonic_ns": 1_000_000,
|
||||
"sim_time_ns": 2_000_000,
|
||||
"frame_id": "map_enu",
|
||||
"child_frame_id": "base_link_flu",
|
||||
"pose": {
|
||||
"position_m": {"x": 1.0, "y": 2.0, "z": 0.0},
|
||||
"orientation_xyzw": {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0},
|
||||
},
|
||||
"source": {
|
||||
"provider": "unreal-native",
|
||||
"topic": "missioncore/vehicle-state",
|
||||
"signal": "ground-truth",
|
||||
"quality": "diagnostic",
|
||||
},
|
||||
"safety": {
|
||||
"scope": "virtual-only",
|
||||
"actuator_authority": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -158,7 +189,8 @@ def test_unix_worker_gateway_sends_bounded_virtual_rover_command() -> None:
|
||||
)
|
||||
thread.join(timeout=2)
|
||||
|
||||
assert accepted["delivery"]["offboard"] is True
|
||||
assert accepted["delivery"]["provider_id"] == "unreal-direct-control"
|
||||
assert accepted["delivery"]["controller_ready"] is True
|
||||
assert requests[0]["operation"] == "command"
|
||||
assert requests[0]["payload"] == {
|
||||
"run_id": "s1c-command-test",
|
||||
@@ -168,3 +200,18 @@ def test_unix_worker_gateway_sends_bounded_virtual_rover_command() -> None:
|
||||
}
|
||||
finally:
|
||||
socket_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_unix_worker_gateway_accepts_provider_neutral_diagnostic_state() -> None:
|
||||
socket_path = Path(f"/tmp/mc-{uuid4().hex}.sock")
|
||||
thread, _ = _serve_once(socket_path, _vehicle_state())
|
||||
gateway = UnixSocketWorkerGateway(socket_path)
|
||||
|
||||
try:
|
||||
state = gateway.live()
|
||||
thread.join(timeout=2)
|
||||
|
||||
assert state["source"]["provider"] == "unreal-native"
|
||||
assert state["frame_id"] == "map_enu"
|
||||
finally:
|
||||
socket_path.unlink(missing_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user