335 lines
10 KiB
Python
335 lines
10 KiB
Python
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()
|