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