init
This commit is contained in:
Executable
+4
@@ -0,0 +1,4 @@
|
||||
# Seat Tests
|
||||
|
||||
`unit/` 覆盖姿态参数和运动学转换,`contract/` 覆盖配置与插件契约,`integration/` 使用模拟 CAN Gateway 覆盖命令、回读、互锁和超时。实车范围需与平台安全标定分别测试,不能只依赖 JSON Schema 的宽范围。
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
+104
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from cognitive_agent.runtime.contracts import ContractValidator
|
||||
from seat_domain.generator import build_function_config
|
||||
from seat_domain.intent import parse_local_intent
|
||||
from seat_domain.matrix import SeatCanMatrix
|
||||
from seat_domain.source_matrix import read_source_signals
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
DOMAIN_ROOT = ROOT / "domains" / "seat"
|
||||
|
||||
|
||||
class SeatContractTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.validator = ContractValidator(ROOT)
|
||||
|
||||
def test_generated_function_config_matches_both_schemas(self) -> None:
|
||||
parsed = parse_local_intent("打开副驾座椅加热")
|
||||
config = build_function_config(parsed, "req-contract-001", "local_rules")
|
||||
|
||||
self.assertEqual(self.validator.validate(config), [])
|
||||
|
||||
def test_inferred_level_function_config_matches_both_schemas(self) -> None:
|
||||
parsed = parse_local_intent("主驾有点热,有点闷")
|
||||
config = build_function_config(parsed, "req-contract-002", "local_rules")
|
||||
|
||||
self.assertEqual(config["spec"]["operations"][0]["parameters"]["level"], 1)
|
||||
self.assertEqual(self.validator.validate(config), [])
|
||||
|
||||
def test_motion_function_config_matches_both_schemas(self) -> None:
|
||||
parsed = {
|
||||
"detected_domains": ["seat"],
|
||||
"actions": [
|
||||
{
|
||||
"action_type": "position",
|
||||
"target": "passenger",
|
||||
"axis": "legrest_extension",
|
||||
"position_pct": 70,
|
||||
}
|
||||
],
|
||||
"deferred_requests": [],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["Extend passenger leg rest."],
|
||||
"confidence": 1.0,
|
||||
}
|
||||
|
||||
config = build_function_config(parsed, "req-contract-motion", "test")
|
||||
|
||||
self.assertEqual(config["spec"]["operations"][0]["parameters"], {"position_pct": 70})
|
||||
self.assertEqual(self.validator.validate(config), [])
|
||||
|
||||
def test_all_active_presets_match_both_schemas(self) -> None:
|
||||
for path in (DOMAIN_ROOT / "config" / "presets").glob("*.yaml"):
|
||||
with self.subTest(path=path.name):
|
||||
document = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(self.validator.validate(document), [])
|
||||
|
||||
def test_normalized_matrix_matches_source_scope(self) -> None:
|
||||
matrix = SeatCanMatrix(DOMAIN_ROOT / "can" / "seat-thermal-matrix.yaml")
|
||||
|
||||
self.assertEqual(len(matrix.data["messages"]), 6)
|
||||
self.assertEqual(sum(len(item["signals"]) for item in matrix.data["messages"]), 23)
|
||||
self.assertFalse(matrix.hardware_execution_allowed)
|
||||
|
||||
def test_synthetic_motion_matrix_is_explicitly_non_hardware(self) -> None:
|
||||
matrix = SeatCanMatrix(DOMAIN_ROOT / "can" / "seat-motion-virtual-matrix.yaml")
|
||||
|
||||
self.assertTrue(matrix.data["synthetic"])
|
||||
self.assertFalse(matrix.hardware_execution_allowed)
|
||||
self.assertEqual(len(matrix.data["messages"]), 4)
|
||||
self.assertEqual(sum(len(item["signals"]) for item in matrix.data["messages"]), 34)
|
||||
|
||||
def test_normalized_signal_layout_matches_supplied_xlsx(self) -> None:
|
||||
source = {
|
||||
item["name"]: item
|
||||
for item in read_source_signals(ROOT / "座椅部分功能CAN矩阵表.xlsx")
|
||||
}
|
||||
matrix = SeatCanMatrix(DOMAIN_ROOT / "can" / "seat-thermal-matrix.yaml")
|
||||
normalized = {
|
||||
signal["name"]: {
|
||||
"frame_id": int(message["frame_id"]),
|
||||
"start_bit": int(signal["start_bit"]),
|
||||
"bit_length": int(signal["bit_length"]),
|
||||
}
|
||||
for message in matrix.data["messages"]
|
||||
for signal in message["signals"]
|
||||
}
|
||||
|
||||
self.assertEqual(set(source), set(normalized))
|
||||
for name, layout in normalized.items():
|
||||
with self.subTest(signal=name):
|
||||
self.assertEqual(layout["frame_id"], source[name]["frame_id"])
|
||||
self.assertEqual(layout["start_bit"], source[name]["start_bit"])
|
||||
self.assertEqual(layout["bit_length"], source[name]["bit_length"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
+610
@@ -0,0 +1,610 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
|
||||
|
||||
class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.service = SeatDemoService(REPOSITORY_ROOT)
|
||||
self.temporary_directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temporary_directory.cleanup)
|
||||
self.service.artifact_root = Path(self.temporary_directory.name)
|
||||
|
||||
def test_supported_request_completes_full_dry_run(self) -> None:
|
||||
result = self.service.process(
|
||||
"打开主驾座椅通风,同时关闭副驾座椅加热", provider="local"
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(len(result["function_config"]["spec"]["operations"]), 2)
|
||||
telemetry = result["execution_report"]["spec"]["results"][0]["telemetry"]
|
||||
self.assertEqual(telemetry["command_frame"]["data_hex"], "01 80")
|
||||
self.assertTrue(Path(result["artifact_path"]).is_absolute())
|
||||
self.assertTrue(
|
||||
(self.service.artifact_root / result["request_id"] / "result.json").is_file()
|
||||
)
|
||||
for event in result["trace_events"]:
|
||||
self.assertEqual(self.service.contracts.validate(event), [])
|
||||
|
||||
def test_gear_request_is_preserved_and_simulated(self) -> None:
|
||||
result = self.service.process("主驾座椅加热开到三档", provider="local")
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
operation = result["function_config"]["spec"]["operations"][0]
|
||||
self.assertEqual(operation["parameters"]["level"], 3)
|
||||
telemetry = result["execution_report"]["spec"]["results"][0]["telemetry"]
|
||||
self.assertEqual(telemetry["effective_level"], 3)
|
||||
self.assertTrue(telemetry["level_command_simulated"])
|
||||
self.assertIn(
|
||||
"LEVEL_COMMAND_SIMULATED", {issue["code"] for issue in result["validation"]}
|
||||
)
|
||||
|
||||
def test_consecutive_feedback_uses_previous_successful_state(self) -> None:
|
||||
conversation_id = "conv-test-context"
|
||||
|
||||
first = self.service.process(
|
||||
"主驾有点热,有点闷",
|
||||
provider="local",
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
second = self.service.process(
|
||||
"主驾还有点闷",
|
||||
provider="local",
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
self.assertEqual(first["status"], "succeeded")
|
||||
self.assertEqual(first["actuator_state"]["driver"]["ventilation"]["level"], 1)
|
||||
self.assertEqual(second["status"], "succeeded")
|
||||
self.assertEqual(second["conversation_turn"], 2)
|
||||
self.assertEqual(second["actuator_state"]["driver"]["ventilation"]["level"], 2)
|
||||
operation = second["function_config"]["spec"]["operations"][0]
|
||||
self.assertEqual(operation["parameters"], {"state": "on", "level": 2})
|
||||
self.assertIn("提升到2档", "".join(second["decision_summary"]))
|
||||
|
||||
def test_four_turn_causal_feedback_sequence(self) -> None:
|
||||
conversation_id = "conv-causal-sequence"
|
||||
utterances = [
|
||||
"我有点热",
|
||||
"我有点冷",
|
||||
"我还有点冷",
|
||||
"副驾吹的我有点冷",
|
||||
]
|
||||
|
||||
first, second, third, fourth = [
|
||||
self.service.process(text, "local", conversation_id) for text in utterances
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
first["function_config"]["spec"]["operations"][0]["parameters"],
|
||||
{"state": "on", "level": 1},
|
||||
)
|
||||
second_operation = second["function_config"]["spec"]["operations"][0]
|
||||
self.assertEqual(second_operation["capability"], "seat.set_ventilation")
|
||||
self.assertEqual(second_operation["parameters"], {"state": "off"})
|
||||
third_operation = third["function_config"]["spec"]["operations"][0]
|
||||
self.assertEqual(third_operation["capability"], "seat.set_heating")
|
||||
self.assertEqual(third_operation["parameters"], {"state": "on", "level": 1})
|
||||
fourth_operation = fourth["function_config"]["spec"]["operations"][0]
|
||||
self.assertEqual(fourth_operation["target"]["instance"], "passenger")
|
||||
self.assertEqual(fourth_operation["capability"], "seat.set_ventilation")
|
||||
self.assertEqual(fourth_operation["parameters"], {"state": "off"})
|
||||
self.assertEqual(fourth["actuator_state"]["driver"]["heating"], {"state": "on", "level": 1})
|
||||
self.assertEqual(fourth["conversation_turn"], 4)
|
||||
|
||||
def test_compound_cold_and_stuffy_feedback_generates_balanced_dry_run(self) -> None:
|
||||
result = self.service.process(
|
||||
"主驾有点冷、有点闷",
|
||||
provider="local",
|
||||
conversation_id="conv-compound-comfort",
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
operations = result["function_config"]["spec"]["operations"]
|
||||
self.assertEqual(
|
||||
{operation["capability"] for operation in operations},
|
||||
{"seat.set_heating", "seat.set_ventilation"},
|
||||
)
|
||||
command = result["command_plan"]["spec"]["commands"][0]
|
||||
self.assertEqual(
|
||||
command["signals"],
|
||||
{"Driver_Seat_Heating_S": 1, "Driver_Seat_Ventilation_S": 1},
|
||||
)
|
||||
self.assertIn(
|
||||
"COMBINED_THERMAL_SIMULATED",
|
||||
{issue["code"] for issue in result["validation"]},
|
||||
)
|
||||
|
||||
def test_vehicle_scope_is_routed_away_from_seat_domain(self) -> None:
|
||||
result = self.service.process(
|
||||
"车上太闷了",
|
||||
provider="local",
|
||||
conversation_id="conv-vehicle-scope",
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "needs_confirmation")
|
||||
self.assertEqual(result["intent_ir"]["spec"]["detected_domains"], ["climate"])
|
||||
self.assertIsNone(result["function_config"])
|
||||
self.assertIsNone(result["command_plan"])
|
||||
self.assertEqual(result["actuator_state"]["passenger"]["ventilation"]["state"], "off")
|
||||
route = next(event for event in result["trace_events"] if event["stage"] == "domain_route")
|
||||
self.assertEqual(route["details"]["domains"], ["climate"])
|
||||
|
||||
def test_openai_output_is_not_rewritten_by_local_semantic_planner(self) -> None:
|
||||
model_output = {
|
||||
"detected_domains": ["seat"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_ventilation",
|
||||
"target_scope": "passenger",
|
||||
"operation": "deactivate",
|
||||
"parameters": [{"name": "state", "value": "off"}],
|
||||
"reason": "model-owned-plan",
|
||||
}
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["model-owned-plan"],
|
||||
"confidence": 0.91,
|
||||
}
|
||||
context = self.service.conversations.snapshot("conv-model-owned-plan")
|
||||
self.service.openai.api_key = "test-key"
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output) as interpret:
|
||||
parsed, provider, notice = self.service._interpret(
|
||||
"车上太闷了", "openai", context
|
||||
)
|
||||
|
||||
self.assertEqual(parsed["decision_summary"], ["model-owned-plan"])
|
||||
self.assertEqual(parsed["model_requests"], model_output["requests"])
|
||||
self.assertEqual(
|
||||
parsed["actions"],
|
||||
[
|
||||
{
|
||||
"target": "passenger",
|
||||
"function": "ventilation",
|
||||
"state": "off",
|
||||
"requested_level": None,
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertEqual(provider, f"openai:{self.service.openai.model}")
|
||||
self.assertIsNone(notice)
|
||||
interpret.assert_called_once_with(
|
||||
"车上太闷了", context, self.service.capability_context
|
||||
)
|
||||
|
||||
def test_model_climate_request_completes_domain_loop(self) -> None:
|
||||
model_output = {
|
||||
"detected_domains": ["climate"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_fan_level",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "increase",
|
||||
"parameters": [{"name": "level", "value": 3}],
|
||||
"reason": "Increase cabin airflow.",
|
||||
}
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["整车舱风量增加需求。"],
|
||||
"confidence": 0.94,
|
||||
}
|
||||
self.service.openai.api_key = "test-key"
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
||||
result = self.service.process(
|
||||
"车上太闷了",
|
||||
"openai",
|
||||
"conv-model-deferred",
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(result["model_requests"], model_output["requests"])
|
||||
self.assertEqual(result["deferred_requests"], [])
|
||||
self.assertIn("climate", result["function_configs"])
|
||||
request = result["intent_ir"]["spec"]["requests"][0]
|
||||
self.assertEqual(request["domain"], "climate")
|
||||
self.assertEqual(request["parameters"]["capability"], "climate.set_fan_level")
|
||||
|
||||
def test_climate_and_seat_requests_generate_scene(self) -> None:
|
||||
model_output = {
|
||||
"detected_domains": ["seat", "climate"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_heating",
|
||||
"target_scope": "driver",
|
||||
"operation": "activate",
|
||||
"parameters": [
|
||||
{"name": "state", "value": "on"},
|
||||
{"name": "level", "value": 1},
|
||||
],
|
||||
"reason": "Warm the driver seat.",
|
||||
},
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_fan_level",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": [{"name": "level", "value": 2}],
|
||||
"reason": "Improve cabin airflow.",
|
||||
},
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["座椅加热与整车空调风量并行执行。"],
|
||||
"confidence": 0.9,
|
||||
}
|
||||
self.service.openai.api_key = "test-key"
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
||||
result = self.service.process(
|
||||
"主驾有点冷、有点闷",
|
||||
"openai",
|
||||
"conv-model-partial",
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertIsNotNone(result["execution_report"])
|
||||
self.assertEqual(result["actuator_state"]["driver"]["heating"], {"state": "on", "level": 1})
|
||||
self.assertEqual(result["domain_state"]["climate"]["fan_level"], 2)
|
||||
self.assertEqual(set(result["function_configs"]), {"seat", "climate"})
|
||||
self.assertEqual(result["scene_config"]["kind"], "SceneConfig")
|
||||
|
||||
def test_model_motion_requests_complete_control_and_feedback_loop(self) -> None:
|
||||
model_output = {
|
||||
"detected_domains": ["seat"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
"target_scope": "driver",
|
||||
"operation": "set",
|
||||
"parameters": [{"name": "position_pct", "value": 35}],
|
||||
"reason": "Move the driver seat backward from its current midpoint.",
|
||||
},
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_backrest_recline",
|
||||
"target_scope": "driver",
|
||||
"operation": "set",
|
||||
"parameters": [{"name": "position_pct", "value": 60}],
|
||||
"reason": "Recline the driver backrest.",
|
||||
},
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_legrest_height",
|
||||
"target_scope": "passenger",
|
||||
"operation": "set",
|
||||
"parameters": [{"name": "position_pct", "value": 65}],
|
||||
"reason": "Raise the passenger leg rest.",
|
||||
},
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_legrest_extension",
|
||||
"target_scope": "passenger",
|
||||
"operation": "set",
|
||||
"parameters": [{"name": "position_pct", "value": 70}],
|
||||
"reason": "Extend the passenger leg rest.",
|
||||
},
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["四个电机轴使用绝对位置目标。"],
|
||||
"confidence": 0.93,
|
||||
}
|
||||
self.service.openai.api_key = "test-key"
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
||||
result = self.service.process(
|
||||
"主驾往后一点并把靠背躺下,副驾腿托升起并伸长",
|
||||
"openai",
|
||||
"conv-model-motion",
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(len(result["function_config"]["spec"]["operations"]), 4)
|
||||
self.assertEqual(len(result["command_plan"]["spec"]["commands"]), 4)
|
||||
self.assertEqual(len(result["execution_report"]["spec"]["results"]), 4)
|
||||
driver = result["actuator_state"]["driver"]["position"]
|
||||
passenger = result["actuator_state"]["passenger"]["position"]
|
||||
self.assertEqual(driver["longitudinal"]["position_pct"], 35)
|
||||
self.assertEqual(driver["backrest_recline"]["position_pct"], 60)
|
||||
self.assertEqual(passenger["legrest_height"]["position_pct"], 65)
|
||||
self.assertEqual(passenger["legrest_extension"]["position_pct"], 70)
|
||||
telemetry = result["execution_report"]["spec"]["results"][0]["telemetry"]
|
||||
self.assertEqual(telemetry["start_position_pct"], 50)
|
||||
self.assertEqual(telemetry["effective_position_pct"], 35)
|
||||
self.assertEqual(telemetry["feedback_frame"]["frame_id"], "0x558")
|
||||
self.assertEqual(telemetry["fault_mask"], 0)
|
||||
codes = {issue["code"] for issue in result["validation"]}
|
||||
self.assertIn("SYNTHETIC_MOTION_CAN", codes)
|
||||
self.assertIn("DRIVER_MOTION_INTERLOCK_NOT_IMPLEMENTED", codes)
|
||||
|
||||
def test_motion_follow_up_state_is_supplied_to_model(self) -> None:
|
||||
def output(position: int) -> dict[str, object]:
|
||||
return {
|
||||
"detected_domains": ["seat"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
"target_scope": "driver",
|
||||
"operation": "decrease",
|
||||
"parameters": [{"name": "position_pct", "value": position}],
|
||||
"reason": "Model resolved the relative request.",
|
||||
}
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": [f"Move to {position}%."],
|
||||
"confidence": 0.9,
|
||||
}
|
||||
|
||||
self.service.openai.api_key = "test-key"
|
||||
with patch.object(
|
||||
self.service.openai,
|
||||
"interpret",
|
||||
side_effect=[output(40), output(30)],
|
||||
) as interpret:
|
||||
first = self.service.process(
|
||||
"主驾往后一点", "openai", "conv-motion-follow-up"
|
||||
)
|
||||
second = self.service.process(
|
||||
"还是靠前,再往后一点", "openai", "conv-motion-follow-up"
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
first["actuator_state"]["driver"]["position"]["longitudinal"][
|
||||
"position_pct"
|
||||
],
|
||||
40,
|
||||
)
|
||||
self.assertEqual(
|
||||
second["actuator_state"]["driver"]["position"]["longitudinal"][
|
||||
"position_pct"
|
||||
],
|
||||
30,
|
||||
)
|
||||
second_context = interpret.call_args_list[1].args[1]
|
||||
self.assertEqual(
|
||||
second_context["devices"]["driver"]["position"]["longitudinal"]["position_pct"],
|
||||
40,
|
||||
)
|
||||
|
||||
def test_motion_adapter_does_not_resolve_missing_absolute_target(self) -> None:
|
||||
model_output = {
|
||||
"detected_domains": ["seat"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
"target_scope": "driver",
|
||||
"operation": "decrease",
|
||||
"parameters": [],
|
||||
"reason": "No absolute target was produced.",
|
||||
}
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["Unresolved relative request."],
|
||||
"confidence": 0.6,
|
||||
}
|
||||
self.service.openai.api_key = "test-key"
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
||||
result = self.service.process(
|
||||
"主驾往后一点", "openai", "conv-motion-missing-target"
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "needs_confirmation")
|
||||
self.assertIsNone(result["function_config"])
|
||||
self.assertIn(
|
||||
"AMBIGUOUS_INTENT", {issue["code"] for issue in result["validation"]}
|
||||
)
|
||||
self.assertNotIn(
|
||||
"NO_EXECUTABLE_OPERATION",
|
||||
{issue["code"] for issue in result["validation"]},
|
||||
)
|
||||
self.assertNotIn(
|
||||
"PUBLIC_SCHEMA", {issue["code"] for issue in result["validation"]}
|
||||
)
|
||||
|
||||
def test_exact_angles_and_named_preset_complete_conversation_loop(self) -> None:
|
||||
def plan(requests: list[dict[str, object]], summary: str) -> dict[str, object]:
|
||||
return {
|
||||
"detected_domains": ["seat"],
|
||||
"requests": requests,
|
||||
"ambiguities": [],
|
||||
"decision_summary": [summary],
|
||||
"confidence": 0.96,
|
||||
}
|
||||
|
||||
def request(
|
||||
capability: str,
|
||||
operation: str,
|
||||
parameters: list[dict[str, object]],
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"domain": "seat",
|
||||
"capability": capability,
|
||||
"target_scope": "passenger",
|
||||
"operation": operation,
|
||||
"parameters": parameters,
|
||||
"reason": "Model resolved the user's posture goal against runtime context.",
|
||||
}
|
||||
|
||||
first_plan = plan(
|
||||
[
|
||||
request(
|
||||
"seat.set_backrest_recline",
|
||||
"set",
|
||||
[{"name": "angle_deg", "value": 128}],
|
||||
),
|
||||
request(
|
||||
"seat.set_legrest_height",
|
||||
"set",
|
||||
[{"name": "angle_deg", "value": 133}],
|
||||
),
|
||||
],
|
||||
"副驾靠背/坐垫夹角设为128度,坐垫/腿托夹角设为133度。",
|
||||
)
|
||||
recline_plan = plan(
|
||||
[
|
||||
request(
|
||||
"seat.set_backrest_recline",
|
||||
"increase",
|
||||
[{"name": "angle_deg", "value": 133}],
|
||||
)
|
||||
],
|
||||
"根据当前128度姿态,将副驾靠背继续后仰至133度。",
|
||||
)
|
||||
save_plan = plan(
|
||||
[
|
||||
request(
|
||||
"seat.save_posture_preset",
|
||||
"save",
|
||||
[{"name": "preset_name", "value": "零重力模式"}],
|
||||
)
|
||||
],
|
||||
"将副驾当前五轴姿态保存为零重力模式。",
|
||||
)
|
||||
leave_preset_plan = plan(
|
||||
[
|
||||
request(
|
||||
"seat.set_backrest_recline",
|
||||
"set",
|
||||
[{"name": "angle_deg", "value": 105}],
|
||||
)
|
||||
],
|
||||
"将副驾靠背恢复到105度用于验证预设回调。",
|
||||
)
|
||||
apply_plan = plan(
|
||||
[
|
||||
request(
|
||||
"seat.apply_posture_preset",
|
||||
"apply",
|
||||
[{"name": "preset_name", "value": "零重力模式"}],
|
||||
)
|
||||
],
|
||||
"调用已保存的副驾零重力模式。",
|
||||
)
|
||||
|
||||
self.service.openai.api_key = "test-key"
|
||||
conversation_id = "conv-angle-preset-loop"
|
||||
with patch.object(
|
||||
self.service.openai,
|
||||
"interpret",
|
||||
side_effect=[
|
||||
first_plan,
|
||||
recline_plan,
|
||||
save_plan,
|
||||
leave_preset_plan,
|
||||
apply_plan,
|
||||
],
|
||||
) as interpret:
|
||||
first = self.service.process(
|
||||
"副驾调整为零重力模式,要求靠背座椅夹角128度,坐垫脚托夹角133度",
|
||||
"openai",
|
||||
conversation_id,
|
||||
)
|
||||
second = self.service.process(
|
||||
"靠背再向后一点", "openai", conversation_id
|
||||
)
|
||||
saved = self.service.process(
|
||||
"将当前状态保存为零重力模式", "openai", conversation_id
|
||||
)
|
||||
changed = self.service.process(
|
||||
"副驾靠背恢复直立一些", "openai", conversation_id
|
||||
)
|
||||
applied = self.service.process(
|
||||
"副驾恢复到零重力模式", "openai", conversation_id
|
||||
)
|
||||
|
||||
first_operations = first["function_config"]["spec"]["operations"]
|
||||
self.assertEqual(
|
||||
first_operations[0]["parameters"],
|
||||
{"position_pct": 73, "angle_deg": 128},
|
||||
)
|
||||
self.assertEqual(
|
||||
first_operations[1]["parameters"],
|
||||
{"position_pct": 58, "angle_deg": 133},
|
||||
)
|
||||
self.assertEqual(
|
||||
first["actuator_state"]["passenger"]["position"]["backrest_recline"][
|
||||
"angle_deg"
|
||||
],
|
||||
128,
|
||||
)
|
||||
first_telemetry = first["execution_report"]["spec"]["results"][0]["telemetry"]
|
||||
self.assertEqual(first_telemetry["requested_angle_deg"], 128)
|
||||
self.assertEqual(first_telemetry["effective_position_pct"], 73)
|
||||
|
||||
self.assertEqual(
|
||||
second["actuator_state"]["passenger"]["position"]["backrest_recline"][
|
||||
"angle_deg"
|
||||
],
|
||||
133,
|
||||
)
|
||||
relative_motion = second["function_config"]["spec"]["operations"][0][
|
||||
"motion"
|
||||
]
|
||||
self.assertEqual(relative_motion["mode"], "relative")
|
||||
self.assertEqual(relative_motion["direction"], "increase")
|
||||
self.assertEqual(relative_motion["start_position_pct"], 73)
|
||||
self.assertEqual(relative_motion["delta_position_pct"], 11)
|
||||
self.assertEqual(relative_motion["delta_angle_deg"], 5)
|
||||
second_context = interpret.call_args_list[1].args[1]
|
||||
self.assertEqual(
|
||||
second_context["devices"]["passenger"]["position"]["backrest_recline"][
|
||||
"angle_deg"
|
||||
],
|
||||
128,
|
||||
)
|
||||
|
||||
self.assertEqual(saved["status"], "succeeded")
|
||||
self.assertIsNone(saved["function_config"])
|
||||
self.assertEqual(saved["preset_config"]["kind"], "FunctionPreset")
|
||||
self.assertEqual(saved["preset_config"]["metadata"]["name"], "零重力模式")
|
||||
self.assertFalse(
|
||||
saved["execution_report"]["spec"]["results"][0]["telemetry"][
|
||||
"can_command_emitted"
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
saved["saved_presets"],
|
||||
[{"name": "零重力模式", "target": "passenger"}],
|
||||
)
|
||||
preset_path = (
|
||||
self.service.artifact_root
|
||||
/ "user-presets"
|
||||
/ f"{conversation_id}.json"
|
||||
)
|
||||
self.assertTrue(preset_path.is_file())
|
||||
|
||||
self.assertEqual(
|
||||
changed["actuator_state"]["passenger"]["position"]["backrest_recline"][
|
||||
"angle_deg"
|
||||
],
|
||||
105,
|
||||
)
|
||||
self.assertEqual(applied["status"], "succeeded")
|
||||
restored = applied["actuator_state"]["passenger"]["position"]
|
||||
self.assertEqual(restored["backrest_recline"]["angle_deg"], 133)
|
||||
self.assertEqual(restored["legrest_height"]["angle_deg"], 133)
|
||||
self.assertEqual(len(applied["function_config"]["spec"]["operations"]), 5)
|
||||
apply_context = interpret.call_args_list[4].args[1]
|
||||
self.assertEqual(apply_context["presets"][0]["name"], "零重力模式")
|
||||
|
||||
def test_missing_target_requests_confirmation(self) -> None:
|
||||
result = self.service.process("打开座椅通风", provider="local")
|
||||
|
||||
self.assertEqual(result["status"], "needs_confirmation")
|
||||
self.assertIsNone(result["function_config"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Executable
+140
@@ -0,0 +1,140 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from seat_domain.intent import parse_local_intent
|
||||
|
||||
|
||||
class LocalIntentParserTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def context(level: int = 0) -> dict:
|
||||
return {
|
||||
"last_target": "driver" if level else None,
|
||||
"devices": {
|
||||
"driver": {
|
||||
"ventilation": {"state": "on" if level else "off", "level": level},
|
||||
"heating": {"state": "off", "level": 0},
|
||||
},
|
||||
"passenger": {
|
||||
"ventilation": {"state": "off", "level": 0},
|
||||
"heating": {"state": "off", "level": 0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def test_parses_driver_ventilation_on(self) -> None:
|
||||
result = parse_local_intent("打开主驾座椅通风")
|
||||
|
||||
self.assertEqual(
|
||||
result["actions"],
|
||||
[
|
||||
{
|
||||
"target": "driver",
|
||||
"function": "ventilation",
|
||||
"state": "on",
|
||||
"requested_level": None,
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertEqual(result["ambiguities"], [])
|
||||
|
||||
def test_expands_front_seats(self) -> None:
|
||||
result = parse_local_intent("主副驾座椅通风都打开")
|
||||
|
||||
self.assertEqual({item["target"] for item in result["actions"]}, {"driver", "passenger"})
|
||||
self.assertTrue(all(item["state"] == "on" for item in result["actions"]))
|
||||
|
||||
def test_keeps_requested_level_for_safety_validation(self) -> None:
|
||||
result = parse_local_intent("主驾座椅加热开到三档")
|
||||
|
||||
self.assertEqual(result["actions"][0]["requested_level"], 3)
|
||||
|
||||
def test_requires_target(self) -> None:
|
||||
result = parse_local_intent("打开座椅通风")
|
||||
|
||||
self.assertEqual(result["actions"], [])
|
||||
self.assertEqual(result["ambiguities"][0]["field"], "target")
|
||||
|
||||
def test_maps_mild_hot_and_stuffy_feedback_to_level_one(self) -> None:
|
||||
result = parse_local_intent("主驾有点热,有点闷", self.context())
|
||||
|
||||
self.assertEqual(
|
||||
result["actions"],
|
||||
[
|
||||
{
|
||||
"target": "driver",
|
||||
"function": "ventilation",
|
||||
"state": "on",
|
||||
"requested_level": 1,
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertIn("热、闷", "".join(result["decision_summary"]))
|
||||
|
||||
def test_increments_active_level_for_continuing_discomfort(self) -> None:
|
||||
result = parse_local_intent("还有点闷", self.context(level=1))
|
||||
|
||||
self.assertEqual(result["actions"][0]["requested_level"], 2)
|
||||
self.assertEqual(result["actions"][0]["target"], "driver")
|
||||
self.assertIn("提升到2档", "".join(result["decision_summary"]))
|
||||
|
||||
def test_cold_feedback_reduces_active_ventilation_before_heating(self) -> None:
|
||||
result = parse_local_intent("我有点冷", self.context(level=1))
|
||||
|
||||
self.assertEqual(
|
||||
result["actions"],
|
||||
[
|
||||
{
|
||||
"target": "driver",
|
||||
"function": "ventilation",
|
||||
"state": "off",
|
||||
"requested_level": None,
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertIn("不立即开启加热", "".join(result["decision_summary"]))
|
||||
|
||||
def test_airflow_cause_targets_ventilation_not_heating(self) -> None:
|
||||
context = self.context()
|
||||
context["devices"]["passenger"]["ventilation"] = {"state": "on", "level": 2}
|
||||
result = parse_local_intent("副驾吹的我有点冷", context)
|
||||
|
||||
self.assertEqual(
|
||||
result["actions"],
|
||||
[
|
||||
{
|
||||
"target": "passenger",
|
||||
"function": "ventilation",
|
||||
"state": "on",
|
||||
"requested_level": 1,
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertNotIn("heating", {action["function"] for action in result["actions"]})
|
||||
self.assertIn("致冷来源", "".join(result["decision_summary"]))
|
||||
|
||||
def test_cold_and_stuffy_are_independent_comfort_dimensions(self) -> None:
|
||||
result = parse_local_intent("主驾有点冷、有点闷", self.context())
|
||||
|
||||
self.assertEqual(
|
||||
{
|
||||
(action["function"], action["state"], action["requested_level"])
|
||||
for action in result["actions"]
|
||||
},
|
||||
{("heating", "on", 1), ("ventilation", "on", 1)},
|
||||
)
|
||||
self.assertEqual(result["ambiguities"], [])
|
||||
self.assertIn("不将其判定为语义冲突", "".join(result["decision_summary"]))
|
||||
|
||||
def test_vehicle_scope_does_not_inherit_a_seat_target(self) -> None:
|
||||
result = parse_local_intent("车上太闷了", self.context(level=1))
|
||||
|
||||
self.assertEqual(result["actions"], [])
|
||||
self.assertEqual(result["ambiguities"][0]["field"], "domain")
|
||||
self.assertEqual(result["detected_domains"], ["climate"])
|
||||
self.assertEqual(result["scope"], "vehicle")
|
||||
self.assertIn("整车舱范围", "".join(result["decision_summary"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from seat_domain.compiler import SeatCompiler
|
||||
from seat_domain.generator import build_function_config
|
||||
from seat_domain.intent import parse_local_intent
|
||||
from seat_domain.matrix import SeatCanMatrix
|
||||
|
||||
|
||||
DOMAIN_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
class MatrixCompilerTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.matrix = SeatCanMatrix(DOMAIN_ROOT / "can" / "seat-thermal-matrix.yaml")
|
||||
self.combined_matrix = SeatCanMatrix(
|
||||
[
|
||||
DOMAIN_ROOT / "can" / "seat-thermal-matrix.yaml",
|
||||
DOMAIN_ROOT / "can" / "seat-motion-virtual-matrix.yaml",
|
||||
]
|
||||
)
|
||||
self.compiler = SeatCompiler(DOMAIN_ROOT / "can" / "signal-mapping.yaml")
|
||||
|
||||
def test_encodes_combined_control_bits(self) -> None:
|
||||
parsed = parse_local_intent("打开主驾座椅通风,同时关闭副驾座椅加热")
|
||||
config = build_function_config(parsed, "req-test-001", "local_rules")
|
||||
plan = self.compiler.compile(config)
|
||||
command = plan["spec"]["commands"][0]
|
||||
|
||||
frame = self.matrix.encode(command["message"], command["signals"])
|
||||
|
||||
self.assertEqual(frame["frame_id"], "0x42A")
|
||||
self.assertEqual(frame["data_hex"], "01 80")
|
||||
|
||||
def test_compilation_is_deterministic(self) -> None:
|
||||
parsed = parse_local_intent("关闭副驾座椅通风")
|
||||
config = build_function_config(parsed, "req-test-002", "local_rules")
|
||||
|
||||
self.assertEqual(self.compiler.compile(config), self.compiler.compile(config))
|
||||
|
||||
def test_compiles_each_motion_axis_with_feedback_completion(self) -> None:
|
||||
parsed = {
|
||||
"detected_domains": ["seat"],
|
||||
"actions": [
|
||||
{
|
||||
"action_type": "position",
|
||||
"target": "driver",
|
||||
"axis": "longitudinal",
|
||||
"position_pct": 35,
|
||||
},
|
||||
{
|
||||
"action_type": "position",
|
||||
"target": "driver",
|
||||
"axis": "backrest_recline",
|
||||
"position_pct": 60,
|
||||
},
|
||||
],
|
||||
"deferred_requests": [],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["Move two driver-seat axes."],
|
||||
"confidence": 1.0,
|
||||
}
|
||||
config = build_function_config(parsed, "req-motion-001", "test")
|
||||
|
||||
plan = self.compiler.compile(config)
|
||||
|
||||
self.assertEqual(len(plan["spec"]["commands"]), 2)
|
||||
first, second = plan["spec"]["commands"]
|
||||
self.assertEqual(first["message"], "Driver_Seat_Motion_Command_550")
|
||||
self.assertEqual(first["signals"]["Longitudinal_Target_Pct"], 35)
|
||||
self.assertEqual(first["signals"]["Motion_Enable_Mask"], 1)
|
||||
self.assertEqual(first["completion"]["message"], "Driver_Seat_Motion_Status_558")
|
||||
self.assertEqual(first["completion"]["signal"], "Longitudinal_Actual_Pct")
|
||||
self.assertEqual(second["signals"]["Backrest_Recline_Target_Pct"], 60)
|
||||
self.assertEqual(second["signals"]["Motion_Enable_Mask"], 4)
|
||||
frame = self.combined_matrix.encode(first["message"], first["signals"])
|
||||
self.assertEqual(frame["frame_id"], "0x550")
|
||||
self.assertEqual(frame["payload_size"], 8)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Executable
+835
@@ -0,0 +1,835 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from collections import deque
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
SEAT_SOURCE = ROOT / "domains" / "seat" / "src"
|
||||
if str(SEAT_SOURCE) not in sys.path:
|
||||
sys.path.insert(0, str(SEAT_SOURCE))
|
||||
|
||||
from cognitive_agent.infrastructure.can.gateway import CanFrame # noqa: E402
|
||||
from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402
|
||||
from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
|
||||
HardwareSafetyError,
|
||||
SeatHardwareSettings,
|
||||
)
|
||||
from cognitive_agent.infrastructure.firmware import ( # noqa: E402
|
||||
EXPECTED_IDENTITY_PAYLOAD,
|
||||
FirmwareProgrammingError,
|
||||
T2GFirmwareGuard,
|
||||
T2GSeatProgrammer,
|
||||
)
|
||||
from seat_domain import HardwareSeatExecutor, SeatCanMatrix, T2GSeatCompiler # noqa: E402
|
||||
|
||||
|
||||
def function_config(*operations: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
"apiVersion": "cognitive-agent/v1alpha1",
|
||||
"kind": "FunctionConfig",
|
||||
"metadata": {
|
||||
"id": "function-test-hardware",
|
||||
"name": "T2G hardware test",
|
||||
"version": "1.0.0",
|
||||
"request_id": "req-hardware-test",
|
||||
},
|
||||
"spec": {"domain": "seat", "operations": list(operations)},
|
||||
}
|
||||
|
||||
|
||||
def operation(
|
||||
capability: str,
|
||||
parameters: dict[str, object],
|
||||
*,
|
||||
operation_id: str = "op-hardware-test",
|
||||
instance: str = "driver",
|
||||
zone: str = "front_left",
|
||||
motion: dict[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
result = {
|
||||
"id": operation_id,
|
||||
"capability": capability,
|
||||
"target": {"instance": instance, "zone": zone},
|
||||
"parameters": parameters,
|
||||
"timeout_ms": 20,
|
||||
}
|
||||
if motion is not None:
|
||||
result["motion"] = motion
|
||||
return result
|
||||
|
||||
|
||||
class ReactiveFakeBus:
|
||||
def __init__(
|
||||
self,
|
||||
matrix: SeatCanMatrix,
|
||||
*,
|
||||
complete_motion: bool = True,
|
||||
position_valid: bool = True,
|
||||
timed_failure_result: int | None = None,
|
||||
timed_failure_event: int = 4,
|
||||
):
|
||||
self.matrix = matrix
|
||||
self.complete_motion = complete_motion
|
||||
self.position_valid = position_valid
|
||||
self.timed_failure_result = timed_failure_result
|
||||
self.timed_failure_event = timed_failure_event
|
||||
self.hall = [0, 0, 0, 0]
|
||||
self.max_hall = [480, 790, 1590, 960]
|
||||
self.sent: list[CanFrame] = []
|
||||
self.received: deque[CanFrame] = deque()
|
||||
self.closed = False
|
||||
|
||||
def _frame(self, message: str, values: dict[str, int]) -> CanFrame:
|
||||
encoded = self.matrix.encode(message, values)
|
||||
return CanFrame(int(encoded["frame_id"], 16), bytes.fromhex(encoded["data_hex"]))
|
||||
|
||||
def send(self, arbitration_id: int, data: bytes) -> CanFrame:
|
||||
frame = CanFrame(arbitration_id, bytes(data))
|
||||
self.sent.append(frame)
|
||||
if arbitration_id == 0x300:
|
||||
command = self.matrix.decode("T2G_Seat_Move_Command_300", data)
|
||||
sequence = command["Sequence"]
|
||||
opcode = command["Opcode"]
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_System_Status_301",
|
||||
{
|
||||
"Ack": 1,
|
||||
"Reject_Reason": 0,
|
||||
"Last_Opcode": opcode,
|
||||
"Last_Sequence": sequence,
|
||||
"Safety_Inhibit": 0,
|
||||
"Safety_Fault": 0,
|
||||
},
|
||||
)
|
||||
)
|
||||
if opcode == 9:
|
||||
for motor in range(4):
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Motor_Status_302",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Task_State": 0,
|
||||
"Result": 0,
|
||||
"Position_Valid": int(self.position_valid),
|
||||
"Motor_Sequence": sequence,
|
||||
"Value_X100": 0,
|
||||
"Duty_Percent": 0,
|
||||
},
|
||||
)
|
||||
)
|
||||
for motor in range(4):
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Diagnostic_303",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Hall_Count": self.hall[motor],
|
||||
"Max_Hall_Count": self.max_hall[motor],
|
||||
"Max_Speed_Pps_X100": 9000,
|
||||
},
|
||||
)
|
||||
)
|
||||
if opcode == 1 and self.complete_motion:
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Motor_Status_302",
|
||||
{
|
||||
"Motor_Sub_ID": command["Motor_Selector"],
|
||||
"Task_State": 8,
|
||||
"Result": 2,
|
||||
"Position_Valid": 1,
|
||||
"Motor_Sequence": sequence,
|
||||
"Value_X100": command["Param_A"],
|
||||
"Duty_Percent": command["Param_B"],
|
||||
},
|
||||
)
|
||||
)
|
||||
if opcode == 3 and self.timed_failure_result is not None:
|
||||
motor = command["Motor_Selector"]
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Motor_Status_302",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Task_State": 9,
|
||||
"Result": self.timed_failure_result,
|
||||
"Direction": command["Direction"],
|
||||
"Position_Valid": int(self.position_valid),
|
||||
"Motor_Sequence": sequence,
|
||||
"Value_X100": 0,
|
||||
"Duty_Percent": command["Param_B"],
|
||||
},
|
||||
)
|
||||
)
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Diagnostic_303",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Last_Motor_Event": self.timed_failure_event,
|
||||
"Hall_Count": self.hall[motor],
|
||||
"Max_Hall_Count": self.max_hall[motor],
|
||||
"Max_Speed_Pps_X100": 9000,
|
||||
},
|
||||
)
|
||||
)
|
||||
elif opcode == 3 and self.complete_motion:
|
||||
motor = command["Motor_Selector"]
|
||||
delta = max(1, command["Param_A"] // 35)
|
||||
if command["Direction"] == 1:
|
||||
self.hall[motor] = min(
|
||||
self.max_hall[motor], self.hall[motor] + delta
|
||||
)
|
||||
else:
|
||||
self.hall[motor] = max(0, self.hall[motor] - delta)
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Motor_Status_302",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Task_State": 8,
|
||||
"Result": 2,
|
||||
"Direction": command["Direction"],
|
||||
"Position_Valid": int(self.position_valid),
|
||||
"Motor_Sequence": sequence,
|
||||
"Value_X100": 0,
|
||||
"Duty_Percent": command["Param_B"],
|
||||
},
|
||||
)
|
||||
)
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Diagnostic_303",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Hall_Count": self.hall[motor],
|
||||
"Max_Hall_Count": self.max_hall[motor],
|
||||
"Max_Speed_Pps_X100": 9000,
|
||||
},
|
||||
)
|
||||
)
|
||||
elif arbitration_id == 0x29E:
|
||||
control = self.matrix.decode("T2G_Seat_Thermal_Control_29E", data)
|
||||
heating = control["Driver_Seat_Heating_On"]
|
||||
ventilation = control["Driver_Seat_Ventilation_On"]
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Thermal_Status_4FA",
|
||||
{
|
||||
"Driver_Seat_Heating_Status": (
|
||||
min(control["Driver_Seat_Heating_Gear"], 3) + 1
|
||||
if heating == 1
|
||||
else 1
|
||||
),
|
||||
"Driver_Seat_Ventilation_Status": (
|
||||
min(control["Driver_Seat_Ventilation_Gear"], 3) + 1
|
||||
if ventilation == 1
|
||||
else 1
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
return frame
|
||||
|
||||
def receive(self, timeout_ms: int) -> CanFrame | None:
|
||||
del timeout_ms
|
||||
return self.received.popleft() if self.received else None
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
class IdentityFakeBus:
|
||||
def __init__(self, identity: bytes | None):
|
||||
self.identity = identity
|
||||
self.sent: list[CanFrame] = []
|
||||
self.closed = False
|
||||
|
||||
def send(self, arbitration_id: int, data: bytes) -> CanFrame:
|
||||
frame = CanFrame(arbitration_id, bytes(data))
|
||||
self.sent.append(frame)
|
||||
return frame
|
||||
|
||||
def receive(self, timeout_ms: int) -> CanFrame | None:
|
||||
del timeout_ms
|
||||
if self.identity is None:
|
||||
return None
|
||||
identity = self.identity
|
||||
self.identity = None
|
||||
return CanFrame(0x305, identity)
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
class FailingIdentityBus(IdentityFakeBus):
|
||||
def send(self, arbitration_id: int, data: bytes) -> CanFrame:
|
||||
del arbitration_id, data
|
||||
raise RuntimeError("CAN target did not ACK the identity probe")
|
||||
|
||||
|
||||
class FakeProgrammer:
|
||||
def __init__(self):
|
||||
self.calls = 0
|
||||
|
||||
def program(self) -> dict[str, object]:
|
||||
self.calls += 1
|
||||
return {
|
||||
"backend": "fake",
|
||||
"programmed": True,
|
||||
"image": "Seat_Controller_demo.hex",
|
||||
"image_sha256": "0" * 64,
|
||||
}
|
||||
|
||||
|
||||
class T2GHardwareTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.matrix = SeatCanMatrix(
|
||||
[
|
||||
ROOT / "domains" / "seat" / "can" / "seat-thermal-hardware-matrix.yaml",
|
||||
ROOT / "domains" / "seat" / "can" / "seat-motion-hardware-matrix.yaml",
|
||||
]
|
||||
)
|
||||
self.compiler = T2GSeatCompiler(
|
||||
ROOT / "domains" / "seat" / "can" / "signal-mapping.hardware.yaml",
|
||||
ROOT / "platforms" / "t2g_seat_bench" / "calibration" / "seat.yaml",
|
||||
)
|
||||
|
||||
def test_compiler_emits_verified_heating_payload(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_heating", {"state": "on", "level": 3})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
frames = [
|
||||
self.matrix.encode(item["message"], item["signals"])
|
||||
for item in plan["spec"]["commands"]
|
||||
]
|
||||
|
||||
self.assertEqual(frames[0]["frame_id"], "0x3CE")
|
||||
self.assertEqual(frames[0]["data_hex"], "10 00 00 00 00 00 00 00")
|
||||
self.assertEqual(frames[1]["frame_id"], "0x29E")
|
||||
self.assertEqual(frames[1]["data_hex"], "16 00 30 00 00 00 00 00")
|
||||
|
||||
def test_compiler_emits_verified_backrest_payload(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_backrest_recline", {"position_pct": 50})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
command = plan["spec"]["commands"][0]
|
||||
frame = self.matrix.encode(command["message"], command["signals"])
|
||||
|
||||
self.assertEqual(frame["frame_id"], "0x300")
|
||||
self.assertEqual(frame["data_hex"], "01 12 00 00 94 11 32 00")
|
||||
|
||||
def test_compiler_maps_small_relative_backrest_request_to_timed_duty(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 31, "angle_deg": 109},
|
||||
motion={
|
||||
"mode": "relative",
|
||||
"direction": "increase",
|
||||
"start_position_pct": 20,
|
||||
"delta_position_pct": 11,
|
||||
"start_angle_deg": 104,
|
||||
"delta_angle_deg": 5,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
command = self.compiler.compile(config, {})["spec"]["commands"][0]
|
||||
|
||||
self.assertEqual(command["signals"]["Opcode"], 3)
|
||||
self.assertEqual(command["signals"]["Direction"], 1)
|
||||
self.assertEqual(command["signals"]["Param_A"], 5500)
|
||||
self.assertEqual(command["signals"]["Param_B"], 50)
|
||||
self.assertEqual(command["completion"]["signal"], "Task_State")
|
||||
|
||||
def test_compiler_maps_large_relative_backrest_request_to_absolute_target(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 64, "angle_deg": 124},
|
||||
motion={
|
||||
"mode": "relative",
|
||||
"direction": "increase",
|
||||
"start_position_pct": 20,
|
||||
"delta_position_pct": 44,
|
||||
"start_angle_deg": 104,
|
||||
"delta_angle_deg": 20,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
command = self.compiler.compile(config, {})["spec"]["commands"][0]
|
||||
|
||||
self.assertEqual(command["signals"]["Opcode"], 1)
|
||||
self.assertEqual(command["signals"]["Direction"], 0)
|
||||
self.assertEqual(command["signals"]["Param_A"], 5760)
|
||||
self.assertEqual(command["completion"]["signal"], "Value_X100")
|
||||
|
||||
def test_passenger_alias_emits_the_same_physical_payload(self) -> None:
|
||||
driver = function_config(
|
||||
operation("seat.set_heating", {"state": "on", "level": 2})
|
||||
)
|
||||
passenger = function_config(
|
||||
operation(
|
||||
"seat.set_heating",
|
||||
{"state": "on", "level": 2},
|
||||
instance="passenger",
|
||||
zone="front_right",
|
||||
)
|
||||
)
|
||||
|
||||
driver_plan = self.compiler.compile(driver, {})
|
||||
passenger_plan = self.compiler.compile(passenger, {})
|
||||
self.assertEqual(
|
||||
driver_plan["spec"]["commands"], passenger_plan["spec"]["commands"]
|
||||
)
|
||||
|
||||
def test_driver_and_passenger_identical_motion_is_sent_once(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 60},
|
||||
operation_id="op-driver",
|
||||
),
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 60},
|
||||
operation_id="op-passenger",
|
||||
instance="passenger",
|
||||
zone="front_right",
|
||||
),
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
self.assertEqual(len(plan["spec"]["commands"]), 1)
|
||||
self.assertEqual(
|
||||
plan["spec"]["commands"][0]["operation_ref"],
|
||||
"op-driver,op-passenger",
|
||||
)
|
||||
|
||||
bus = ReactiveFakeBus(self.matrix)
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 1])
|
||||
operation_results = [
|
||||
item for item in report["spec"]["results"] if item["ref"].startswith("op-")
|
||||
]
|
||||
self.assertEqual({item["ref"] for item in operation_results}, {"op-driver", "op-passenger"})
|
||||
|
||||
def test_executor_waits_for_ack_and_physical_motion_feedback(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_backrest_recline", {"position_pct": 50})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix)
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["mode"], "hil")
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
self.assertFalse(report["spec"]["results"][0]["telemetry"]["simulated"])
|
||||
self.assertTrue(report["spec"]["results"][0]["telemetry"]["hardware_feedback"])
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 1])
|
||||
self.assertTrue(bus.closed)
|
||||
|
||||
def test_executor_blocks_absolute_motion_when_board_position_is_invalid(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_backrest_recline", {"position_pct": 50})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix, position_valid=False)
|
||||
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 9])
|
||||
failure = report["spec"]["results"][-1]
|
||||
self.assertIn("Position_Valid=0", failure["message"])
|
||||
self.assertTrue(failure["telemetry"]["calibration_required"])
|
||||
|
||||
def test_large_relative_request_is_blocked_without_sending_absolute_opcode(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 64, "angle_deg": 124},
|
||||
motion={
|
||||
"mode": "relative",
|
||||
"direction": "increase",
|
||||
"start_position_pct": 20,
|
||||
"delta_position_pct": 44,
|
||||
"start_angle_deg": 104,
|
||||
"delta_angle_deg": 20,
|
||||
},
|
||||
)
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix, position_valid=False)
|
||||
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 9])
|
||||
self.assertNotIn(1, [item.data[0] for item in bus.sent])
|
||||
self.assertTrue(
|
||||
report["spec"]["results"][-1]["telemetry"]["calibration_required"]
|
||||
)
|
||||
|
||||
def test_executor_accepts_hall_confirmed_timed_motion_without_position(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 31, "angle_deg": 109},
|
||||
motion={
|
||||
"mode": "relative",
|
||||
"direction": "increase",
|
||||
"start_position_pct": 20,
|
||||
"delta_position_pct": 11,
|
||||
"start_angle_deg": 104,
|
||||
"delta_angle_deg": 5,
|
||||
},
|
||||
)
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix, position_valid=False)
|
||||
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 3])
|
||||
telemetry = report["spec"]["results"][0]["telemetry"]
|
||||
self.assertEqual(telemetry["motion_mode"], "relative_timed_duty")
|
||||
self.assertFalse(telemetry["position_valid"])
|
||||
self.assertGreater(telemetry["hall_delta"], 0)
|
||||
self.assertTrue(telemetry["hall_direction_verified"])
|
||||
|
||||
def test_executor_reports_stall_with_post_failure_hall_diagnostics(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 31, "angle_deg": 109},
|
||||
motion={
|
||||
"mode": "relative",
|
||||
"direction": "increase",
|
||||
"start_position_pct": 20,
|
||||
"delta_position_pct": 11,
|
||||
"start_angle_deg": 104,
|
||||
"delta_angle_deg": 5,
|
||||
},
|
||||
)
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(
|
||||
self.matrix,
|
||||
position_valid=False,
|
||||
timed_failure_result=4,
|
||||
timed_failure_event=4,
|
||||
)
|
||||
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 3, 9])
|
||||
failure = report["spec"]["results"][-1]
|
||||
self.assertIn("STALL", failure["message"])
|
||||
self.assertEqual(failure["telemetry"]["motor_result_name"], "STALL")
|
||||
self.assertEqual(failure["telemetry"]["last_motor_event_name"], "STALL")
|
||||
self.assertEqual(
|
||||
failure["telemetry"]["failure_category"], "stall_no_hall_activity"
|
||||
)
|
||||
self.assertEqual(failure["telemetry"]["hall_before"], 0)
|
||||
self.assertEqual(failure["telemetry"]["hall_after"], 0)
|
||||
self.assertEqual(
|
||||
failure["telemetry"]["diagnostic_frame"]["frame_id"], "0x303"
|
||||
)
|
||||
|
||||
def test_executor_sends_stop_all_after_motion_feedback_timeout(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_legrest_extension", {"position_pct": 50})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix, complete_motion=False)
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 1, 9])
|
||||
self.assertTrue(
|
||||
report["spec"]["results"][-1]["telemetry"]["stop_all_attempted"]
|
||||
)
|
||||
|
||||
def test_executor_waits_for_real_thermal_status(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_ventilation", {"state": "on", "level": 2})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix)
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, thermal_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
telemetry = report["spec"]["results"][0]["telemetry"]
|
||||
self.assertEqual(telemetry["feedback_frame"]["frame_id"], "0x4FA")
|
||||
self.assertEqual(telemetry["feedback_value"], 3)
|
||||
|
||||
def test_settings_allow_shared_target_aliases_but_fail_closed_for_other_targets(self) -> None:
|
||||
environment = {
|
||||
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "true",
|
||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN": "bench-secret",
|
||||
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROFILES": "combined",
|
||||
"COGNITIVE_AGENT_ENABLE_UNVERIFIED_LONGITUDINAL": "false",
|
||||
}
|
||||
with patch.dict(os.environ, environment, clear=False):
|
||||
settings = SeatHardwareSettings.from_env(ROOT)
|
||||
settings = replace(
|
||||
settings, firmware_images={"combined": ROOT / "pyproject.toml"}
|
||||
)
|
||||
|
||||
settings.authorize("bench-secret")
|
||||
settings.validate_function_config(
|
||||
function_config(
|
||||
operation(
|
||||
"seat.set_heating",
|
||||
{"state": "on", "level": 1},
|
||||
instance="passenger",
|
||||
zone="front_right",
|
||||
)
|
||||
)
|
||||
)
|
||||
with self.assertRaises(HardwareSafetyError):
|
||||
settings.validate_function_config(
|
||||
function_config(
|
||||
operation(
|
||||
"seat.set_heating",
|
||||
{"state": "on", "level": 1},
|
||||
instance="rear_left",
|
||||
zone="rear_left",
|
||||
)
|
||||
)
|
||||
)
|
||||
with self.assertRaises(HardwareSafetyError):
|
||||
settings.validate_function_config(
|
||||
function_config(
|
||||
operation(
|
||||
"seat.set_longitudinal_position", {"position_pct": 40}
|
||||
)
|
||||
)
|
||||
)
|
||||
with self.assertRaises(HardwareSafetyError):
|
||||
settings.authorize("wrong-secret")
|
||||
|
||||
def test_settings_reject_conflicting_shared_alias_requests(self) -> None:
|
||||
environment = {
|
||||
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "true",
|
||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN": "bench-secret",
|
||||
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROFILES": "combined",
|
||||
}
|
||||
with patch.dict(os.environ, environment, clear=False):
|
||||
settings = SeatHardwareSettings.from_env(ROOT)
|
||||
with self.assertRaises(HardwareSafetyError):
|
||||
settings.validate_function_config(
|
||||
function_config(
|
||||
operation(
|
||||
"seat.set_heating",
|
||||
{"state": "on", "level": 1},
|
||||
operation_id="op-driver",
|
||||
),
|
||||
operation(
|
||||
"seat.set_heating",
|
||||
{"state": "on", "level": 3},
|
||||
operation_id="op-passenger",
|
||||
instance="passenger",
|
||||
zone="front_right",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def test_firmware_guard_reuses_bus_when_identity_matches(self) -> None:
|
||||
bus = IdentityFakeBus(EXPECTED_IDENTITY_PAYLOAD)
|
||||
guard = T2GFirmwareGuard(
|
||||
lambda: bus,
|
||||
None,
|
||||
auto_flash=False,
|
||||
probe_timeout_ms=20,
|
||||
)
|
||||
|
||||
prepared, telemetry = guard.prepare_bus()
|
||||
|
||||
self.assertIs(prepared, bus)
|
||||
self.assertFalse(telemetry["programmed_this_request"])
|
||||
self.assertEqual(bus.sent[0].arbitration_id, 0x304)
|
||||
self.assertFalse(bus.closed)
|
||||
|
||||
def test_firmware_guard_programs_then_reverifies(self) -> None:
|
||||
before = IdentityFakeBus(None)
|
||||
after = IdentityFakeBus(EXPECTED_IDENTITY_PAYLOAD)
|
||||
buses = deque([before, after])
|
||||
programmer = FakeProgrammer()
|
||||
guard = T2GFirmwareGuard(
|
||||
buses.popleft,
|
||||
programmer, # type: ignore[arg-type]
|
||||
auto_flash=True,
|
||||
probe_timeout_ms=20,
|
||||
boot_wait_ms=0,
|
||||
)
|
||||
|
||||
prepared, telemetry = guard.prepare_bus()
|
||||
|
||||
self.assertIs(prepared, after)
|
||||
self.assertEqual(programmer.calls, 1)
|
||||
self.assertTrue(before.closed)
|
||||
self.assertTrue(telemetry["programmed_this_request"])
|
||||
|
||||
def test_firmware_guard_programs_when_unflashed_target_does_not_ack(self) -> None:
|
||||
before = FailingIdentityBus(None)
|
||||
after = IdentityFakeBus(EXPECTED_IDENTITY_PAYLOAD)
|
||||
buses = deque([before, after])
|
||||
programmer = FakeProgrammer()
|
||||
guard = T2GFirmwareGuard(
|
||||
buses.popleft,
|
||||
programmer, # type: ignore[arg-type]
|
||||
auto_flash=True,
|
||||
probe_timeout_ms=20,
|
||||
boot_wait_ms=0,
|
||||
)
|
||||
|
||||
prepared, telemetry = guard.prepare_bus()
|
||||
|
||||
self.assertIs(prepared, after)
|
||||
self.assertEqual(programmer.calls, 1)
|
||||
self.assertTrue(before.closed)
|
||||
self.assertTrue(telemetry["programmed_this_request"])
|
||||
|
||||
def test_firmware_guard_fails_closed_when_auto_flash_is_disabled(self) -> None:
|
||||
bus = IdentityFakeBus(None)
|
||||
guard = T2GFirmwareGuard(
|
||||
lambda: bus,
|
||||
None,
|
||||
auto_flash=False,
|
||||
probe_timeout_ms=20,
|
||||
)
|
||||
|
||||
with self.assertRaises(FirmwareProgrammingError):
|
||||
guard.prepare_bus()
|
||||
self.assertTrue(bus.closed)
|
||||
|
||||
def test_programmer_command_downloads_and_hands_off_to_running_target(self) -> None:
|
||||
firmware_root = ROOT / "firmware" / "t2g" / "Seat_Controller_demo"
|
||||
output_image = (
|
||||
firmware_root / "IAR" / "rev_d" / "Exe" / "Seat_Controller_demo.out"
|
||||
)
|
||||
program_macro = firmware_root / "IAR" / "Seat_Controller_program.mac"
|
||||
programmer = T2GSeatProgrammer(
|
||||
cspybat=ROOT / "fake-iar" / "common" / "bin" / "cspybat.exe",
|
||||
debug_file=output_image,
|
||||
hex_image=(
|
||||
firmware_root / "IAR" / "rev_d" / "Exe" / "Seat_Controller_demo.hex"
|
||||
),
|
||||
macro_file=program_macro,
|
||||
)
|
||||
|
||||
# Dependency discovery is covered by settings/configuration tests. This
|
||||
# test only validates the command and never starts C-SPY or a probe.
|
||||
with patch.object(T2GSeatProgrammer, "required_files", return_value=()):
|
||||
command = programmer.command()
|
||||
|
||||
self.assertEqual(command[3], str(output_image))
|
||||
self.assertIn(f"--macro={program_macro}", command)
|
||||
self.assertNotIn("--download_only", command)
|
||||
self.assertIn("--leave_target_running", command)
|
||||
timeout_index = command.index("--timeout")
|
||||
self.assertEqual(command[timeout_index + 1], "3000")
|
||||
self.assertIn("--jet_probe=cmsisdap", command)
|
||||
self.assertNotIn("Seat_Controller_demo.mac", " ".join(command))
|
||||
|
||||
def test_hardware_validation_replaces_reference_dry_run_warnings(self) -> None:
|
||||
validation = [
|
||||
{"severity": "warning", "code": code, "message": "dry-run"}
|
||||
for code in (
|
||||
"LEVEL_COMMAND_SIMULATED",
|
||||
"SYNTHETIC_MOTION_CAN",
|
||||
"DRIVER_MOTION_INTERLOCK_NOT_IMPLEMENTED",
|
||||
"COMBINED_THERMAL_SIMULATED",
|
||||
"VIRTUAL_EXECUTION_ONLY",
|
||||
)
|
||||
]
|
||||
|
||||
SeatDemoService._adapt_validation_for_hardware(validation)
|
||||
|
||||
codes = {issue["code"] for issue in validation}
|
||||
self.assertEqual(
|
||||
codes,
|
||||
{
|
||||
"LEVEL_COMMAND_T2G_MAPPED",
|
||||
"T2G_MOTION_PROTOCOL_MAPPED",
|
||||
"BENCH_ONLY_NO_VEHICLE_INTERLOCKS",
|
||||
"COMBINED_THERMAL_T2G_MAPPED",
|
||||
"REFERENCE_MATRIX_OVERRIDDEN_BY_T2G_BENCH",
|
||||
},
|
||||
)
|
||||
messages = " ".join(issue["message"] for issue in validation)
|
||||
self.assertNotIn("禁止实车下发", messages)
|
||||
self.assertNotIn("只能虚拟执行", messages)
|
||||
|
||||
def test_physical_feedback_is_mirrored_to_both_logical_seats(self) -> None:
|
||||
actions = [
|
||||
{
|
||||
"target": "passenger",
|
||||
"function": "ventilation",
|
||||
"state": "on",
|
||||
"requested_level": 2,
|
||||
}
|
||||
]
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_ventilation",
|
||||
{"state": "on", "level": 2},
|
||||
instance="passenger",
|
||||
zone="front_right",
|
||||
)
|
||||
)
|
||||
report = {
|
||||
"spec": {
|
||||
"results": [
|
||||
{
|
||||
"ref": "op-hardware-test",
|
||||
"status": "succeeded",
|
||||
"telemetry": {"feedback_value": 3},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
projected = SeatDemoService._hardware_feedback_actions(
|
||||
actions, config, report
|
||||
)
|
||||
|
||||
self.assertEqual({item["target"] for item in projected}, {"driver", "passenger"})
|
||||
self.assertTrue(
|
||||
all(item["requested_level"] == 2 for item in projected)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user