import unittest from cockpit_agent.common.enums import ( ControlType, Domain, Interaction, Operation, SelectionState, SliderOrientation, State, Unit, Zone, ) from cockpit_agent.intent.schemas import SemanticAction from cockpit_agent.perception.schemas import UIState from cockpit_agent.planning.planner import Planner, PlanningError def temperature_action(value: int) -> SemanticAction: return SemanticAction( domain=Domain.CLIMATE, function="temperature", zone=Zone.DRIVER, operation=Operation.SET, state=None, value=value, unit=Unit.CELSIUS, ) def ui_state( control_type: ControlType | None, *, function: str = "temperature", zone: Zone | None = Zone.DRIVER, current_value=None, step_value=None, current_state=None, min_value=None, max_value=None, options=None, selection_state=None, track_visible=None, knob_visible=None, orientation=None, ) -> UIState: return UIState( function=function, zone=zone, control_type=control_type, current_value=current_value, step_value=step_value, current_state=current_state, min_value=min_value, max_value=max_value, options=options, selection_state=selection_state, track_visible=track_visible, knob_visible=knob_visible, orientation=orientation, ) def recirculation_action() -> SemanticAction: return SemanticAction( domain=Domain.CLIMATE, function="air_recirculation", zone=Zone.ALL, operation=Operation.SET, state=State.ON, value=None, unit=None, ) class PlannerTest(unittest.TestCase): def setUp(self) -> None: self.planner = Planner() def test_stepper_decreases_three_times(self) -> None: plan = self.planner.plan( temperature_action(23), ui_state(ControlType.STEPPER, current_value=26, step_value=1), ) self.assertEqual(plan.interaction, Interaction.TAP) self.assertEqual(plan.function, "temperature_decrease") self.assertEqual(plan.zone, Zone.DRIVER) self.assertEqual(plan.repeat, 3) self.assertEqual(plan.target_value, 23) self.assertNotIn("target", plan.to_dict()) def test_stepper_increases_three_times(self) -> None: plan = self.planner.plan( temperature_action(23), ui_state(ControlType.STEPPER, current_value=20, step_value=1), ) self.assertEqual(plan.function, "temperature_increase") self.assertEqual(plan.repeat, 3) def test_stepper_without_step_is_rejected(self) -> None: with self.assertRaisesRegex(PlanningError, "refusing to guess"): self.planner.plan( temperature_action(23), ui_state(ControlType.STEPPER, current_value=26), ) def test_toggle(self) -> None: plan = self.planner.plan( recirculation_action(), ui_state( ControlType.TOGGLE, function="air_recirculation", zone=Zone.ALL, current_state=State.OFF, ), ) self.assertEqual(plan.interaction, Interaction.TAP) self.assertEqual(plan.function, "air_recirculation") def test_icon_button_with_unknown_state_proposes_tap(self) -> None: plan = self.planner.plan( recirculation_action(), ui_state( ControlType.ICON_BUTTON, function="air_recirculation", zone=Zone.ALL, current_state=None, ), ) self.assertEqual(plan.interaction, Interaction.TAP) self.assertEqual(plan.function, "air_recirculation") def test_icon_button_cannot_set_numeric_target(self) -> None: with self.assertRaisesRegex(PlanningError, "numeric target"): self.planner.plan( temperature_action(23), ui_state(ControlType.ICON_BUTTON, current_state=None), ) def test_slider(self) -> None: intent = SemanticAction( domain=Domain.BODY, function="tailgate_max_opening", zone=None, operation=Operation.SET, state=None, value=80, unit=Unit.PERCENT, ) plan = self.planner.plan( intent, ui_state( ControlType.SLIDER, function="tailgate_max_opening", zone=None, current_value=90, min_value=50, max_value=100, orientation=SliderOrientation.HORIZONTAL, ), ) self.assertEqual(plan.interaction, Interaction.DRAG) self.assertEqual(plan.function, "tailgate_max_opening") self.assertAlmostEqual(plan.normalized_target or 0, 0.6) def test_slider_allows_unknown_current_value(self) -> None: intent = SemanticAction( domain=Domain.BODY, function="tailgate_max_opening", zone=None, operation=Operation.SET, state=None, value=80, unit=Unit.PERCENT, ) plan = self.planner.plan( intent, ui_state( ControlType.SLIDER, function="tailgate_max_opening", zone=None, current_value=None, min_value=50, max_value=100, orientation=SliderOrientation.HORIZONTAL, ), ) self.assertEqual(plan.interaction, Interaction.DRAG) self.assertAlmostEqual(plan.normalized_target or 0, 0.6) def test_slider_requires_observed_horizontal_orientation(self) -> None: intent = SemanticAction( domain=Domain.BODY, function="tailgate_max_opening", zone=None, operation=Operation.SET, state=None, value=80, unit=Unit.PERCENT, ) with self.assertRaisesRegex(PlanningError, "horizontal orientation"): self.planner.plan( intent, ui_state( ControlType.SLIDER, function="tailgate_max_opening", zone=None, current_value=None, min_value=50, max_value=100, orientation=None, ), ) def test_segmented_control(self) -> None: intent = SemanticAction( domain=Domain.BODY, function="unlock_mode", zone=None, operation=Operation.SET, state=None, value="all_doors", unit=None, ) plan = self.planner.plan( intent, ui_state( ControlType.SEGMENTED_CONTROL, function="unlock_mode", zone=None, current_value="driver_door", options=("driver_door", "all_doors"), selection_state=SelectionState.SELECTED, ), ) self.assertEqual(plan.interaction, Interaction.TAP) self.assertEqual(plan.function, "unlock_mode") self.assertEqual(plan.option, "all_doors") def test_noop(self) -> None: plan = self.planner.plan( recirculation_action(), ui_state( ControlType.TOGGLE, function="air_recirculation", zone=Zone.ALL, current_state=State.ON, ), ) self.assertEqual(plan.interaction, Interaction.NOOP) self.assertEqual(plan.reason, "already_in_target_state") def test_radio_option_already_selected_is_noop(self) -> None: intent = SemanticAction( domain=Domain.BODY, function="unlock_mode", zone=None, operation=Operation.SET, state=None, value="all_doors", unit=None, ) plan = self.planner.plan( intent, ui_state( ControlType.RADIO, function="unlock_mode", zone=None, current_value="all_doors", options=("driver_door", "all_doors"), selection_state=SelectionState.SELECTED, ), ) self.assertEqual(plan.interaction, Interaction.NOOP) self.assertEqual(plan.reason, "already_in_target_state") def test_matching_option_with_unknown_selection_proposes_tap(self) -> None: intent = SemanticAction( domain=Domain.BODY, function="unlock_mode", zone=None, operation=Operation.SET, state=None, value="all_doors", unit=None, ) plan = self.planner.plan( intent, ui_state( ControlType.SEGMENTED_CONTROL, function="unlock_mode", zone=None, current_value="all_doors", options=("driver_door", "all_doors"), selection_state=None, ), ) self.assertEqual(plan.interaction, Interaction.TAP) self.assertEqual(plan.option, "all_doors") def test_selected_target_noops_even_when_options_are_partial(self) -> None: intent = SemanticAction( domain=Domain.BODY, function="unlock_mode", zone=None, operation=Operation.SET, state=None, value="all_doors", unit=None, ) plan = self.planner.plan( intent, ui_state( ControlType.SEGMENTED_CONTROL, function="unlock_mode", zone=None, current_value="all_doors", options=("driver_door",), selection_state=SelectionState.SELECTED, ), ) self.assertEqual(plan.interaction, Interaction.NOOP) self.assertEqual(plan.reason, "already_in_target_state") def test_unknown_control_type_is_rejected(self) -> None: with self.assertRaisesRegex(PlanningError, "control_type"): self.planner.plan( temperature_action(23), ui_state(None), ) if __name__ == "__main__": unittest.main()