Files
Cognitive_Agent/tests/integration/test_seat_frontend_input_coverage.py

230 lines
9.8 KiB
Python

from __future__ import annotations
import json
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.infrastructure.can.hardware import (
HardwareSafetyError,
SeatHardwareSettings,
VehicleCanSettings,
)
SUITE_PATH = REPOSITORY_ROOT / "tests" / "fixtures" / "seat_frontend_hil_suite.json"
EXPECTED_CAPABILITIES = {
"heating": "seat.set_heating",
"ventilation": "seat.set_ventilation",
"longitudinal": "seat.set_longitudinal_position",
"cushion_tilt": "seat.set_cushion_tilt",
"backrest_recline": "seat.set_backrest_recline",
"legrest_height": "seat.set_legrest_height",
}
def expand_entries(entries: list[dict], group: str | None = None) -> list[dict]:
expanded = []
for entry in entries:
base = {key: value for key, value in entry.items() if key != "variants"}
if group is not None:
base["group"] = group
variants = entry.get("variants")
if not variants:
expanded.append(base)
continue
expanded.extend({**base, **variant} for variant in variants)
return expanded
class SeatFrontendInputCoverageTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.temporary_directory = tempfile.TemporaryDirectory()
with patch.dict(
os.environ,
{
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_MODE_PROFILE_ROOT": str(
Path(cls.temporary_directory.name) / "mode-profiles"
),
},
clear=False,
):
cls.service = SeatDemoService(REPOSITORY_ROOT)
cls.service.artifact_root = Path(cls.temporary_directory.name)
cls.suite = json.loads(SUITE_PATH.read_text(encoding="utf-8"))
cls.positive_cases = expand_entries(cls.suite["cases"]) + expand_entries(
cls.suite["mode_cases"], "mode"
)
cls.model_cases = expand_entries(cls.suite.get("model_cases", []), "model")
cls.rejection_cases = expand_entries(cls.suite["rejection_cases"], "safety")
@classmethod
def tearDownClass(cls) -> None:
cls.temporary_directory.cleanup()
def test_suite_has_broad_equivalence_and_safety_coverage(self) -> None:
self.assertGreaterEqual(len(self.positive_cases), 160)
self.assertGreaterEqual(len(self.model_cases), 4)
self.assertGreaterEqual(len(self.rejection_cases), 15)
self.assertEqual(
{case["group"] for case in self.positive_cases},
{"thermal", "motion", "combined", "mode"},
)
self.assertEqual(
{
case["mode_id"]
for case in self.positive_cases
if case["group"] == "mode" and case.get("mode_id")
},
{
"easy_entry_exit",
"office",
"queen_passenger_driving",
"baby_nap",
"cruise_sofa",
"meditation_space",
"leisure_lounger",
"calf_decompression_float",
"electric_beach_chair",
"nap",
"zero_gravity",
"rocking_horse",
"dynamic_cradle",
},
)
def test_all_expanded_case_ids_and_utterances_are_unique(self) -> None:
cases = [*self.positive_cases, *self.model_cases, *self.rejection_cases]
self.assertEqual(len({case["id"] for case in cases}), len(cases))
self.assertEqual(len({case["utterance"] for case in cases}), len(cases))
def test_model_cases_require_gpt56_and_define_physical_targets(self) -> None:
for case in self.model_cases:
with self.subTest(case=case["id"]):
self.assertEqual(case["provider"], "openai")
self.assertEqual(
case["expected_provider_prefix"], "openai:gpt-5.6-sol"
)
self.assertEqual(
set(case["expected"]),
{
"longitudinal",
"cushion_tilt",
"backrest_recline",
"legrest_height",
},
)
def test_every_positive_utterance_generates_the_expected_dry_run(self) -> None:
for case in self.positive_cases:
with self.subTest(case=case["id"], utterance=case["utterance"]):
result = self.service.process(
case["utterance"],
provider=case.get("provider", "local"),
conversation_id="conv-coverage-positive-sequence",
execution_mode="dry_run",
vehicle_id=self.suite["vehicle_id"],
)
self.assertEqual(result["status"], "succeeded")
if case.get("mode_id"):
self.assertIn(
case["mode_id"],
{item["id"] for item in result["matched_modes"]},
)
seat_config = result["function_configs"].get("seat") or result[
"function_config"
]
self.assertIsNotNone(seat_config)
operations = seat_config["spec"]["operations"]
by_capability = {item["capability"]: item for item in operations}
passenger = result["actuator_state"]["passenger"]
for field, expected in case["expected"].items():
capability = EXPECTED_CAPABILITIES[field]
self.assertIn(capability, by_capability)
operation = by_capability[capability]
self.assertEqual(operation["target"]["instance"], "passenger")
self.assertEqual(operation["target"]["zone"], "front_right")
if field in {"heating", "ventilation"}:
if expected == 0:
self.assertEqual(operation["parameters"]["state"], "off")
else:
self.assertEqual(passenger[field]["level"], expected)
else:
self.assertEqual(operation["parameters"]["position_pct"], expected)
self.assertEqual(
passenger["position"][field]["position_pct"], expected
)
expected_motion = case.get("expected_motion")
if expected_motion:
capability = EXPECTED_CAPABILITIES[expected_motion["axis"]]
operation = by_capability[capability]
self.assertIn("motion", operation)
for field, expected in expected_motion.items():
if field != "axis":
self.assertEqual(operation["motion"][field], expected)
expected_dynamic_mode = case.get("expected_dynamic_mode")
if expected_dynamic_mode:
capability = (
"seat.stop_dynamic_mode"
if expected_dynamic_mode["mode_id"] == 0
else "seat.start_dynamic_mode"
)
self.assertIn(capability, by_capability)
if expected_dynamic_mode["mode_id"]:
self.assertEqual(
by_capability[capability]["parameters"]["mode_id"],
expected_dynamic_mode["mode_id"],
)
def test_every_rejection_utterance_stops_before_execution(self) -> None:
for case in self.rejection_cases:
with self.subTest(case=case["id"], utterance=case["utterance"]):
result = self.service.process(
case["utterance"],
provider=case.get("provider", "local"),
conversation_id=f"conv-coverage-{case['id'].lower()}",
execution_mode="dry_run",
vehicle_id=self.suite["vehicle_id"],
)
self.assertEqual(result["status"], case["expected_status"])
self.assertIsNone(result["function_config"])
self.assertEqual(result["function_configs"], {})
self.assertIsNone(result["execution_report"])
def test_mixed_ch1_driver_and_ch0_passenger_hardware_scope_fails_closed(self) -> None:
utterances = (
"打开主驾和副驾座椅通风",
"seat driver vent low, seat passenger vent low",
)
for index, utterance in enumerate(utterances):
with self.subTest(utterance=utterance):
with (
patch.object(self.service, "_interpret") as interpret,
patch.object(SeatHardwareSettings, "authorize") as seat_auth,
patch.object(VehicleCanSettings, "authorize") as vehicle_auth,
patch.object(VehicleCanSettings, "bridge") as bridge,
):
with self.assertRaisesRegex(
HardwareSafetyError, "mixed CH1.*CH0"
):
self.service.process(
utterance,
provider="local",
conversation_id=f"conv-mixed-scope-{index}",
execution_mode="hardware",
vehicle_id=self.suite["vehicle_id"],
)
interpret.assert_not_called()
seat_auth.assert_not_called()
vehicle_auth.assert_not_called()
bridge.assert_not_called()
if __name__ == "__main__":
unittest.main()