cockpit-agent/tests/test_planner.py
2026-08-24 16:49:44 +08:00

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()