233 lines
7.8 KiB
Python
233 lines
7.8 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from cognitive_agent.application.direct_can_control import build_direct_can_intent
|
|
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
|
|
|
|
|
def test_driver_device_batch_keeps_each_exact_control_independent() -> None:
|
|
commands = [
|
|
"seat driver vent low",
|
|
"seat driver heat off",
|
|
"seat driver massage mode stretch",
|
|
"seat driver massage strength strong",
|
|
]
|
|
|
|
normalized, parsed = build_direct_can_intent(commands)
|
|
|
|
assert normalized == commands
|
|
assert [item["capability"] for item in parsed["native_can_actions"]] == [
|
|
"seat.set_driver_ventilation_native",
|
|
"seat.set_driver_heating_native",
|
|
"seat.set_driver_massage_native",
|
|
"seat.set_driver_massage_native",
|
|
]
|
|
assert [item["parameters"] for item in parsed["native_can_actions"]] == [
|
|
{"state": "on", "level": 2},
|
|
{"state": "off"},
|
|
{"action": "mode", "mode": "stretch"},
|
|
{"action": "strength", "strength": 3},
|
|
]
|
|
assert {
|
|
item["parse_source"] for item in parsed["native_can_actions"]
|
|
} == {"direct_control"}
|
|
|
|
|
|
def test_passenger_device_batch_builds_can0_position_action() -> None:
|
|
normalized, parsed = build_direct_can_intent(
|
|
["seat passenger backrest position 36"]
|
|
)
|
|
|
|
assert normalized == ["seat passenger backrest position 36"]
|
|
assert parsed["native_can_actions"] == []
|
|
assert parsed["actions"] == [
|
|
{
|
|
"target": "passenger",
|
|
"action_type": "position",
|
|
"axis": "backrest_recline",
|
|
"position_pct": 36,
|
|
"parse_source": "direct_control",
|
|
"source_span": "seat passenger backrest position 36",
|
|
}
|
|
]
|
|
|
|
|
|
def test_passenger_can0_canonical_absolute_relative_thermal_and_dynamic() -> None:
|
|
context = {
|
|
"devices": {
|
|
"passenger": {
|
|
"position": {
|
|
"longitudinal": {"position_pct": 40},
|
|
"cushion_tilt": {"position_pct": 50},
|
|
"backrest_recline": {"position_pct": 60},
|
|
"legrest_height": {"position_pct": 70},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
commands = [
|
|
"seat passenger horizontal position 35",
|
|
"seat passenger basin relative up 10",
|
|
"seat passenger vent medium",
|
|
"seat passenger dynamic rocking-horse",
|
|
"seat passenger dynamic stop",
|
|
]
|
|
|
|
normalized, parsed = build_direct_can_intent(commands, context)
|
|
|
|
assert normalized == commands
|
|
assert parsed["detected_domains"] == ["seat"]
|
|
assert parsed["actions"][0]["axis"] == "longitudinal"
|
|
assert parsed["actions"][0]["position_pct"] == 35
|
|
assert parsed["actions"][1]["axis"] == "cushion_tilt"
|
|
assert parsed["actions"][1]["position_pct"] == 40
|
|
assert parsed["actions"][1]["motion_intent"] == {
|
|
"mode": "relative",
|
|
"direction": "decrease",
|
|
"start_position_pct": 50,
|
|
"delta_position_pct": 10,
|
|
}
|
|
assert parsed["actions"][2]["function"] == "ventilation"
|
|
assert parsed["actions"][2]["requested_level"] == 2
|
|
assert parsed["actions"][3]["action_type"] == "dynamic_mode"
|
|
assert parsed["actions"][3]["mode_id"] == 1
|
|
assert parsed["actions"][4]["action_type"] == "dynamic_mode_stop"
|
|
|
|
|
|
def test_passenger_can0_relative_command_requires_current_position() -> None:
|
|
with pytest.raises(ValueError, match="requires a valid current"):
|
|
build_direct_can_intent(
|
|
["seat passenger horizontal relative forward 10"],
|
|
{"devices": {"passenger": {"position": {}}}},
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"commands",
|
|
(
|
|
[],
|
|
["seat driver vent medium"],
|
|
["seat passenger height position 101"],
|
|
["headlight low-beam on"],
|
|
),
|
|
)
|
|
def test_direct_batch_rejects_non_whitelisted_commands(commands: list[str]) -> None:
|
|
with pytest.raises(ValueError):
|
|
build_direct_can_intent(commands)
|
|
|
|
|
|
def test_service_direct_batch_bypasses_natural_language_interpreter() -> None:
|
|
with tempfile.TemporaryDirectory() as directory, patch.dict(
|
|
os.environ,
|
|
{
|
|
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
|
|
},
|
|
clear=False,
|
|
):
|
|
service = SeatDemoService(REPOSITORY_ROOT)
|
|
service.artifact_root = Path(directory) / "artifacts"
|
|
with patch.object(
|
|
service,
|
|
"_interpret",
|
|
side_effect=AssertionError("natural-language parsing must not run"),
|
|
):
|
|
result = service.process_can_commands(
|
|
[
|
|
"ac on",
|
|
"ac temp driver 24",
|
|
"ac fan 2",
|
|
"ac cycle internal",
|
|
],
|
|
execution_mode="dry_run",
|
|
control_transport="wireless_mqtt",
|
|
)
|
|
|
|
assert result["status"] == "succeeded"
|
|
assert result["provider_used"] == "direct_can_control"
|
|
assert result["control_transport"] == "wireless_mqtt"
|
|
assert result["can_commands"] == [
|
|
"ac on",
|
|
"ac temp driver 24",
|
|
"ac fan 2",
|
|
"ac cycle internal",
|
|
]
|
|
operations = [
|
|
operation
|
|
for configuration in result["function_configs"].values()
|
|
for operation in configuration["spec"]["operations"]
|
|
]
|
|
assert [item["capability"] for item in operations] == [
|
|
"climate.set_power",
|
|
"climate.set_temperature",
|
|
"climate.set_fan_level",
|
|
"climate.set_cycle_mode",
|
|
]
|
|
assert {item["parse_source"] for item in operations} == {"direct_control"}
|
|
|
|
|
|
def test_service_direct_batch_combines_can0_and_can1_semantics() -> None:
|
|
with tempfile.TemporaryDirectory() as directory, patch.dict(
|
|
os.environ,
|
|
{
|
|
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
|
|
},
|
|
clear=False,
|
|
):
|
|
service = SeatDemoService(REPOSITORY_ROOT)
|
|
service.artifact_root = Path(directory) / "artifacts"
|
|
result = service.process_can_commands(
|
|
[
|
|
"seat passenger basin relative up 10",
|
|
"seat passenger vent medium",
|
|
"seat passenger dynamic rocking-horse",
|
|
"ac fan 2",
|
|
"light ambient on",
|
|
],
|
|
conversation_id="conv-direct-can0-can1",
|
|
execution_mode="dry_run",
|
|
)
|
|
|
|
assert result["status"] == "succeeded"
|
|
assert result["can_commands"] == [
|
|
"seat passenger basin relative up 10",
|
|
"seat passenger vent medium",
|
|
"seat passenger dynamic rocking-horse",
|
|
"ac fan 2",
|
|
"light ambient on",
|
|
]
|
|
operations = [
|
|
operation
|
|
for configuration in result["function_configs"].values()
|
|
for operation in configuration["spec"]["operations"]
|
|
]
|
|
by_capability = {item["capability"]: item for item in operations}
|
|
assert by_capability["seat.set_cushion_tilt"]["parameters"] == {
|
|
"position_pct": 40
|
|
}
|
|
assert by_capability["seat.set_cushion_tilt"]["motion"] == {
|
|
"mode": "relative",
|
|
"direction": "decrease",
|
|
"start_position_pct": 50,
|
|
"delta_position_pct": 10,
|
|
}
|
|
assert by_capability["seat.start_dynamic_mode"]["parameters"] == {
|
|
"mode_id": 1
|
|
}
|
|
assert by_capability["seat.set_ventilation"]["parameters"] == {
|
|
"state": "on",
|
|
"level": 2,
|
|
}
|
|
assert by_capability["climate.set_fan_level"]["parameters"] == {"level": 2}
|
|
assert by_capability["ambient_light.set_power"]["parameters"] == {
|
|
"state": "on"
|
|
}
|
|
assert {item["parse_source"] for item in operations} == {"direct_control"}
|