feat: synchronize CH1 vehicle control and feedback

This commit is contained in:
vee
2026-08-11 10:42:13 +08:00
parent 3701192a11
commit 32379a1573
50 changed files with 13636 additions and 736 deletions
+194 -16
View File
@@ -35,7 +35,10 @@ class SeatWebApiTests(unittest.TestCase):
# per-request approval token.
with patch.dict(
os.environ,
{"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false"},
{
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
},
clear=False,
):
service = SeatDemoService(REPOSITORY_ROOT)
@@ -115,7 +118,7 @@ class SeatWebApiTests(unittest.TestCase):
def test_static_console_and_runtime_endpoint(self) -> None:
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8")
with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-44", timeout=3) as response:
with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-51", timeout=3) as response:
app_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/gesture-control.js?v=android-pad-6", timeout=3
@@ -130,7 +133,7 @@ class SeatWebApiTests(unittest.TestCase):
) as response:
console_styles = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/self-extension.js?v=extension-6", timeout=3
f"{self.base_url}/self-extension.js?v=extension-15", timeout=3
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
@@ -148,14 +151,21 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn('id="seatTwinCanvas"', html)
self.assertIn('data-vehicle-model="byd-tang-2024-reference"', html)
self.assertIn("整车孪生界面", html)
self.assertIn("<strong>阅读灯</strong>", html)
self.assertIn("<strong>车内灯光</strong>", html)
self.assertIn('id="interiorLightPower"', html)
self.assertIn('id="ambientBrightness"', html)
self.assertIn('id="ambientColorIndex"', html)
self.assertIn('data-function="massage"', html)
self.assertIn("CMD 0x1DE · FB 0x404 · CH1", html)
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
self.assertNotIn("透明车体 · 实体设备", html)
self.assertIn('src="/app.js?v=seat-hil-44"', html)
self.assertIn('src="/app.js?v=seat-hil-51"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-6"', html)
self.assertNotIn('class="vehicle-system-hud"', html)
self.assertIn('src="/console-v2.js?v=desktop-refactor-28"', html)
self.assertIn('href="/console-v2.css?v=desktop-refactor-68"', html)
self.assertIn('src="/self-extension.js?v=extension-6"', html)
self.assertIn('src="/self-extension.js?v=extension-15"', html)
self.assertIn('href="/self-extension.css?v=extension-9"', html)
self.assertNotIn("滚动查看更多", html)
self.assertIn('aria-label="执行车舱控制指令"', html)
@@ -227,6 +237,10 @@ class SeatWebApiTests(unittest.TestCase):
self.assertNotIn('id="hardwareClientLabel"', html)
self.assertNotIn('id="requestHardwareAccess"', html)
self.assertNotIn('id="releaseHardwareAccess"', html)
self.assertNotIn('id="approvalField"', html)
self.assertNotIn('id="approvalToken"', html)
self.assertNotIn("approval_token: approvalToken", app_script)
self.assertNotIn("授权并真实执行", app_script)
self.assertIn('const dynamicModeOrder = ["rocking_horse", "dynamic_cradle"]', app_script)
self.assertIn('divider.className = "mode-group-divider"', app_script)
self.assertIn(
@@ -234,6 +248,10 @@ class SeatWebApiTests(unittest.TestCase):
app_script,
)
self.assertIn("hardwareButton.disabled = !hardwareExecutionReady", app_script)
self.assertIn('const seat = driverOnly ? "driver" : "passenger"', app_script)
self.assertIn('data-mode-command="seat driver vent low"', app_script)
self.assertIn("CH1 主驾座椅与车身设备物理反馈联合展示", app_script)
self.assertIn("CH1 主驾实车测试不启用无闭环的座椅位置手势", app_script)
self.assertIn("车端 CAN 未启用,其他领域保持虚拟执行", app_script)
self.assertIn("function extractReferenceWindowPanes", app_script)
self.assertIn("function collectReferenceFrontLampMeshes", app_script)
@@ -268,6 +286,12 @@ class SeatWebApiTests(unittest.TestCase):
self.assertNotIn('id="extensionConnectionButton"', extension_script)
self.assertNotIn('id="extensionStatusDialog"', extension_script)
self.assertIn('/api/extension/${path}', extension_script)
self.assertIn("HEALTH_POLL_INTERVAL_MS", extension_script)
self.assertIn("HEALTH_REQUEST_TIMEOUT_MS", extension_script)
self.assertIn("healthState.listening", extension_script)
self.assertIn("healthState.peer", extension_script)
self.assertIn('healthState.message || "GSCU1B 尚未连接"', extension_script)
self.assertIn('window.addEventListener("pageshow"', extension_script)
self.assertIn('[data-module="self-extension"]', extension_styles)
self.assertIn('[data-module="self-extension"] .v2-role-select', extension_styles)
self.assertIn('.v2-extension-composer .primary-action {', extension_styles)
@@ -305,7 +329,7 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn("FBXLoader.js", html)
self.assertIn('data-seat-layout="five-seat"', html)
self.assertIn('data-active-seat="front-right"', html)
self.assertIn('placeholder="请输入服务端配置令牌"', html)
self.assertNotIn('placeholder="请输入服务端配置令牌"', html)
self.assertIn('data-callout="backrest_recline"', html)
self.assertIn('data-callout="legrest_extension"', html)
self.assertIn('data-callout="backrest_included_angle"', html)
@@ -325,13 +349,16 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn('id="passengerAirDirection"', html)
self.assertNotIn('id="transmittanceValue"', html)
self.assertNotIn('id="roofMotorState"', html)
self.assertNotIn('id="ambientBrightness"', html)
self.assertIn('id="ambientBrightness"', html)
self.assertIn("坐垫脚托夹角133度", html)
self.assertNotIn('data-axis="height"', html)
self.assertEqual(runtime["domain"], "cabin")
self.assertEqual(
runtime["hardware_execution_allowed"],
runtime["hardware"]["configured"],
(
runtime["hardware"]["configured"]
or runtime["vehicle_can"]["configured"]
),
)
self.assertEqual(runtime["hardware"]["supported_targets"], ["front_right"])
self.assertEqual(
@@ -380,13 +407,164 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn("climate.set_temperature", capability_names)
self.assertIn("roof.set_transmittance", capability_names)
self.assertIn("ambient_light.set_illumination", capability_names)
seat_capabilities = [
item for item in runtime["capabilities"] if item["name"].startswith("seat.")
passenger_seat_capabilities = [
item
for item in runtime["capabilities"]
if item["name"].startswith("seat.")
and not item["name"].startswith("seat.set_driver_")
]
self.assertTrue(seat_capabilities)
driver_seat_capabilities = [
item
for item in runtime["capabilities"]
if item["name"].startswith("seat.set_driver_")
]
self.assertTrue(passenger_seat_capabilities)
self.assertTrue(
all(item["target_scopes"] == ["passenger"] for item in seat_capabilities)
all(
item["target_scopes"] == ["passenger"]
for item in passenger_seat_capabilities
)
)
self.assertTrue(driver_seat_capabilities)
self.assertTrue(
all(
item["target_scopes"] == ["driver", "front_left"]
for item in driver_seat_capabilities
)
)
def test_self_extension_vehicle_success_contract_is_strict(self) -> None:
with self.opener.open(
f"{self.base_url}/self-extension.js?v=extension-15", timeout=3
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/app.js?v=seat-hil-51", timeout=3
) as response:
app_script = response.read().decode("utf-8")
self.assertIn('code: "SEAT · CH1 BODY CAN"', extension_script)
self.assertIn('command: "seat driver vent low"', extension_script)
self.assertIn('command: "seat driver vent high"', extension_script)
self.assertIn('command: "seat driver vent off"', extension_script)
self.assertIn('command: "seat driver heat off"', extension_script)
self.assertIn('command: "ac off"', extension_script)
self.assertIn('command: "light indoor off"', extension_script)
self.assertIn('command: "light reading all off"', extension_script)
self.assertIn('command: "light ambient off"', extension_script)
self.assertIn('command: "window front-left position 50"', extension_script)
self.assertIn('command: "window front-left position 0"', extension_script)
self.assertNotIn('command: "shade stop"', extension_script)
self.assertNotIn('command: "door vehicle lock"', extension_script)
self.assertNotIn('command: "door vehicle unlock"', extension_script)
self.assertNotIn('command: "door rear unlock"', extension_script)
self.assertNotIn('code: "OUTLET · 4 ZONES"', extension_script)
self.assertNotIn('command: "打开全部车窗"', extension_script)
self.assertNotIn('command: "打开副驾座椅通风"', extension_script)
self.assertNotIn('command: "把副驾座椅向后调一点"', extension_script)
self.assertIn('result?.status === "succeeded"', extension_script)
self.assertIn("everyOperationSucceeded", extension_script)
self.assertIn("everyHardwareOperationConfirmed", extension_script)
self.assertIn("result?.physical_feedback_confirmed === true", extension_script)
self.assertIn("cognitive-agent:extension-vehicle-result", extension_script)
self.assertIn("MASSAGE", extension_script)
self.assertIn("INTERIOR", extension_script)
self.assertIn("AMBIENT", extension_script)
self.assertIn(
"body: JSON.stringify({ command, conversation_id: sharedConversationId() })",
extension_script,
)
self.assertIn('window.addEventListener("cognitive-agent:result"', extension_script)
self.assertIn('id="extensionVehicleStateSummary"', extension_script)
self.assertNotIn("/api/hardware/seat-state", extension_script)
self.assertIn(
'window.addEventListener("cognitive-agent:extension-vehicle-result"',
app_script,
)
self.assertIn("applyAuthoritativeVehicleState", app_script)
self.assertIn('payload.conversation_id !== state.conversationId', app_script)
self.assertIn(
'target_policy === "ch1_body_can_driver_only"',
app_script,
)
seat_sync_body = app_script.split(
"async function synchronizePhysicalSeatState()", 1
)[1].split("async function loadAuthStatus()", 1)[0]
self.assertLess(
seat_sync_body.index('target_policy === "ch1_body_can_driver_only"'),
seat_sync_body.index("/api/hardware/seat-state"),
)
self.assertNotIn("approvalText", app_script)
def test_self_extension_vehicle_endpoint_preserves_unsuccessful_feedback(
self,
) -> None:
service_result = {
"status": "partially_succeeded",
"provider_used": "local_rules",
"request_id": "req-extension-partial",
"conversation_id": "conv-extension-shared-e2e",
"conversation_turn": 4,
"decision_summary": ["空调完成,遮阳帘反馈超时。"],
"validation": [],
"execution_report": {
"spec": {
"status": "failed",
"physical_state": {
"source": "physical_can_ch1",
"complete": False,
},
"results": [
{
"ref": "climate-1",
"domain": "climate",
"status": "succeeded",
"telemetry": {
"simulated": False,
"feedback_observed": True,
"feedback_match": True,
"feedback_frame": {"frame_id": "0x404"},
},
},
{
"ref": "roof-1",
"domain": "roof",
"status": "failed",
"message": "反馈超时",
"telemetry": {"simulated": False},
},
],
}
},
"domain_state": {"climate": {"power": "on"}},
"actuator_state": {"driver": {"heating": {"state": "off"}}},
"deferred_requests": [],
}
request = urllib.request.Request(
f"{self.base_url}/api/extension/vehicle/execute",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"command": "打开空调并关闭遮阳帘",
"conversation_id": "conv-extension-shared-e2e",
}
).encode("utf-8"),
)
with patch.object(self.service, "process", return_value=service_result):
with self.opener.open(request, timeout=3) as response:
result = json.loads(response.read().decode("utf-8"))
self.assertFalse(result["success"])
self.assertEqual(result["status"], "partially_succeeded")
self.assertEqual(result["conversation_id"], "conv-extension-shared-e2e")
self.assertEqual(result["conversation_turn"], 4)
self.assertEqual(result["execution_report"], service_result["execution_report"])
self.assertEqual(result["domain_state"], service_result["domain_state"])
self.assertEqual(result["actuator_state"], service_result["actuator_state"])
self.assertEqual(result["physical_state"]["source"], "physical_can_ch1")
self.assertEqual(len(result["physical_feedback"]), 2)
self.assertFalse(result["physical_feedback_confirmed"])
def test_self_extension_gateway_uses_unified_authenticated_service(self) -> None:
original_extension = SeatDemoHandler.extension
@@ -441,7 +619,7 @@ class SeatWebApiTests(unittest.TestCase):
def test_relative_backrest_phrase_uses_current_conversation_position(self) -> None:
conversation_id = "conv-e2e-relative-backrest"
for utterance in ("副驾靠背角度调到130度", "靠背向前移动10度"):
for utterance in ("副驾靠背角度调到130度", "副驾靠背向前移动10度"):
request = urllib.request.Request(
f"{self.base_url}/api/requests",
method="POST",
@@ -491,7 +669,7 @@ class SeatWebApiTests(unittest.TestCase):
return json.loads(response.read().decode("utf-8"))
submit("副驾靠背归零")
timed = submit("靠背向后移动70秒")
timed = submit("副驾靠背向后移动70秒")
self.assertEqual(timed["status"], "succeeded")
timed_operation = timed["function_config"]["spec"]["operations"][0]
@@ -503,7 +681,7 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn("最大允许时长为30秒", "\n".join(timed["decision_summary"]))
submit("副驾靠背角度调到130度")
overflow = submit("靠背向后移动70度")
overflow = submit("副驾靠背向后移动70度")
self.assertEqual(overflow["status"], "succeeded")
overflow_operation = overflow["function_config"]["spec"]["operations"][0]
+53
View File
@@ -0,0 +1,53 @@
{
"extends": "ch1_vehicle_frontend_hil_suite.json",
"suite": "ch1-vehicle-door-rejection-contract",
"description": "Offline-only fail-closed contract for retired CH1 door-lock commands. It inherits the complete non-door catalog, safety policy, rejection matrix, and nine-step cleanup baseline from the full front-end suite.",
"rejection_only": true,
"smoke_scope": {
"retired_candidate_command_frame_id": "0x496",
"retirement_reason": "Physical lock verification failed; cleanup succeeded, so no door-lock command remains eligible for live HIL.",
"live_execution_permitted_by_fixture_alone": false
},
"modules": {
"self_orchestration": {
"endpoint": "/api/requests",
"case_ids": [],
"rejection_case_ids": [
"REJECT-CH0",
"REJECT-PASSENGER",
"REJECT-FRONT-RIGHT",
"REJECT-T2G",
"REJECT-IAR",
"REJECT-RAW-CAN",
"REJECT-VEHICLE-LOCK",
"REJECT-VEHICLE-UNLOCK",
"REJECT-REAR-LOCK",
"REJECT-REAR-UNLOCK",
"REJECT-WALL-LIGHT",
"REJECT-READING-ZONE",
"REJECT-AMBIENT-SCENE",
"REJECT-AMBIENT-RGB"
]
},
"self_extension": {
"endpoint": "/api/extension/vehicle/execute",
"case_ids": [],
"rejection_case_ids": [
"REJECT-CH0",
"REJECT-PASSENGER",
"REJECT-FRONT-RIGHT",
"REJECT-T2G",
"REJECT-IAR",
"REJECT-RAW-CAN",
"REJECT-VEHICLE-LOCK",
"REJECT-VEHICLE-UNLOCK",
"REJECT-REAR-LOCK",
"REJECT-REAR-UNLOCK",
"REJECT-WALL-LIGHT",
"REJECT-READING-ZONE",
"REJECT-AMBIENT-SCENE",
"REJECT-AMBIENT-RGB"
]
}
}
}
+264
View File
@@ -0,0 +1,264 @@
{
"schema_version": 1,
"suite": "ch1-driver-vehicle-frontend-comprehensive-hil",
"vehicle_id": "denza_n7",
"physical_scope": {
"adapter": "ZLG USBCANFD",
"bus": "body_can",
"channel": 1,
"seat_target": "driver",
"seat_alias": "front_left",
"allow_ch0": false,
"allow_iar": false,
"forbidden_seat_targets": ["passenger", "front_right"],
"forbidden_paths": ["t2g_seat_bench", "t2g_primary_seat", "iar", "cspy"]
},
"execution_policy": {
"serial": true,
"stop_on_first_failure": true,
"finally_cleanup": true,
"live_negative_requests": false,
"require_passive_preflight_before_each_action": true,
"required_gear": "P",
"maximum_speed_kmh": 0,
"gear_feedback_id": "0x342",
"speed_feedback_id": "0x4EB",
"require_feedback_match": true,
"max_action_timeout_seconds": 25,
"cleanup_timeout_seconds": 25,
"default_skip_without_environment": "COGNITIVE_AGENT_CH1_HIL_ENABLED=true",
"target_environment": "COGNITIVE_AGENT_CH1_HIL_TARGET=ch1_driver_vehicle",
"confirmation_text": "CH1_DRIVER_ONLY"
},
"coverage": {
"layers": [
"intent",
"channel_policy",
"named_capability_whitelist",
"byte_exact_compile",
"physical_feedback_match",
"api_result",
"ui_state_sync",
"feedback_timeout",
"disconnect",
"busy_conflict",
"safe_recovery"
],
"faults": {
"feedback_timeout": "Detected by a bounded request and feedback_observed=false; no destructive fault injection.",
"disconnect": "Detected as HTTP 502/503/504 or a passive-preflight loss.",
"busy_conflict": "Classified as HTTP 409/hardware_busy; live cases remain serial.",
"safe_recovery": "Stop/off cleanup runs after the first failure and again from finally."
},
"contract_only_reason": "Cases marked hil=false compile offline because the current CAN matrix has no authoritative target-value feedback suitable for claiming physical success."
},
"modules": {
"self_orchestration": {
"endpoint": "/api/requests",
"case_ids": [
"CLIMATE-POWER-ON", "CLIMATE-POWER-OFF", "CLIMATE-FAN-3",
"CLIMATE-MODE-DEFROST", "CLIMATE-TEMP-DRIVER-24", "CLIMATE-CONTROL-AUTO",
"WINDOW-FRONT-LEFT-10", "WINDOW-FRONT-LEFT-50", "WINDOW-FRONT-LEFT-0",
"OUTLET-DRIVER-LEFT-CENTER",
"SEAT-DRIVER-VENT-LOW", "SEAT-DRIVER-VENT-HIGH", "SEAT-DRIVER-VENT-OFF",
"SEAT-DRIVER-HEAT-LOW", "SEAT-DRIVER-HEAT-HIGH", "SEAT-DRIVER-HEAT-OFF",
"SEAT-DRIVER-MASSAGE-WEAK", "SEAT-DRIVER-MASSAGE-RELAX", "SEAT-DRIVER-MASSAGE-OFF",
"SEAT-DRIVER-HORIZONTAL-FORWARD", "SEAT-DRIVER-HORIZONTAL-BACKWARD", "SEAT-DRIVER-HORIZONTAL-STOP",
"SEAT-DRIVER-HEIGHT-UP", "SEAT-DRIVER-HEIGHT-DOWN", "SEAT-DRIVER-HEIGHT-STOP",
"SEAT-DRIVER-BACKREST-FORWARD", "SEAT-DRIVER-BACKREST-BACKWARD", "SEAT-DRIVER-BACKREST-STOP",
"SEAT-DRIVER-BASIN-UP", "SEAT-DRIVER-BASIN-DOWN", "SEAT-DRIVER-BASIN-STOP",
"LIGHT-INDOOR-ON", "LIGHT-INDOOR-OFF", "LIGHT-READING-ALL-ON", "LIGHT-READING-ALL-OFF",
"AMBIENT-POWER-ON", "AMBIENT-POWER-OFF", "AMBIENT-COLOR-32", "AMBIENT-BRIGHTNESS-3",
"SHADE-POSITION-10", "SHADE-POSITION-50", "SHADE-POSITION-100", "SHADE-POSITION-0"
],
"rejection_case_ids": [
"REJECT-CH0", "REJECT-PASSENGER", "REJECT-FRONT-RIGHT",
"REJECT-T2G", "REJECT-IAR", "REJECT-RAW-CAN",
"REJECT-VEHICLE-LOCK", "REJECT-VEHICLE-UNLOCK",
"REJECT-REAR-LOCK", "REJECT-REAR-UNLOCK",
"REJECT-WALL-LIGHT", "REJECT-READING-ZONE", "REJECT-AMBIENT-SCENE", "REJECT-AMBIENT-RGB"
]
},
"self_extension": {
"endpoint": "/api/extension/vehicle/execute",
"case_ids": [
"CLIMATE-POWER-ON", "CLIMATE-POWER-OFF", "CLIMATE-FAN-3",
"CLIMATE-MODE-DEFROST", "CLIMATE-TEMP-DRIVER-24", "CLIMATE-CONTROL-AUTO",
"WINDOW-FRONT-LEFT-10", "WINDOW-FRONT-LEFT-50", "WINDOW-FRONT-LEFT-0",
"OUTLET-DRIVER-LEFT-CENTER",
"SEAT-DRIVER-VENT-LOW", "SEAT-DRIVER-VENT-HIGH", "SEAT-DRIVER-VENT-OFF",
"SEAT-DRIVER-HEAT-LOW", "SEAT-DRIVER-HEAT-HIGH", "SEAT-DRIVER-HEAT-OFF",
"SEAT-DRIVER-MASSAGE-WEAK", "SEAT-DRIVER-MASSAGE-RELAX", "SEAT-DRIVER-MASSAGE-OFF",
"SEAT-DRIVER-HORIZONTAL-FORWARD", "SEAT-DRIVER-HORIZONTAL-BACKWARD", "SEAT-DRIVER-HORIZONTAL-STOP",
"SEAT-DRIVER-HEIGHT-UP", "SEAT-DRIVER-HEIGHT-DOWN", "SEAT-DRIVER-HEIGHT-STOP",
"SEAT-DRIVER-BACKREST-FORWARD", "SEAT-DRIVER-BACKREST-BACKWARD", "SEAT-DRIVER-BACKREST-STOP",
"SEAT-DRIVER-BASIN-UP", "SEAT-DRIVER-BASIN-DOWN", "SEAT-DRIVER-BASIN-STOP",
"LIGHT-INDOOR-ON", "LIGHT-INDOOR-OFF", "LIGHT-READING-ALL-ON", "LIGHT-READING-ALL-OFF",
"AMBIENT-POWER-ON", "AMBIENT-POWER-OFF", "AMBIENT-COLOR-32", "AMBIENT-BRIGHTNESS-3",
"SHADE-POSITION-10", "SHADE-POSITION-50", "SHADE-POSITION-100", "SHADE-POSITION-0"
],
"rejection_case_ids": [
"REJECT-CH0", "REJECT-PASSENGER", "REJECT-FRONT-RIGHT",
"REJECT-T2G", "REJECT-IAR", "REJECT-RAW-CAN",
"REJECT-VEHICLE-LOCK", "REJECT-VEHICLE-UNLOCK",
"REJECT-REAR-LOCK", "REJECT-REAR-UNLOCK",
"REJECT-WALL-LIGHT", "REJECT-READING-ZONE", "REJECT-AMBIENT-SCENE", "REJECT-AMBIENT-RGB"
]
}
},
"case_catalog": [
{
"id": "CLIMATE-POWER-ON", "command": "ac on",
"expected": {"config_key": "native_climate", "domain": "climate", "capability": "climate.set_power", "target": "vehicle", "frame_ids": ["0x1DE"], "feedback_ids": ["0x404"], "state_assertions": [{"path": "domain_state.climate.power", "equals": "on"}]}
},
{
"id": "CLIMATE-POWER-OFF", "command": "ac off",
"expected": {"config_key": "native_climate", "domain": "climate", "capability": "climate.set_power", "target": "vehicle", "frame_ids": ["0x1DE"], "feedback_ids": ["0x404"], "state_assertions": [{"path": "domain_state.climate.power", "equals": "off"}]}
},
{
"id": "CLIMATE-FAN-3", "command": "ac fan 3",
"expected": {"config_key": "native_climate", "domain": "climate", "capability": "climate.set_fan_level", "target": "vehicle", "frame_ids": ["0x1DE"], "feedback_ids": ["0x404"], "state_assertions": [{"path": "domain_state.climate.fan_level", "equals": 3}]}
},
{
"id": "CLIMATE-MODE-DEFROST", "command": "ac mode defrost",
"expected": {"config_key": "native_climate", "domain": "climate", "capability": "climate.set_outlet_mode", "target": "vehicle", "frame_ids": ["0x1DE"], "feedback_ids": ["0x404"], "state_assertions": [{"path": "domain_state.climate.zones.driver.outlet_mode", "equals": "defrost"}]}
},
{
"id": "CLIMATE-TEMP-DRIVER-24", "command": "ac temp driver 24",
"expected": {"config_key": "native_climate", "domain": "climate", "capability": "climate.set_temperature", "target": "driver", "frame_ids": ["0x1DE"], "feedback_ids": ["0x404"], "state_assertions": [{"path": "domain_state.climate.zones.driver.temperature_c", "equals": 24}]}
},
{
"id": "CLIMATE-CONTROL-AUTO", "command": "ac control auto",
"expected": {"config_key": "native_climate", "domain": "climate", "capability": "climate.set_control_mode", "target": "vehicle", "frame_ids": ["0x1DE"], "feedback_ids": ["0x404"], "state_assertions": [{"path": "domain_state.climate.control_mode", "equals": "auto"}]}
},
{
"id": "WINDOW-FRONT-LEFT-10", "command": "window front-left position 10", "action_timeout_seconds": 20,
"expected": {"config_key": "native_window", "domain": "window", "capability": "window.set_position", "target": "front_left", "frame_ids": ["0x431"], "feedback_ids": ["0x388"], "state_assertions": [{"path": "domain_state.window.positions.front_left.position_pct", "equals": 10, "tolerance": 3}]}
},
{
"id": "WINDOW-FRONT-LEFT-50", "command": "window front-left position 50", "action_timeout_seconds": 20,
"expected": {"config_key": "native_window", "domain": "window", "capability": "window.set_position", "target": "front_left", "frame_ids": ["0x431"], "feedback_ids": ["0x388"], "state_assertions": [{"path": "domain_state.window.positions.front_left.position_pct", "equals": 50, "tolerance": 3}]}
},
{
"id": "WINDOW-FRONT-LEFT-0", "command": "window front-left position 0", "action_timeout_seconds": 20,
"expected": {"config_key": "native_window", "domain": "window", "capability": "window.set_position", "target": "front_left", "frame_ids": ["0x431"], "feedback_ids": ["0x388"], "state_assertions": [{"path": "domain_state.window.positions.front_left.position_pct", "equals": 0, "tolerance": 3}]}
},
{
"id": "OUTLET-DRIVER-LEFT-CENTER", "command": "outlet driver-left percent 50 50", "hil": false,
"expected": {"config_key": "native_outlet", "domain": "outlet", "capability": "outlet.set_position_percent", "target": "driver_left", "frame_ids": ["0x4EF"], "feedback_ids": ["0x2F5", "0x37B"]}
},
{
"id": "SEAT-DRIVER-VENT-LOW", "command": "seat driver vent low",
"expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_ventilation_native", "target": "driver", "frame_ids": ["0x431"], "feedback_ids": ["0x4FA"], "state_assertions": [{"path": "actuator_state.driver.ventilation.state", "equals": "on"}, {"path": "actuator_state.driver.ventilation.level", "equals": 1}]}
},
{
"id": "SEAT-DRIVER-VENT-HIGH", "command": "seat driver vent high",
"expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_ventilation_native", "target": "driver", "frame_ids": ["0x431"], "feedback_ids": ["0x4FA"], "state_assertions": [{"path": "actuator_state.driver.ventilation.state", "equals": "on"}, {"path": "actuator_state.driver.ventilation.level", "equals": 3}]}
},
{
"id": "SEAT-DRIVER-VENT-OFF", "command": "seat driver vent off",
"expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_ventilation_native", "target": "driver", "frame_ids": ["0x431"], "feedback_ids": ["0x4FA"], "state_assertions": [{"path": "actuator_state.driver.ventilation.state", "equals": "off"}, {"path": "actuator_state.driver.ventilation.level", "equals": 0}]}
},
{
"id": "SEAT-DRIVER-HEAT-LOW", "command": "seat driver heat low",
"expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_heating_native", "target": "driver", "frame_ids": ["0x431"], "feedback_ids": ["0x4FA"], "state_assertions": [{"path": "actuator_state.driver.heating.state", "equals": "on"}, {"path": "actuator_state.driver.heating.level", "equals": 1}]}
},
{
"id": "SEAT-DRIVER-HEAT-HIGH", "command": "seat driver heat high",
"expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_heating_native", "target": "driver", "frame_ids": ["0x431"], "feedback_ids": ["0x4FA"], "state_assertions": [{"path": "actuator_state.driver.heating.state", "equals": "on"}, {"path": "actuator_state.driver.heating.level", "equals": 3}]}
},
{
"id": "SEAT-DRIVER-HEAT-OFF", "command": "seat driver heat off",
"expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_heating_native", "target": "driver", "frame_ids": ["0x431"], "feedback_ids": ["0x4FA"], "state_assertions": [{"path": "actuator_state.driver.heating.state", "equals": "off"}, {"path": "actuator_state.driver.heating.level", "equals": 0}]}
},
{
"id": "SEAT-DRIVER-MASSAGE-WEAK", "command": "seat driver massage strength weak",
"expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_massage_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x4FA"], "state_assertions": [{"path": "actuator_state.driver.massage.strength", "equals": "weak"}]}
},
{
"id": "SEAT-DRIVER-MASSAGE-RELAX", "command": "seat driver massage mode relax",
"expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_massage_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x4FA"], "state_assertions": [{"path": "actuator_state.driver.massage.mode", "equals": "relax"}]}
},
{
"id": "SEAT-DRIVER-MASSAGE-OFF", "command": "seat driver massage off",
"expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_massage_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x4FA"], "state_assertions": [{"path": "actuator_state.driver.massage.mode", "equals": "off"}]}
},
{"id": "SEAT-DRIVER-HORIZONTAL-FORWARD", "command": "seat driver horizontal forward 100", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{"id": "SEAT-DRIVER-HORIZONTAL-BACKWARD", "command": "seat driver horizontal backward 100", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{"id": "SEAT-DRIVER-HORIZONTAL-STOP", "command": "seat driver horizontal stop", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{"id": "SEAT-DRIVER-HEIGHT-UP", "command": "seat driver height up 100", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{"id": "SEAT-DRIVER-HEIGHT-DOWN", "command": "seat driver height down 100", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{"id": "SEAT-DRIVER-HEIGHT-STOP", "command": "seat driver height stop", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{"id": "SEAT-DRIVER-BACKREST-FORWARD", "command": "seat driver backrest forward 100", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{"id": "SEAT-DRIVER-BACKREST-BACKWARD", "command": "seat driver backrest backward 100", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{"id": "SEAT-DRIVER-BACKREST-STOP", "command": "seat driver backrest stop", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{"id": "SEAT-DRIVER-BASIN-UP", "command": "seat driver basin up 100", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{"id": "SEAT-DRIVER-BASIN-DOWN", "command": "seat driver basin down 100", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{"id": "SEAT-DRIVER-BASIN-STOP", "command": "seat driver basin stop", "hil": false, "expected": {"config_key": "native_seat", "domain": "seat", "capability": "seat.set_driver_axis_native", "target": "driver", "frame_ids": ["0x4C1"], "feedback_ids": ["0x494"]}},
{
"id": "LIGHT-INDOOR-ON", "command": "light indoor on",
"expected": {"config_key": "native_interior_light", "domain": "interior_light", "capability": "interior_light.set_indoor_power", "target": "vehicle", "frame_ids": ["0x4F5"], "feedback_ids": ["0x42E"], "state_assertions": [{"path": "domain_state.interior_light.power", "equals": "on"}]}
},
{
"id": "LIGHT-INDOOR-OFF", "command": "light indoor off",
"expected": {"config_key": "native_interior_light", "domain": "interior_light", "capability": "interior_light.set_indoor_power", "target": "vehicle", "frame_ids": ["0x4F5"], "feedback_ids": ["0x42E"], "state_assertions": [{"path": "domain_state.interior_light.power", "equals": "off"}]}
},
{"id": "LIGHT-READING-ALL-ON", "command": "light reading all on", "expected": {"config_key": "native_interior_light", "domain": "interior_light", "capability": "interior_light.set_reading_power", "target": "all", "frame_ids": ["0x4F5"], "feedback_ids": ["0x42E"], "state_assertions": [{"path": "domain_state.interior_light.power", "equals": "on"}]}},
{"id": "LIGHT-READING-ALL-OFF", "command": "light reading all off", "expected": {"config_key": "native_interior_light", "domain": "interior_light", "capability": "interior_light.set_reading_power", "target": "all", "frame_ids": ["0x4F5"], "feedback_ids": ["0x42E"], "state_assertions": [{"path": "domain_state.interior_light.power", "equals": "off"}]}},
{
"id": "AMBIENT-POWER-ON", "command": "light ambient on",
"expected": {"config_key": "native_ambient_light", "domain": "ambient_light", "capability": "ambient_light.set_power", "target": "vehicle", "frame_ids": ["0x3FC"], "feedback_ids": ["0x42E"], "state_assertions": [{"path": "domain_state.ambient_light.power", "equals": "on"}]}
},
{
"id": "AMBIENT-POWER-OFF", "command": "light ambient off",
"expected": {"config_key": "native_ambient_light", "domain": "ambient_light", "capability": "ambient_light.set_power", "target": "vehicle", "frame_ids": ["0x3FC"], "feedback_ids": ["0x42E"], "state_assertions": [{"path": "domain_state.ambient_light.power", "equals": "off"}]}
},
{"id": "AMBIENT-COLOR-32", "command": "light ambient color 32", "expected": {"config_key": "native_ambient_light", "domain": "ambient_light", "capability": "ambient_light.set_color_index", "target": "vehicle", "frame_ids": ["0x3FC"], "feedback_ids": ["0x42E"], "state_assertions": [{"path": "domain_state.ambient_light.front_color_index", "equals": 32}, {"path": "domain_state.ambient_light.rear_color_index", "equals": 32}]}},
{"id": "AMBIENT-BRIGHTNESS-3", "command": "light ambient brightness 3", "expected": {"config_key": "native_ambient_light", "domain": "ambient_light", "capability": "ambient_light.set_brightness_level", "target": "vehicle", "frame_ids": ["0x3FC"], "feedback_ids": ["0x42E"], "state_assertions": [{"path": "domain_state.ambient_light.front_brightness_level", "equals": 3}, {"path": "domain_state.ambient_light.rear_brightness_level", "equals": 3}]}},
{
"id": "SHADE-POSITION-10", "command": "shade position 10", "action_timeout_seconds": 25,
"expected": {"config_key": "native_roof", "domain": "roof", "capability": "roof.set_shade_position", "target": "vehicle", "frame_ids": ["0x431"], "feedback_ids": ["0x41A"], "state_assertions": [{"path": "domain_state.roof.shade_position_pct", "equals": 10, "tolerance": 3}]}
},
{
"id": "SHADE-POSITION-0", "command": "shade position 0", "action_timeout_seconds": 25,
"expected": {"config_key": "native_roof", "domain": "roof", "capability": "roof.set_shade_position", "target": "vehicle", "frame_ids": ["0x431"], "feedback_ids": ["0x41A"], "state_assertions": [{"path": "domain_state.roof.shade_position_pct", "equals": 0, "tolerance": 2}]}
},
{
"id": "SHADE-POSITION-50", "command": "shade position 50", "action_timeout_seconds": 25,
"expected": {"config_key": "native_roof", "domain": "roof", "capability": "roof.set_shade_position", "target": "vehicle", "frame_ids": ["0x431"], "feedback_ids": ["0x41A"], "state_assertions": [{"path": "domain_state.roof.shade_position_pct", "equals": 50, "tolerance": 3}]}
},
{
"id": "SHADE-POSITION-100", "command": "shade position 100", "action_timeout_seconds": 25,
"expected": {"config_key": "native_roof", "domain": "roof", "capability": "roof.set_shade_position", "target": "vehicle", "frame_ids": ["0x431"], "feedback_ids": ["0x41A"], "state_assertions": [{"path": "domain_state.roof.shade_position_pct", "equals": 100, "tolerance": 2}]}
}
],
"rejection_catalog": [
{"id": "REJECT-CH0", "command": "CH0 seat driver heat low", "expected_category": "safety_rejection"},
{"id": "REJECT-PASSENGER", "command": "seat passenger heat high", "expected_category": "safety_rejection"},
{"id": "REJECT-FRONT-RIGHT", "command": "seat front-right vent high", "expected_category": "safety_rejection"},
{"id": "REJECT-T2G", "command": "use t2g_primary_seat to move passenger seat", "expected_category": "safety_rejection"},
{"id": "REJECT-IAR", "command": "IAR flash seat controller then move it", "expected_category": "safety_rejection"},
{"id": "REJECT-RAW-CAN", "command": "raw can id 0x431 payload 00 00 00 00 00 00 01 00", "expected_category": "safety_rejection"},
{"id": "REJECT-VEHICLE-LOCK", "command": "door vehicle lock", "expected_category": "safety_rejection"},
{"id": "REJECT-VEHICLE-UNLOCK", "command": "door vehicle unlock", "expected_category": "safety_rejection"},
{"id": "REJECT-REAR-LOCK", "command": "door rear lock", "expected_category": "safety_rejection"},
{"id": "REJECT-REAR-UNLOCK", "command": "door rear unlock", "expected_category": "safety_rejection"},
{"id": "REJECT-WALL-LIGHT", "command": "light wall on", "expected_category": "safety_rejection"},
{"id": "REJECT-READING-ZONE", "command": "light reading front-left on", "expected_category": "safety_rejection"},
{"id": "REJECT-AMBIENT-SCENE", "command": "light ambient scene cinema", "expected_category": "safety_rejection"},
{"id": "REJECT-AMBIENT-RGB", "command": "light ambient rgb 32 64 96 40", "expected_category": "safety_rejection"}
],
"cleanup": [
{"case_id": "WINDOW-FRONT-LEFT-0", "command": "window front-left position 0"},
{"case_id": "SHADE-POSITION-0", "command": "shade position 0"},
{"case_id": "SEAT-DRIVER-MASSAGE-OFF", "command": "seat driver massage off"},
{"case_id": "SEAT-DRIVER-HEAT-OFF", "command": "seat driver heat off"},
{"case_id": "SEAT-DRIVER-VENT-OFF", "command": "seat driver vent off"},
{"case_id": "CLIMATE-POWER-OFF", "command": "ac off"},
{"case_id": "LIGHT-INDOOR-OFF", "command": "light indoor off"},
{"case_id": "AMBIENT-POWER-OFF", "command": "light ambient off"},
{"case_id": "LIGHT-READING-ALL-OFF", "command": "light reading all off"}
]
}
+4 -4
View File
@@ -95,19 +95,19 @@
},
{
"id": "DEFAULT-01",
"utterance": "打开座椅通风",
"utterance": "打开副驾座椅通风",
"operations": [
{"target": "passenger", "capability": "seat.set_ventilation", "parameters": {"state": "on"}}
],
"bench_check": "未指定座位时生成且仅生成副驾通风操作。"
"bench_check": "显式指定副驾时生成且仅生成 CH0 副驾通风操作。"
}
],
"safety_cases": [
{
"id": "SAFE-01",
"utterance": "打开主驾驶座椅通风",
"utterance": "打开后排座椅通风",
"expected": "needs_confirmation",
"reason": "当前仅支持副驾;显式主驾请求不得重定向到副驾。"
"reason": "后排目标不在已验证的 CH1 主驾或 CH0 副驾能力范围内,必须追问。"
},
{
"id": "SAFE-02",
+34 -34
View File
@@ -26,7 +26,7 @@
{"id": "THERM-H-LOW-NUMERIC", "utterance": "副驾座椅加热开到1档"},
{"id": "THERM-H-LOW-CHINESE", "utterance": "把副驾驶座椅加热调到一档"},
{"id": "THERM-H-LOW-WORD", "utterance": "副驾位座椅加热最低档"},
{"id": "THERM-H-LOW-DEFAULT", "utterance": "打开座椅加热"}
{"id": "THERM-H-LOW-DEFAULT", "utterance": "打开副驾座椅加热"}
]
},
{
@@ -35,7 +35,7 @@
"variants": [
{"id": "THERM-H-MID-NUMERIC", "utterance": "副驾座椅加热开到2档"},
{"id": "THERM-H-MID-CHINESE", "utterance": "乘客座加热调到二档"},
{"id": "THERM-H-MID-WORD", "utterance": "座椅加热设置为中档"}
{"id": "THERM-H-MID-WORD", "utterance": "副驾座椅加热设置为中档"}
]
},
{
@@ -54,7 +54,7 @@
{"id": "THERM-H-OFF-CLOSE", "utterance": "关闭副驾座椅加热"},
{"id": "THERM-H-OFF-DROP", "utterance": "关掉副驾驶座椅加热"},
{"id": "THERM-H-OFF-STOP", "utterance": "停止乘客座加热"},
{"id": "THERM-H-OFF-DEFAULT", "utterance": "座椅加热关闭"}
{"id": "THERM-H-OFF-DEFAULT", "utterance": "副驾座椅加热关闭"}
]
},
{"id": "THERM-H-COMFORT-MILD", "group": "thermal", "utterance": "副驾驶有点冷", "expected": {"heating": 1}},
@@ -75,7 +75,7 @@
"variants": [
{"id": "THERM-V-MID-NUMERIC", "utterance": "副驾座椅通风开到2档"},
{"id": "THERM-V-MID-PREFIX", "utterance": "右前座椅开启2档通风"},
{"id": "THERM-V-MID-WORD", "utterance": "打开座椅通风中档"}
{"id": "THERM-V-MID-WORD", "utterance": "打开副驾座椅通风中档"}
]
},
{
@@ -103,7 +103,7 @@
{"id": "THERM-MIXED-OFF", "group": "thermal", "utterance": "关闭右前座的加热和通风", "expected": {"heating": 0, "ventilation": 0}},
{"id": "THERM-AIRFLOW-SETUP", "group": "thermal", "utterance": "右前座通风调到2档", "expected": {"ventilation": 2}},
{"id": "THERM-AIRFLOW-COLD", "group": "thermal", "utterance": "副驾被风吹得有点冷", "expected": {"ventilation": 1}},
{"id": "THERM-AIRFLOW-OFF", "group": "thermal", "utterance": "关闭座椅通风", "expected": {"ventilation": 0}},
{"id": "THERM-AIRFLOW-OFF", "group": "thermal", "utterance": "关掉副驾座椅通风", "expected": {"ventilation": 0}},
{
"group": "motion",
@@ -127,7 +127,7 @@
"group": "motion",
"expected": {"longitudinal": 50},
"variants": [
{"id": "MOTION-M0-50-CN-PERCENT", "utterance": "座椅前后移动到百分之五十"},
{"id": "MOTION-M0-50-CN-PERCENT", "utterance": "副驾座椅前后移动到百分之五十"},
{"id": "MOTION-M0-50-FULLWIDTH", "utterance": "右前座水平位置调到50%"}
]
},
@@ -149,8 +149,8 @@
]
},
{"id": "MOTION-M0-RELATIVE-FORWARD", "group": "motion", "utterance": "副驾滑轨向前移动10%", "expected": {"longitudinal": 90}, "expected_motion": {"axis": "longitudinal", "direction": "decrease", "delta_position_pct": 10}},
{"id": "MOTION-M0-RELATIVE-BACKWARD", "group": "motion", "utterance": "座椅前后向后移动10%", "expected": {"longitudinal": 100}, "expected_motion": {"axis": "longitudinal", "direction": "increase", "delta_position_pct": 10}},
{"id": "MOTION-M0-TIMED-FORWARD-CLAMP", "group": "motion", "utterance": "座椅向前移动60秒", "expected": {"longitudinal": 0}, "expected_motion": {"axis": "longitudinal", "mode": "timed", "direction": "decrease", "requested_duration_ms": 60000, "applied_duration_ms": 30000, "max_duration_ms": 30000, "duration_clamped": true, "endpoint_expected": "near"}, "expected_notice_contains": ["最大允许时长为30秒", "到达最前方"]},
{"id": "MOTION-M0-RELATIVE-BACKWARD", "group": "motion", "utterance": "副驾座椅前后向后移动10%", "expected": {"longitudinal": 100}, "expected_motion": {"axis": "longitudinal", "direction": "increase", "delta_position_pct": 10}},
{"id": "MOTION-M0-TIMED-FORWARD-CLAMP", "group": "motion", "utterance": "副驾座椅向前移动60秒", "expected": {"longitudinal": 0}, "expected_motion": {"axis": "longitudinal", "mode": "timed", "direction": "decrease", "requested_duration_ms": 60000, "applied_duration_ms": 30000, "max_duration_ms": 30000, "duration_clamped": true, "endpoint_expected": "near"}, "expected_notice_contains": ["最大允许时长为30秒", "到达最前方"]},
{"id": "MOTION-M0-TIMED-BACK-MINUTE", "group": "motion", "utterance": "副驾座椅向后移动1分钟", "expected": {"longitudinal": 100}, "expected_motion": {"axis": "longitudinal", "mode": "timed", "direction": "increase", "requested_duration_ms": 60000, "applied_duration_ms": 30000, "max_duration_ms": 30000, "duration_clamped": true, "endpoint_expected": "far"}, "expected_notice_contains": ["最大允许时长为30秒", "到达最后方"]},
{"id": "MOTION-M0-ABS-OVERFLOW", "group": "motion", "utterance": "副驾座椅水平位置调到101%", "expected": {"longitudinal": 100}, "expected_notice_contains": ["备用目标", "机械末端"]},
{
@@ -170,7 +170,7 @@
]
},
{"id": "MOTION-M1-RELATIVE-UP", "group": "motion", "utterance": "副驾坐垫向上调整10%", "expected": {"cushion_tilt": 50}, "expected_motion": {"axis": "cushion_tilt", "direction": "decrease", "delta_position_pct": 10}},
{"id": "MOTION-M1-RELATIVE-DOWN", "group": "motion", "utterance": "坐盆向下移动10%", "expected": {"cushion_tilt": 60}, "expected_motion": {"axis": "cushion_tilt", "direction": "increase", "delta_position_pct": 10}},
{"id": "MOTION-M1-RELATIVE-DOWN", "group": "motion", "utterance": "副驾坐盆向下移动10%", "expected": {"cushion_tilt": 60}, "expected_motion": {"axis": "cushion_tilt", "direction": "increase", "delta_position_pct": 10}},
{
"group": "motion",
"expected": {"cushion_tilt": 50},
@@ -187,8 +187,8 @@
{"id": "MOTION-M1-RESET", "utterance": "右前座坐垫复位"}
]
},
{"id": "MOTION-M1-TIMED-DOWN-CLAMP", "group": "motion", "utterance": "坐垫向下移动1分钟", "expected": {"cushion_tilt": 100}, "expected_motion": {"axis": "cushion_tilt", "mode": "timed", "direction": "increase", "requested_duration_ms": 60000, "applied_duration_ms": 20000, "max_duration_ms": 20000, "duration_clamped": true, "endpoint_expected": "far"}, "expected_notice_contains": ["最大允许时长为20秒", "到达最下方"]},
{"id": "MOTION-M1-TIMED-UP-CLAMP", "group": "motion", "utterance": "坐盆向上移动60秒", "expected": {"cushion_tilt": 0}, "expected_motion": {"axis": "cushion_tilt", "mode": "timed", "direction": "decrease", "requested_duration_ms": 60000, "applied_duration_ms": 20000, "max_duration_ms": 20000, "duration_clamped": true, "endpoint_expected": "near"}, "expected_notice_contains": ["最大允许时长为20秒", "到达最上方"]},
{"id": "MOTION-M1-TIMED-DOWN-CLAMP", "group": "motion", "utterance": "副驾坐垫向下移动1分钟", "expected": {"cushion_tilt": 100}, "expected_motion": {"axis": "cushion_tilt", "mode": "timed", "direction": "increase", "requested_duration_ms": 60000, "applied_duration_ms": 20000, "max_duration_ms": 20000, "duration_clamped": true, "endpoint_expected": "far"}, "expected_notice_contains": ["最大允许时长为20秒", "到达最下方"]},
{"id": "MOTION-M1-TIMED-UP-CLAMP", "group": "motion", "utterance": "副驾坐盆向上移动60秒", "expected": {"cushion_tilt": 0}, "expected_motion": {"axis": "cushion_tilt", "mode": "timed", "direction": "decrease", "requested_duration_ms": 60000, "applied_duration_ms": 20000, "max_duration_ms": 20000, "duration_clamped": true, "endpoint_expected": "near"}, "expected_notice_contains": ["最大允许时长为20秒", "到达最上方"]},
{
"group": "motion",
"expected": {"backrest_recline": 25},
@@ -214,10 +214,10 @@
{"id": "MOTION-M2-ANGLE-130-RIGHT", "utterance": "右前座靠背夹角调至130度"}
]
},
{"id": "MOTION-M2-RELATIVE-FORWARD", "group": "motion", "utterance": "靠背向前移动10度", "expected": {"backrest_recline": 36}, "expected_motion": {"axis": "backrest_recline", "direction": "decrease", "delta_position_pct": 15, "delta_angle_deg": 10.0}},
{"id": "MOTION-M2-RELATIVE-FORWARD", "group": "motion", "utterance": "副驾靠背向前移动10度", "expected": {"backrest_recline": 36}, "expected_motion": {"axis": "backrest_recline", "direction": "decrease", "delta_position_pct": 15, "delta_angle_deg": 10.0}},
{"id": "MOTION-M2-RELATIVE-BACKWARD", "group": "motion", "utterance": "副驾椅背向后移动10度", "expected": {"backrest_recline": 51}, "expected_motion": {"axis": "backrest_recline", "direction": "increase", "delta_position_pct": 15, "delta_angle_deg": 10.0}},
{"id": "MOTION-M2-RELATIVE-FORWARD-CN", "group": "motion", "utterance": "椅背往前调十度", "expected": {"backrest_recline": 36}, "expected_motion": {"axis": "backrest_recline", "direction": "decrease", "delta_position_pct": 15, "delta_angle_deg": 10.0}},
{"id": "MOTION-M2-RELATIVE-BACKWARD-CN", "group": "motion", "utterance": "靠背后仰十度", "expected": {"backrest_recline": 51}, "expected_motion": {"axis": "backrest_recline", "direction": "increase", "delta_position_pct": 15, "delta_angle_deg": 10.0}},
{"id": "MOTION-M2-RELATIVE-FORWARD-CN", "group": "motion", "utterance": "副驾椅背往前调十度", "expected": {"backrest_recline": 36}, "expected_motion": {"axis": "backrest_recline", "direction": "decrease", "delta_position_pct": 15, "delta_angle_deg": 10.0}},
{"id": "MOTION-M2-RELATIVE-BACKWARD-CN", "group": "motion", "utterance": "副驾靠背后仰十度", "expected": {"backrest_recline": 51}, "expected_motion": {"axis": "backrest_recline", "direction": "increase", "delta_position_pct": 15, "delta_angle_deg": 10.0}},
{
"group": "motion",
"expected": {"backrest_recline": 81},
@@ -235,10 +235,10 @@
{"id": "MOTION-M2-RESET", "utterance": "右前座靠背复位"}
]
},
{"id": "MOTION-M2-RELATIVE-OVERFLOW-BACK", "group": "motion", "utterance": "靠背向后移动70度", "expected": {"backrest_recline": 100}, "expected_motion": {"axis": "backrest_recline", "mode": "relative", "direction": "increase", "delta_position_pct": 100, "delta_angle_deg": 68.0, "requested_delta_angle_deg": 70.0, "range_clamped": true}, "expected_notice_contains": ["备用目标", "最后方"]},
{"id": "MOTION-M2-RELATIVE-OVERFLOW-FORWARD", "group": "motion", "utterance": "靠背向前移动70度", "expected": {"backrest_recline": 0}, "expected_motion": {"axis": "backrest_recline", "mode": "relative", "direction": "decrease", "delta_position_pct": 100, "delta_angle_deg": 68.0, "requested_delta_angle_deg": 70.0, "range_clamped": true}, "expected_notice_contains": ["备用目标", "最前方"]},
{"id": "MOTION-M2-TIMED-BACK-CLAMP", "group": "motion", "utterance": "靠背向后移动70秒", "expected": {"backrest_recline": 100}, "expected_motion": {"axis": "backrest_recline", "mode": "timed", "direction": "increase", "requested_duration_ms": 70000, "applied_duration_ms": 30000, "max_duration_ms": 30000, "duration_clamped": true, "endpoint_expected": "far"}, "expected_notice_contains": ["最大允许时长为30秒", "到达最后方"]},
{"id": "MOTION-M2-TIMED-FORWARD-MINUTE", "group": "motion", "utterance": "椅背向前移动1分钟", "expected": {"backrest_recline": 0}, "expected_motion": {"axis": "backrest_recline", "mode": "timed", "direction": "decrease", "requested_duration_ms": 60000, "applied_duration_ms": 30000, "max_duration_ms": 30000, "duration_clamped": true, "endpoint_expected": "near"}, "expected_notice_contains": ["最大允许时长为30秒", "到达最前方"]},
{"id": "MOTION-M2-RELATIVE-OVERFLOW-BACK", "group": "motion", "utterance": "副驾靠背向后移动70度", "expected": {"backrest_recline": 100}, "expected_motion": {"axis": "backrest_recline", "mode": "relative", "direction": "increase", "delta_position_pct": 100, "delta_angle_deg": 68.0, "requested_delta_angle_deg": 70.0, "range_clamped": true}, "expected_notice_contains": ["备用目标", "最后方"]},
{"id": "MOTION-M2-RELATIVE-OVERFLOW-FORWARD", "group": "motion", "utterance": "副驾靠背向前移动70度", "expected": {"backrest_recline": 0}, "expected_motion": {"axis": "backrest_recline", "mode": "relative", "direction": "decrease", "delta_position_pct": 100, "delta_angle_deg": 68.0, "requested_delta_angle_deg": 70.0, "range_clamped": true}, "expected_notice_contains": ["备用目标", "最前方"]},
{"id": "MOTION-M2-TIMED-BACK-CLAMP", "group": "motion", "utterance": "副驾靠背向后移动70秒", "expected": {"backrest_recline": 100}, "expected_motion": {"axis": "backrest_recline", "mode": "timed", "direction": "increase", "requested_duration_ms": 70000, "applied_duration_ms": 30000, "max_duration_ms": 30000, "duration_clamped": true, "endpoint_expected": "far"}, "expected_notice_contains": ["最大允许时长为30秒", "到达最后方"]},
{"id": "MOTION-M2-TIMED-FORWARD-MINUTE", "group": "motion", "utterance": "副驾椅背向前移动1分钟", "expected": {"backrest_recline": 0}, "expected_motion": {"axis": "backrest_recline", "mode": "timed", "direction": "decrease", "requested_duration_ms": 60000, "applied_duration_ms": 30000, "max_duration_ms": 30000, "duration_clamped": true, "endpoint_expected": "near"}, "expected_notice_contains": ["最大允许时长为30秒", "到达最前方"]},
{"id": "MOTION-M2-ABS-ANGLE-OVERFLOW", "group": "motion", "utterance": "副驾靠背调到200度", "expected": {"backrest_recline": 100}, "expected_notice_contains": ["备用目标", "163度", "最后方"]},
{"id": "MOTION-M2-POST-OVERFLOW-HOME", "group": "motion", "utterance": "副驾靠背归零", "expected": {"backrest_recline": 0}},
{
@@ -265,8 +265,8 @@
{"id": "MOTION-M3-ANGLE-145-FOOTREST", "utterance": "把副驾驶脚托设为145°"}
]
},
{"id": "MOTION-M3-RELATIVE-DOWN", "group": "motion", "utterance": "脚托往下调10度", "expected": {"legrest_height": 55}, "expected_motion": {"axis": "legrest_height", "direction": "decrease", "delta_position_pct": 14, "delta_angle_deg": 10.0}},
{"id": "MOTION-M3-RELATIVE-UP", "group": "motion", "utterance": "腿托向上抬十度", "expected": {"legrest_height": 69}, "expected_motion": {"axis": "legrest_height", "direction": "increase", "delta_position_pct": 14, "delta_angle_deg": 10.0}},
{"id": "MOTION-M3-RELATIVE-DOWN", "group": "motion", "utterance": "副驾脚托往下调10度", "expected": {"legrest_height": 55}, "expected_motion": {"axis": "legrest_height", "direction": "decrease", "delta_position_pct": 14, "delta_angle_deg": 10.0}},
{"id": "MOTION-M3-RELATIVE-UP", "group": "motion", "utterance": "副驾腿托向上抬十度", "expected": {"legrest_height": 69}, "expected_motion": {"axis": "legrest_height", "direction": "increase", "delta_position_pct": 14, "delta_angle_deg": 10.0}},
{
"group": "motion",
"expected": {"legrest_height": 90},
@@ -284,8 +284,8 @@
{"id": "MOTION-M3-RESET", "utterance": "右前座腿部支撑复位"}
]
},
{"id": "MOTION-M3-TIMED-UP-CLAMP", "group": "motion", "utterance": "腿托向上移动1分钟", "expected": {"legrest_height": 100}, "expected_motion": {"axis": "legrest_height", "mode": "timed", "direction": "increase", "requested_duration_ms": 60000, "applied_duration_ms": 25000, "max_duration_ms": 25000, "duration_clamped": true, "endpoint_expected": "far"}, "expected_notice_contains": ["最大允许时长为25秒", "到达最上方"]},
{"id": "MOTION-M3-TIMED-DOWN-CLAMP", "group": "motion", "utterance": "脚托向下移动60秒", "expected": {"legrest_height": 0}, "expected_motion": {"axis": "legrest_height", "mode": "timed", "direction": "decrease", "requested_duration_ms": 60000, "applied_duration_ms": 25000, "max_duration_ms": 25000, "duration_clamped": true, "endpoint_expected": "near"}, "expected_notice_contains": ["最大允许时长为25秒", "到达最下方"]},
{"id": "MOTION-M3-TIMED-UP-CLAMP", "group": "motion", "utterance": "副驾腿托向上移动1分钟", "expected": {"legrest_height": 100}, "expected_motion": {"axis": "legrest_height", "mode": "timed", "direction": "increase", "requested_duration_ms": 60000, "applied_duration_ms": 25000, "max_duration_ms": 25000, "duration_clamped": true, "endpoint_expected": "far"}, "expected_notice_contains": ["最大允许时长为25秒", "到达最上方"]},
{"id": "MOTION-M3-TIMED-DOWN-CLAMP", "group": "motion", "utterance": "副驾脚托向下移动60秒", "expected": {"legrest_height": 0}, "expected_motion": {"axis": "legrest_height", "mode": "timed", "direction": "decrease", "requested_duration_ms": 60000, "applied_duration_ms": 25000, "max_duration_ms": 25000, "duration_clamped": true, "endpoint_expected": "near"}, "expected_notice_contains": ["最大允许时长为25秒", "到达最下方"]},
{"id": "MOTION-ALL-40", "group": "motion", "utterance": "副驾驶水平、坐垫、椅背、脚托都调到40%", "expected": {"longitudinal": 40, "cushion_tilt": 40, "backrest_recline": 40, "legrest_height": 40}},
{"id": "MOTION-ALL-40-FULLWIDTH", "group": "motion", "utterance": "右前座前后、坐盆、靠背、腿托全部设置为40%", "expected": {"longitudinal": 40, "cushion_tilt": 40, "backrest_recline": 40, "legrest_height": 40}},
{"id": "MOTION-ALL-ZERO", "group": "motion", "utterance": "把副驾水平、坐盆、靠背、腿托四轴归零", "expected": {"longitudinal": 0, "cushion_tilt": 0, "backrest_recline": 0, "legrest_height": 0}},
@@ -303,19 +303,19 @@
{"id": "THERM-V-SYNONYM-AIR", "group": "thermal", "utterance": "副驾座椅吹风开到2档", "expected": {"ventilation": 2}},
{"id": "MOTION-M0-TIMED-WITHIN-RANGE", "group": "motion", "utterance": "副驾滑轨向后移动2秒", "expected": {}, "expected_motion": {"axis": "longitudinal", "mode": "timed", "direction": "increase", "requested_duration_ms": 2000, "applied_duration_ms": 2000, "max_duration_ms": 30000, "min_duration_ms": 300, "duration_clamped": false}},
{"id": "MOTION-M1-TIMED-ENGLISH-UNIT", "group": "motion", "utterance": "坐垫向下移动2s", "expected": {}, "expected_motion": {"axis": "cushion_tilt", "mode": "timed", "direction": "increase", "requested_duration_ms": 2000, "applied_duration_ms": 2000, "max_duration_ms": 20000, "min_duration_ms": 300, "duration_clamped": false}},
{"id": "MOTION-M2-TIMED-HALF-SECOND", "group": "motion", "utterance": "靠背向后移动半秒", "expected": {}, "expected_motion": {"axis": "backrest_recline", "mode": "timed", "direction": "increase", "requested_duration_ms": 500, "applied_duration_ms": 500, "max_duration_ms": 30000, "min_duration_ms": 300, "duration_clamped": false}},
{"id": "MOTION-M3-TIMED-MIN-CLAMP", "group": "motion", "utterance": "腿托向上移动200毫秒", "expected": {}, "expected_motion": {"axis": "legrest_height", "mode": "timed", "direction": "increase", "requested_duration_ms": 200, "applied_duration_ms": 300, "max_duration_ms": 25000, "min_duration_ms": 300, "duration_clamped": true}, "expected_notice_contains": ["最小允许时长为0.3秒", "已按0.3秒"]},
{"id": "MOTION-M1-TIMED-ENGLISH-UNIT", "group": "motion", "utterance": "副驾坐垫向下移动2s", "expected": {}, "expected_motion": {"axis": "cushion_tilt", "mode": "timed", "direction": "increase", "requested_duration_ms": 2000, "applied_duration_ms": 2000, "max_duration_ms": 20000, "min_duration_ms": 300, "duration_clamped": false}},
{"id": "MOTION-M2-TIMED-HALF-SECOND", "group": "motion", "utterance": "副驾靠背向后移动半秒", "expected": {}, "expected_motion": {"axis": "backrest_recline", "mode": "timed", "direction": "increase", "requested_duration_ms": 500, "applied_duration_ms": 500, "max_duration_ms": 30000, "min_duration_ms": 300, "duration_clamped": false}},
{"id": "MOTION-M3-TIMED-MIN-CLAMP", "group": "motion", "utterance": "副驾腿托向上移动200毫秒", "expected": {}, "expected_motion": {"axis": "legrest_height", "mode": "timed", "direction": "increase", "requested_duration_ms": 200, "applied_duration_ms": 300, "max_duration_ms": 25000, "min_duration_ms": 300, "duration_clamped": true}, "expected_notice_contains": ["最小允许时长为0.3秒", "已按0.3秒"]},
{"id": "MOTION-M1-ENDPOINT-UPPER", "group": "motion", "utterance": "坐盆抬到最上方", "expected": {"cushion_tilt": 0}},
{"id": "MOTION-M1-ENDPOINT-LOWER", "group": "motion", "utterance": "坐盆降到最下方", "expected": {"cushion_tilt": 100}},
{"id": "MOTION-M2-ENDPOINT-RECLINED", "group": "motion", "utterance": "靠背放倒到最后", "expected": {"backrest_recline": 100}},
{"id": "MOTION-M3-ENDPOINT-UPPER", "group": "motion", "utterance": "腿托升到最高", "expected": {"legrest_height": 100}},
{"id": "MOTION-M3-ENDPOINT-LOWER", "group": "motion", "utterance": "腿托降到最低", "expected": {"legrest_height": 0}},
{"id": "MOTION-M1-ENDPOINT-UPPER", "group": "motion", "utterance": "副驾坐盆抬到最上方", "expected": {"cushion_tilt": 0}},
{"id": "MOTION-M1-ENDPOINT-LOWER", "group": "motion", "utterance": "副驾坐盆降到最下方", "expected": {"cushion_tilt": 100}},
{"id": "MOTION-M2-ENDPOINT-RECLINED", "group": "motion", "utterance": "副驾靠背放倒到最后", "expected": {"backrest_recline": 100}},
{"id": "MOTION-M3-ENDPOINT-UPPER", "group": "motion", "utterance": "副驾腿托升到最高", "expected": {"legrest_height": 100}},
{"id": "MOTION-M3-ENDPOINT-LOWER", "group": "motion", "utterance": "副驾腿托降到最低", "expected": {"legrest_height": 0}},
{"id": "MOTION-M1-ABS-OVERFLOW", "group": "motion", "utterance": "坐垫位置调到150%", "expected": {"cushion_tilt": 100}, "expected_notice_contains": ["备用目标", "机械末端"]},
{"id": "MOTION-M1-ABS-OVERFLOW", "group": "motion", "utterance": "副驾坐垫位置调到150%", "expected": {"cushion_tilt": 100}, "expected_notice_contains": ["备用目标", "机械末端"]},
{"id": "MOTION-M2-ABS-ANGLE-UNDERFLOW", "group": "motion", "utterance": "副驾靠背调到80度", "expected": {"backrest_recline": 0}, "expected_notice_contains": ["备用目标", "95度", "最前方"]},
{"id": "MOTION-M3-ABS-ANGLE-OVERFLOW", "group": "motion", "utterance": "腿托调到200度", "expected": {"legrest_height": 100}, "expected_notice_contains": ["备用目标", "167度", "最上方"]},
{"id": "MOTION-M3-ABS-ANGLE-OVERFLOW", "group": "motion", "utterance": "副驾腿托调到200度", "expected": {"legrest_height": 100}, "expected_notice_contains": ["备用目标", "167度", "最上方"]},
{"id": "MOTION-M0-ABS-DECIMAL-ROUND", "group": "motion", "utterance": "副驾座椅水平位置调到50.5%", "expected": {"longitudinal": 51}, "expected_notice_contains": ["取整为51%"]},
{"id": "MOTION-M0-ABS-NEGATIVE-CLAMP", "group": "motion", "utterance": "副驾座椅水平位置调到-10%", "expected": {"longitudinal": 0}, "expected_notice_contains": ["备用目标", "0%", "机械末端"]},
{"id": "MOTION-M2-TIMED-ZERO-MIN-CLAMP", "group": "motion", "utterance": "副驾靠背向后移动0秒", "expected": {}, "expected_motion": {"axis": "backrest_recline", "mode": "timed", "direction": "increase", "requested_duration_ms": 0, "applied_duration_ms": 300, "max_duration_ms": 30000, "min_duration_ms": 300, "duration_clamped": true}, "expected_notice_contains": ["最小允许时长为0.3秒", "已按0.3秒"]}
@@ -409,7 +409,8 @@
}
],
"rejection_cases": [
{"id": "SAFE-DRIVER-THERMAL", "utterance": "打开主驾座椅通风", "expected_status": "needs_confirmation"},
{"id": "SAFE-DRIVER-THERMAL-LEVEL", "utterance": "主驾座椅通风4档", "expected_status": "needs_confirmation"},
{"id": "SAFE-DRIVER-MASSAGE-GENERIC", "utterance": "打开主驾座椅按摩", "expected_status": "needs_confirmation"},
{"id": "SAFE-DRIVER-MOTION", "utterance": "把驾驶位靠背调到50%", "expected_status": "needs_confirmation"},
{"id": "SAFE-DRIVER-MODE", "utterance": "主驾进入零重力模式", "expected_status": "needs_confirmation"},
{"id": "SAFE-FRONT-ROW", "utterance": "打开前排座椅加热", "expected_status": "needs_confirmation"},
@@ -422,7 +423,6 @@
{"id": "SAFE-RELATIVE-WRONG-UNIT", "utterance": "坐垫向前移动10度", "expected_status": "needs_confirmation"},
{"id": "SAFE-RELATIVE-MISSING-AXIS", "utterance": "向前移动10度", "expected_status": "needs_confirmation"},
{"id": "SAFE-UNSUPPORTED-REAR-ROW", "utterance": "打开后排座椅加热", "expected_status": "needs_confirmation"},
{"id": "SAFE-UNSUPPORTED-FRONT-LEFT", "utterance": "打开左前座椅通风", "expected_status": "needs_confirmation"},
{"id": "SAFE-UNSUPPORTED-ALL-SEATS", "utterance": "打开所有座椅加热", "expected_status": "needs_confirmation"}
],
"final_state": {
File diff suppressed because it is too large Load Diff
+32 -177
View File
@@ -8,6 +8,7 @@ from unittest.mock import MagicMock, patch
from cognitive_agent.application.domain_runtime import ConfiguredDomainRuntime
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.infrastructure.can.hardware import HardwareSafetyError
class ConfiguredDomainRuntimeTests(unittest.TestCase):
@@ -311,7 +312,7 @@ class MultiDomainServiceTests(unittest.TestCase):
},
)
def test_hardware_orchestration_uses_physical_passenger_seat_and_virtual_roof(
def test_hardware_orchestration_rejects_ch0_seat_and_virtual_roof_before_send(
self,
) -> None:
model_output = {
@@ -338,112 +339,32 @@ class MultiDomainServiceTests(unittest.TestCase):
"decision_summary": ["执行副驾与天幕联合模式。"],
"confidence": 0.96,
}
physical_state = {
"position": {
"longitudinal": {"position_pct": 51, "position_valid": True},
"cushion_tilt": {"position_pct": 11, "position_valid": True},
"backrest_recline": {"position_pct": 54, "position_valid": True},
"legrest_height": {"position_pct": 0, "position_valid": True},
},
"ventilation": {"state": "off", "level": 0},
"heating": {"state": "off", "level": 0},
"captured_at": "2026-07-31T04:00:00Z",
"complete": True,
"missing": [],
}
def execute_hardware(plan, config, devices):
del plan, devices
operation = config["spec"]["operations"][0]
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "ExecutionReport",
"metadata": {
"id": "report-hardware-test",
"name": "T2G seat hardware execution report",
"version": "1.0.0",
"request_id": config["metadata"]["request_id"],
"labels": {"simulation": "false"},
},
"spec": {
"mode": "hil",
"status": "succeeded",
"started_at": "2026-07-31T04:00:00Z",
"finished_at": "2026-07-31T04:00:01Z",
"physical_state": physical_state,
"results": [
{
"ref": operation["id"],
"domain": "seat",
"status": "succeeded",
"message": "physical target reached",
"telemetry": {
"motion_axis": "backrest_recline",
"effective_position_pct": 54,
"position_valid": True,
"simulated": False,
},
}
],
},
}
hardware_executor = MagicMock()
hardware_executor.execute.side_effect = execute_hardware
initial_driver_backrest = self.service.conversations.snapshot(
"conv-mixed-hardware-success"
)["devices"]["driver"]["position"]["backrest_recline"]["position_pct"]
with (
patch.object(self.service.openai, "interpret", return_value=model_output),
patch.object(type(self.service.hardware_settings), "authorize"),
patch.object(
type(self.service.hardware_settings),
"validate_function_config",
),
patch.object(
self.service,
"_new_hardware_executor",
return_value=hardware_executor,
) as executor_factory,
):
result = self.service.process(
"副驾靠背调到55%,天幕透光率调到20%",
"openai",
"conv-mixed-hardware-success",
execution_mode="hardware",
approval_token="test-token",
vehicle_id="denza_n7",
)
with self.assertRaisesRegex(
HardwareSafetyError,
"hardware mode refuses virtual or unverified vehicle actions",
):
self.service.process(
"副驾靠背调到55%,天幕透光率调到20%",
"openai",
"conv-mixed-hardware-success",
execution_mode="hardware",
approval_token="test-token",
vehicle_id="denza_n7",
)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(result["execution_report"]["spec"]["mode"], "hil")
self.assertEqual(
result["execution_report"]["metadata"]["labels"]["simulation"],
"mixed",
)
executor_factory.assert_called_once_with()
hardware_executor.execute.assert_called_once()
by_domain = {
item["domain"]: item
for item in result["execution_report"]["spec"]["results"]
}
self.assertFalse(by_domain["seat"]["telemetry"]["simulated"])
self.assertTrue(by_domain["roof"]["telemetry"]["simulated"])
self.assertEqual(
result["actuator_state"]["passenger"]["position"][
"backrest_recline"
]["position_pct"],
54,
)
self.assertEqual(
result["actuator_state"]["driver"]["position"]["backrest_recline"][
"position_pct"
],
initial_driver_backrest,
)
self.assertEqual(result["domain_state"]["roof"]["transmittance_pct"], 20)
executor_factory.assert_not_called()
hardware_executor.execute.assert_not_called()
def test_hardware_orchestration_failure_still_synchronizes_physical_seat(
def test_hardware_orchestration_rejects_ch0_motion_before_executor_creation(
self,
) -> None:
model_output = {
@@ -471,95 +392,29 @@ class MultiDomainServiceTests(unittest.TestCase):
"confidence": 0.96,
}
hardware_executor = MagicMock()
def execute_hardware(plan, config, devices):
del plan, devices
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "ExecutionReport",
"metadata": {
"id": "report-hardware-failure-test",
"name": "T2G seat hardware execution report",
"version": "1.0.0",
"request_id": config["metadata"]["request_id"],
"labels": {"simulation": "false"},
},
"spec": {
"mode": "hil",
"status": "failed",
"started_at": "2026-07-31T04:00:00Z",
"finished_at": "2026-07-31T04:00:01Z",
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 63,
"position_valid": True,
}
},
"captured_at": "2026-07-31T04:00:01Z",
"complete": False,
"missing": ["cushion_tilt", "backrest_recline", "legrest_height"],
},
"results": [
{
"ref": config["spec"]["operations"][0]["id"],
"domain": "seat",
"status": "failed",
"message": "Motor0 TIMEOUT",
"telemetry": {"simulated": False},
}
],
},
}
hardware_executor.execute.side_effect = execute_hardware
initial_roof = self.service.conversations.snapshot(
"conv-mixed-hardware-failure"
)["domain_devices"]["roof"]["transmittance_pct"]
with (
patch.object(self.service.openai, "interpret", return_value=model_output),
patch.object(type(self.service.hardware_settings), "authorize"),
patch.object(
type(self.service.hardware_settings),
"validate_function_config",
),
patch.object(
self.service,
"_new_hardware_executor",
return_value=hardware_executor,
),
) as executor_factory,
):
result = self.service.process(
"副驾水平移到100%,天幕透光率调到20%",
"openai",
"conv-mixed-hardware-failure",
execution_mode="hardware",
approval_token="test-token",
vehicle_id="denza_n7",
)
with self.assertRaisesRegex(
HardwareSafetyError,
"hardware mode refuses virtual or unverified vehicle actions",
):
self.service.process(
"副驾水平移到100%,天幕透光率调到20%",
"openai",
"conv-mixed-hardware-failure",
execution_mode="hardware",
approval_token="test-token",
vehicle_id="denza_n7",
)
self.assertEqual(result["status"], "partially_succeeded")
self.assertEqual(result["execution_report"]["spec"]["status"], "failed")
self.assertEqual(
result["actuator_state"]["passenger"]["position"]["longitudinal"][
"position_pct"
],
63,
)
self.assertEqual(
result["actuator_state"]["passenger"]["position"]["longitudinal"][
"feedback_source"
],
"physical_can",
)
self.assertEqual(
result["domain_state"]["roof"]["transmittance_pct"],
20,
)
self.assertNotEqual(
result["domain_state"]["roof"]["transmittance_pct"],
initial_roof,
)
executor_factory.assert_not_called()
hardware_executor.execute.assert_not_called()
def test_reference_mode_library_modes_execute_for_denza(self) -> None:
model_output = {
@@ -8,6 +8,11 @@ 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"
@@ -185,6 +190,35 @@ class SeatFrontendInputCoverageTests(unittest.TestCase):
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()
+424
View File
@@ -0,0 +1,424 @@
from __future__ import annotations
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import MagicMock, patch
from cognitive_agent.application.model_plan_adapter import ModelPlanAdapter
from cognitive_agent.application.native_can_intent import merge_native_can_intent
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.application.self_extension import (
SelfExtensionService,
SimulationGateway,
)
HARDWARE_DISABLED_ENV = {
"COGNITIVE_AGENT_CH1_HIL_ENABLED": "false",
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
}
def _empty_intent() -> dict:
return {
"actions": [],
"domain_actions": [],
"native_can_actions": [],
"deferred_requests": [],
"model_requests": [],
"preset_actions": [],
"preset_events": [],
"ambiguities": [],
"detected_domains": [],
"decision_summary": [],
"confidence": 0.0,
}
class Ch1DefaultTargetTests(unittest.TestCase):
def setUp(self) -> None:
self.environment = patch.dict(os.environ, HARDWARE_DISABLED_ENV, clear=False)
self.environment.start()
self.addCleanup(self.environment.stop)
self.temporary_directory = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary_directory.cleanup)
self.service = SeatDemoService(REPOSITORY_ROOT)
self.service.artifact_root = Path(self.temporary_directory.name)
def test_local_requests_use_driver_console_defaults_and_one_ch1_operation(self) -> None:
cases = (
(
"车窗开启一半",
"native_window",
"window.set_position",
"front_left",
{"position_pct": 50},
),
(
"打开车窗",
"native_window",
"window.set_motion",
"front_left",
{"motion": "open"},
),
(
"空调调到21度",
"native_climate",
"climate.set_temperature",
"driver",
{"temperature_c": 21},
),
(
"把风口左右30%上下40%",
"native_outlet",
"outlet.set_position_percent",
"driver_left",
{"horizontal_pct": 30, "vertical_pct": 40},
),
(
"打开座椅加热",
"native_seat",
"seat.set_driver_heating_native",
"driver",
{"state": "on", "level": 2},
),
(
"打开座椅通风",
"native_seat",
"seat.set_driver_ventilation_native",
"driver",
{"state": "on", "level": 2},
),
)
for index, (utterance, config_key, capability, target, parameters) in enumerate(
cases
):
with self.subTest(utterance=utterance):
result = self.service.process(
utterance,
provider="local",
conversation_id=f"conv-ch1-default-local-{index}",
execution_mode="dry_run",
)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(set(result["function_configs"]), {config_key})
self.assertNotIn("seat", result["function_configs"])
operations = result["function_configs"][config_key]["spec"][
"operations"
]
self.assertEqual(len(operations), 1)
self.assertEqual(operations[0]["capability"], capability)
self.assertEqual(operations[0]["target"]["zone"], target)
self.assertEqual(operations[0]["parameters"], parameters)
commands = result["command_plans"][config_key]["spec"]["commands"]
self.assertEqual(len(commands), 1)
self.assertEqual(commands[0]["bus"], "body_can")
summaries = " ".join(result["decision_summary"])
self.assertNotIn("副驾座椅功能单域", summaries)
self.assertNotIn("目标:副驾座椅", summaries)
def test_explicit_vehicle_targets_take_priority_over_defaults(self) -> None:
cases = (
("右前车窗开启一半", "native_window", "front_right", {"position_pct": 50}),
("副驾空调调到21度", "native_climate", "passenger", {"temperature_c": 21}),
)
for index, (utterance, config_key, target, parameters) in enumerate(cases):
with self.subTest(utterance=utterance):
result = self.service.process(
utterance,
provider="local",
conversation_id=f"conv-ch1-explicit-target-{index}",
)
operation = result["function_configs"][config_key]["spec"][
"operations"
][0]
self.assertEqual(operation["target"]["zone"], target)
self.assertEqual(operation["parameters"], parameters)
passenger = self.service.process(
"打开副驾驶座椅加热",
provider="local",
conversation_id="conv-ch1-explicit-passenger-seat",
)
self.assertEqual(set(passenger["function_configs"]), {"seat"})
self.assertNotIn("native_seat", passenger["function_configs"])
self.assertEqual(
passenger["function_configs"]["seat"]["spec"]["operations"][0][
"target"
]["zone"],
"front_right",
)
def test_group_or_non_driver_seat_scope_is_not_treated_as_unscoped(self) -> None:
for index, utterance in enumerate(
("打开前排座椅加热", "打开后排座椅加热", "打开所有座椅加热")
):
with self.subTest(utterance=utterance):
result = self.service.process(
utterance,
provider="local",
conversation_id=f"conv-ch1-non-driver-scope-{index}",
)
self.assertEqual(result["status"], "needs_confirmation")
self.assertEqual(result["function_configs"], {})
self.assertNotIn("native_seat", result["command_plans"])
def test_unresolved_unscoped_seat_request_never_falls_back_to_passenger_ch0(self) -> None:
parsed = _empty_intent()
parsed["actions"] = [
{
"action_type": "thermal",
"target": "passenger",
"function": "heating",
"state": "on",
}
]
parsed["model_requests"] = [
{
"domain": "seat",
"capability": "seat.set_heating",
"target_scope": "passenger",
"operation": "set",
"parameters": [{"name": "state", "value": "on"}],
"reason": "model guessed passenger",
}
]
result = merge_native_can_intent(parsed, "座椅调舒服一点")
self.assertFalse(result["actions"])
self.assertFalse(result["native_can_actions"])
self.assertFalse(result["model_requests"])
self.assertTrue(
any("不会转入副驾 CH0" in item["question"] for item in result["ambiguities"])
)
def test_unverified_driver_absolute_motion_is_not_degraded_to_a_timed_pulse(self) -> None:
for index, utterance in enumerate(
("座椅前后移动到50%", "靠背向后移动70度", "坐盆抬到最高")
):
with self.subTest(utterance=utterance):
result = self.service.process(
utterance,
provider="local",
conversation_id=f"conv-ch1-unsupported-absolute-{index}",
)
self.assertEqual(result["status"], "needs_confirmation")
self.assertEqual(result["function_configs"], {})
timed = self.service.process(
"座椅向前移动2秒",
provider="local",
conversation_id="conv-ch1-verified-timed-axis",
)
self.assertEqual(timed["status"], "succeeded")
operation = timed["function_configs"]["native_seat"]["spec"][
"operations"
][0]
self.assertEqual(operation["capability"], "seat.set_driver_axis_native")
self.assertEqual(
operation["parameters"],
{"axis": "horizontal", "direction": "forward", "duration_ms": 2000},
)
def test_model_adapter_overrides_only_implicit_targets_with_ch1_defaults(self) -> None:
window = ModelPlanAdapter._apply_ch1_default_target(
{
"domain": "window",
"capability": "window.set_position",
"target_scope": "all",
},
"车窗开启一半",
)
climate = ModelPlanAdapter._apply_ch1_default_target(
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "vehicle",
},
"空调调到21度",
)
seat = ModelPlanAdapter._apply_ch1_default_target(
{
"domain": "seat",
"capability": "seat.set_heating",
"target_scope": "passenger",
},
"打开座椅加热",
)
explicit_passenger = ModelPlanAdapter._apply_ch1_default_target(
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "passenger",
},
"副驾空调调到21度",
)
self.assertEqual(window["target_scope"], "front_left")
self.assertEqual(climate["target_scope"], "driver")
self.assertEqual(seat["capability"], "seat.set_driver_heating_native")
self.assertEqual(seat["target_scope"], "driver")
self.assertEqual(explicit_passenger["target_scope"], "passenger")
def test_gpt_provider_is_preserved_while_wrong_model_targets_are_replaced(self) -> None:
cases = (
(
"车窗开启一半",
{
"domain": "window",
"capability": "window.set_position",
"target_scope": "all",
"operation": "set",
"parameters": [{"name": "position_pct", "value": 100}],
"reason": "model chose every window",
},
"native_window",
"front_left",
{"position_pct": 50},
),
(
"空调调到21度",
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "vehicle",
"operation": "set",
"parameters": [{"name": "temperature_c", "value": 21}],
"reason": "model chose the whole vehicle",
},
"native_climate",
"driver",
{"temperature_c": 21},
),
(
"打开座椅加热",
{
"domain": "seat",
"capability": "seat.set_heating",
"target_scope": "passenger",
"operation": "set",
"parameters": [
{"name": "state", "value": "on"},
{"name": "level", "value": 3},
],
"reason": "model guessed the legacy passenger seat",
},
"native_seat",
"driver",
{"state": "on", "level": 2},
),
)
self.service.openai.api_key = "offline-test-key"
self.service.openai.model = "gpt-5.6-sol"
for index, (utterance, request, config_key, target, parameters) in enumerate(
cases
):
model_output = {
"detected_domains": [request["domain"]],
"requests": [request],
"ambiguities": [],
"decision_summary": ["GPT model output"],
"confidence": 0.9,
}
with self.subTest(utterance=utterance):
with patch.object(
self.service.openai, "interpret", return_value=model_output
) as interpret:
result = self.service.process(
utterance,
provider="openai",
conversation_id=f"conv-ch1-default-openai-{index}",
)
interpret.assert_called_once()
self.assertEqual(result["provider_used"], "openai:gpt-5.6-sol")
self.assertEqual(result["status"], "succeeded")
self.assertEqual(set(result["function_configs"]), {config_key})
self.assertNotIn("seat", result["function_configs"])
operation = result["function_configs"][config_key]["spec"][
"operations"
][0]
self.assertEqual(operation["target"]["zone"], target)
self.assertEqual(operation["parameters"], parameters)
self.assertEqual(
len(result["command_plans"][config_key]["spec"]["commands"]),
1,
)
def test_self_extension_vehicle_endpoint_uses_the_same_default_contract(self) -> None:
extension = SelfExtensionService(SimulationGateway())
extension.vehicle_provider = "local"
extension.vehicle_execution_mode = "dry_run"
extension.vehicle_id = "denza_n7"
cases = (
("车窗开启一半", "window", "front_left", {"position_pct": 50}),
("空调调到21度", "climate", "driver", {"temperature_c": 21}),
("打开座椅加热", "seat", "driver", {"state": "on", "level": 2}),
)
for index, (command, domain, target, parameters) in enumerate(cases):
with self.subTest(command=command):
result = extension.execute_vehicle(
{
"command": command,
"conversation_id": f"conv-extension-ch1-default-{index}",
},
self.service,
hardware_access_bypass=True,
)
self.assertTrue(result["success"])
self.assertEqual(result["status"], "succeeded")
self.assertEqual(result["provider_used"], "local_rules")
self.assertEqual(len(result["operation_results"]), 1)
operation = result["operation_results"][0]
self.assertEqual(operation["domain"], domain)
self.assertEqual(operation["telemetry"]["target"], target)
self.assertEqual(
operation["telemetry"]["requested_parameters"], parameters
)
def test_self_extension_gpt_prompt_limits_confirmation_to_toy_peripherals(self) -> None:
response_plan = {
"summary": "ok",
"needs_confirmation": False,
"question": "",
"steps": [],
}
response = MagicMock()
response.__enter__.return_value = response
response.read.return_value = json.dumps(
{"output_text": json.dumps(response_plan, ensure_ascii=False)},
ensure_ascii=False,
).encode("utf-8")
with patch.dict(os.environ, {"OPENAI_STREAM": "false"}, clear=False):
extension = SelfExtensionService(SimulationGateway())
with patch(
"cognitive_agent.application.self_extension.urlopen",
return_value=response,
) as open_request:
extension._request_gpt_plan(
"车窗开启一半,空调调到21度",
[],
[{"label": "车窗"}, {"label": "空调"}],
"offline-test-key",
)
request_payload = json.loads(open_request.call_args.args[0].data.decode("utf-8"))
prompt = request_payload["input"][0]["content"][0]["text"]
self.assertIn("多台可匹配的外设玩具", prompt)
self.assertIn("此追问规则不适用于", prompt)
self.assertIn("window front-left position 50", prompt)
self.assertIn("ac temp driver 21", prompt)
self.assertIn("seat driver", prompt)
self.assertIn("绝不能转入副驾/CH0", prompt)
if __name__ == "__main__":
unittest.main()
+96
View File
@@ -0,0 +1,96 @@
from __future__ import annotations
from contextlib import nullcontext
import os
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.infrastructure.can.hardware import (
HardwareSafetyError,
VehicleCanSettings,
)
class Ch1VehicleSafetyTests(unittest.TestCase):
def settings(self, channel: int) -> VehicleCanSettings:
environment = {
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "true",
"COGNITIVE_AGENT_VEHICLE_CAN_CHANNEL": str(channel),
"COGNITIVE_AGENT_VEHICLE_CAN_REQUIRED_CHANNEL": "1",
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED": "true",
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN": "approved",
}
with patch.dict(os.environ, environment, clear=False):
return VehicleCanSettings.from_env(REPOSITORY_ROOT)
def test_ch1_is_the_only_allowed_body_can_channel(self) -> None:
settings = self.settings(1)
self.assertTrue(settings.channel_is_allowed)
self.assertEqual(settings.required_channel, 1)
self.assertTrue(settings.configured)
settings.authorize("approved")
info = settings.public_info()
self.assertEqual(info["target_policy"], "ch1_body_can_driver_only")
self.assertTrue(info["channel_is_allowed"])
def test_ch0_is_rejected_before_adapter_open(self) -> None:
settings = self.settings(0)
self.assertFalse(settings.channel_is_allowed)
self.assertFalse(settings.configured)
with self.assertRaisesRegex(HardwareSafetyError, "CH1.*CH0 is forbidden"):
settings.authorize("approved")
def test_policy_cannot_be_redirected_to_ch0(self) -> None:
with patch.dict(
os.environ,
{
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "true",
"COGNITIVE_AGENT_VEHICLE_CAN_CHANNEL": "0",
"COGNITIVE_AGENT_VEHICLE_CAN_REQUIRED_CHANNEL": "0",
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED": "false",
},
clear=False,
):
settings = VehicleCanSettings.from_env(REPOSITORY_ROOT)
self.assertFalse(settings.channel_is_allowed)
with self.assertRaises(HardwareSafetyError):
settings.authorize(None)
def test_driver_only_ch1_uses_deployment_authority_without_browser_lease(self) -> None:
service = object.__new__(SeatDemoService)
service.hardware_settings = SimpleNamespace(enabled=False)
service.vehicle_can_settings = SimpleNamespace(
configured=True,
channel_is_allowed=True,
channel=1,
)
service.hardware_execution = Mock()
service.hardware_execution.reserve.return_value = nullcontext()
service.hardware_access = Mock()
service._process_once = Mock(return_value={"status": "succeeded"})
result = service.process(
"ac off",
execution_mode="hardware",
hardware_access_bypass=False,
)
self.assertEqual(result["status"], "succeeded")
self.assertTrue(service.driver_only_ch1_execution_active)
service.hardware_access.ensure_authorized.assert_not_called()
def test_browser_lease_remains_required_if_legacy_seat_path_is_enabled(self) -> None:
service = object.__new__(SeatDemoService)
service.hardware_settings = SimpleNamespace(enabled=True)
service.vehicle_can_settings = SimpleNamespace(
configured=True,
channel_is_allowed=True,
channel=1,
)
self.assertFalse(service.driver_only_ch1_execution_active)
if __name__ == "__main__":
unittest.main()
+529
View File
@@ -0,0 +1,529 @@
from __future__ import annotations
import json
import unittest
from pathlib import Path
from unittest.mock import patch
from cognitive_agent.application.capability_registry import CapabilityRegistry
from cognitive_agent.application.native_can_intent import merge_native_can_intent
from cognitive_agent.application.native_can_runtime import (
NativeCanCompileError,
NativeCanExecutor,
NativeCanRuntime,
)
from cognitive_agent.infrastructure.can.gateway import CanFrame
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
class _FakeBus:
def __init__(
self,
received: list[CanFrame] | None = None,
*,
fail_on_calls: set[int] | None = None,
) -> None:
self.received = list(received or [])
self.fail_on_calls = set(fail_on_calls or set())
self.send_calls = 0
self.sent: list[CanFrame] = []
self.closed = False
def send(
self,
arbitration_id: int,
data: bytes,
*,
is_fd: bool = False,
bitrate_switch: bool = False,
) -> CanFrame:
self.send_calls += 1
if self.send_calls in self.fail_on_calls:
raise RuntimeError(f"injected send fault {self.send_calls}")
frame = CanFrame(
arbitration_id,
bytes(data),
channel=1,
is_fd=is_fd,
bitrate_switch=bitrate_switch,
)
self.sent.append(frame)
return frame
def receive(
self, timeout_ms: int, *, include_fd: bool = False
) -> CanFrame | None:
return self.received.pop(0) if self.received else None
def close(self) -> None:
self.closed = True
def _native_action(
capability: str, parameters: dict, target: str = "driver"
) -> dict:
return {
"domain": "seat",
"capability": capability,
"target": target,
"operation": "set",
"parameters": parameters,
}
class DriverSeatNativeCanTests(unittest.TestCase):
def setUp(self) -> None:
self.runtime = NativeCanRuntime(REPOSITORY_ROOT)
def _compile(
self, capability: str, parameters: dict, target: str = "driver"
) -> tuple[dict, dict]:
config = self.runtime.build_function_config(
"seat",
[_native_action(capability, parameters, target)],
"req-driver-seat",
"local_rules",
)
return config, self.runtime.compile(config)
@staticmethod
def _parsed_action(utterance: str) -> dict | None:
parsed = merge_native_can_intent({}, utterance)
actions = parsed.get("native_can_actions", [])
return actions[0] if len(actions) == 1 else None
def test_catalog_exposes_only_driver_and_front_left_native_targets(self) -> None:
snapshot = CapabilityRegistry(REPOSITORY_ROOT, {"seat"}).snapshot()
native_seat = next(
item
for item in snapshot["domains"]
if item["domain"] == "seat" and item["availability"] == "native_can"
)
capabilities = {item["name"]: item for item in native_seat["capabilities"]}
self.assertEqual(
set(capabilities),
{
"seat.set_driver_ventilation_native",
"seat.set_driver_heating_native",
"seat.set_driver_massage_native",
"seat.set_driver_axis_native",
},
)
for capability in capabilities.values():
self.assertEqual(set(capability["target_scopes"]), {"driver", "front_left"})
def test_exact_commands_preserve_verified_driver_seat_parameters(self) -> None:
cases = {
"seat driver vent low": (
"seat.set_driver_ventilation_native",
{"state": "on", "level": 2},
"driver",
),
"seat front-left heat high": (
"seat.set_driver_heating_native",
{"state": "on", "level": 1},
"front_left",
),
"seat driver massage off": (
"seat.set_driver_massage_native",
{"action": "off"},
"driver",
),
"seat driver massage strength strong": (
"seat.set_driver_massage_native",
{"action": "strength", "strength": 3},
"driver",
),
"seat driver massage mode relax": (
"seat.set_driver_massage_native",
{"action": "mode", "mode": "relax"},
"driver",
),
"seat driver backrest backward 800": (
"seat.set_driver_axis_native",
{"axis": "backrest", "direction": "backward", "duration_ms": 800},
"driver",
),
"seat driver height stop": (
"seat.set_driver_axis_native",
{"axis": "height", "direction": "stop", "duration_ms": 500},
"driver",
),
}
for utterance, expected in cases.items():
with self.subTest(utterance=utterance):
action = self._parsed_action(utterance)
self.assertIsNotNone(action)
assert action is not None
self.assertEqual(
(action["capability"], action["parameters"], action["target"]),
expected,
)
def test_chinese_commands_require_explicit_driver_scope(self) -> None:
cases = {
"打开主驾座椅通风": (
"seat.set_driver_ventilation_native",
{"state": "on", "level": 2},
),
"主驾座椅加热高档": (
"seat.set_driver_heating_native",
{"state": "on", "level": 1},
),
"把驾驶位按摩调成波浪模式": (
"seat.set_driver_massage_native",
{"action": "mode", "mode": "wave"},
),
"主驾座椅按摩强度调高": (
"seat.set_driver_massage_native",
{"action": "strength", "strength": 3},
),
"主驾座椅向前移动500毫秒": (
"seat.set_driver_axis_native",
{"axis": "horizontal", "direction": "forward", "duration_ms": 500},
),
"主驾座椅靠背后仰800毫秒": (
"seat.set_driver_axis_native",
{"axis": "backrest", "direction": "backward", "duration_ms": 800},
),
"把主驾座椅坐盆降低": (
"seat.set_driver_axis_native",
{"axis": "basin", "direction": "down", "duration_ms": 500},
),
"主驾座椅升高": (
"seat.set_driver_axis_native",
{"axis": "height", "direction": "up", "duration_ms": 500},
),
}
for utterance, expected in cases.items():
with self.subTest(utterance=utterance):
action = self._parsed_action(utterance)
self.assertIsNotNone(action)
assert action is not None
self.assertEqual((action["capability"], action["parameters"]), expected)
self.assertEqual(action["target"], "driver")
def test_parser_rejects_passenger_right_front_and_unsafe_guesses(self) -> None:
utterances = (
"seat passenger vent low",
"seat front-right heat high",
"打开副驾座椅通风",
"打开右前座椅加热",
"打开主驾和副驾座椅通风",
"seat driver vent low, seat passenger vent low",
"打开主驾座椅按摩",
"主驾座椅向前移动50毫秒",
"seat driver height forward 500",
)
for utterance in utterances:
with self.subTest(utterance=utterance):
parsed = merge_native_can_intent({}, utterance)
self.assertFalse(parsed.get("native_can_actions"))
def test_deterministic_driver_parse_supersedes_deferred_model_copy(self) -> None:
parsed = {
"actions": [],
"domain_actions": [],
"native_can_actions": [],
"deferred_requests": [
{
"domain": "seat",
"capability": "seat.set_driver_ventilation_native",
"target_scope": "driver",
"parameters": {"state": "on", "level": 2},
"execution_backend": "native_can",
}
],
"model_requests": [
{
"domain": "seat",
"capability": "seat.set_driver_ventilation_native",
"target_scope": "driver",
"operation": "set",
"parameters": [
{"name": "state", "value": "on"},
{"name": "level", "value": 2},
],
"reason": "model",
}
],
}
merged = merge_native_can_intent(parsed, "打开主驾座椅通风")
self.assertEqual(len(merged["native_can_actions"]), 1)
self.assertEqual(
merged["native_can_actions"][0]["capability"],
"seat.set_driver_ventilation_native",
)
def test_thermal_frames_match_cpp_whitelist_and_feedback_contract(self) -> None:
cases = (
(
"seat.set_driver_ventilation_native",
{"state": "on", "level": 1},
"00 01 00 00 00 00 00 00",
"driver_seat_ventilation:3",
),
(
"seat.set_driver_ventilation_native",
{"state": "off"},
"00 03 00 00 00 00 00 00",
"driver_seat_ventilation:1",
),
(
"seat.set_driver_heating_native",
{"state": "on", "level": 2},
"00 20 00 00 00 00 00 00",
"driver_seat_heating:2",
),
(
"seat.set_driver_heating_native",
{"state": "off"},
"00 30 00 00 00 00 00 00",
"driver_seat_heating:1",
),
)
for capability, parameters, payload, expected in cases:
with self.subTest(capability=capability, parameters=parameters):
_, plan = self._compile(capability, parameters)
command = plan["spec"]["commands"][0]
self.assertEqual(command["signals"]["frame_id"], "0x431")
self.assertEqual(command["signals"]["data_hex"], payload)
self.assertTrue(command["signals"]["is_fd"])
self.assertTrue(command["signals"]["bitrate_switch"])
self.assertEqual(command["repeat"], {"count": 3, "period_ms": 100})
self.assertEqual(command["signals"]["feedback_ids"], "0x4FA")
self.assertEqual(command["signals"]["expected_feedback"], expected)
def test_massage_frames_match_cpp_whitelist(self) -> None:
cases = (
({"action": "off"}, "12 00 00 01 00 00 00 00", "driver_seat_massage_mode:1"),
(
{"action": "strength", "strength": 3},
"12 03 00 00 00 00 00 00",
"driver_seat_massage_strength:11",
),
(
{"action": "mode", "mode": "relax"},
"12 00 00 06 00 00 00 00",
"driver_seat_massage_mode:6",
),
)
for parameters, payload, expected in cases:
with self.subTest(parameters=parameters):
_, plan = self._compile("seat.set_driver_massage_native", parameters)
command = plan["spec"]["commands"][0]
self.assertEqual(command["signals"]["frame_id"], "0x4C1")
self.assertEqual(command["signals"]["data_hex"], payload)
self.assertEqual(command["repeat"], {"count": 3, "period_ms": 100})
self.assertEqual(command["signals"]["feedback_ids"], "0x4FA")
self.assertEqual(command["signals"]["expected_feedback"], expected)
def test_all_four_axes_compile_bounded_motion_and_forced_stop(self) -> None:
cases = (
("horizontal", "forward", "05 01 00 00 00 00 00 00"),
("height", "down", "05 08 00 00 00 00 00 00"),
("backrest", "forward", "05 10 00 00 00 00 00 00"),
("basin", "down", "05 80 00 00 00 00 00 00"),
)
for axis, direction, payload in cases:
with self.subTest(axis=axis):
_, plan = self._compile(
"seat.set_driver_axis_native",
{"axis": axis, "direction": direction, "duration_ms": 500},
)
command = plan["spec"]["commands"][0]
self.assertEqual(command["signals"]["frame_id"], "0x4C1")
self.assertEqual(command["signals"]["data_hex"], payload)
self.assertEqual(command["repeat"], {"count": 5, "period_ms": 100})
self.assertEqual(command["signals"]["feedback_ids"], "0x494")
self.assertEqual(command["signals"]["special_handler"], "driver_seat_axis")
safety = json.loads(command["signals"]["special_parameters"])
self.assertEqual(safety["stop_data_hex"], "05 00 00 00 00 00 00 00")
self.assertEqual(safety["stop_repeat_count"], 3)
self.assertEqual(safety["stop_period_ms"], 100)
public = self.runtime.command_frame_public(command)
self.assertTrue(public["safety"]["forced_stop"])
def test_explicit_axis_stop_is_three_fd_stop_frames(self) -> None:
_, plan = self._compile(
"seat.set_driver_axis_native",
{"axis": "height", "direction": "stop"},
)
command = plan["spec"]["commands"][0]
self.assertEqual(command["signals"]["data_hex"], "05 00 00 00 00 00 00 00")
self.assertEqual(command["repeat"], {"count": 3, "period_ms": 100})
self.assertNotIn("special_handler", command["signals"])
def test_compiler_fails_closed_for_wrong_seat_or_invalid_parameters(self) -> None:
for target in ("passenger", "front_right"):
action = _native_action(
"seat.set_driver_ventilation_native",
{"state": "on", "level": 2},
target,
)
with self.subTest(target=target):
self.assertFalse(self.runtime.supports(action))
with self.assertRaises(NativeCanCompileError):
self.runtime.build_function_config(
"seat", [action], "req-reject", "local_rules"
)
self.assertNotIn("seat.set_ventilation", self.runtime.supported_capabilities)
invalid = (
(
"seat.set_driver_ventilation_native",
{"state": "on", "level": 3},
),
(
"seat.set_driver_massage_native",
{"action": "mode", "mode": "unknown"},
),
(
"seat.set_driver_axis_native",
{"axis": "height", "direction": "forward", "duration_ms": 500},
),
(
"seat.set_driver_axis_native",
{"axis": "horizontal", "direction": "forward", "duration_ms": 2001},
),
)
for capability, parameters in invalid:
with self.subTest(capability=capability, parameters=parameters):
config = self.runtime.build_function_config(
"seat",
[_native_action(capability, parameters)],
"req-invalid",
"local_rules",
)
with self.assertRaises(NativeCanCompileError):
self.runtime.compile(config)
def test_thermal_feedback_is_semantically_matched_and_decoded(self) -> None:
config, plan = self._compile(
"seat.set_driver_ventilation_native",
{"state": "on", "level": 1},
)
feedback = CanFrame(
0x4FA,
bytes((0x0B, 0, 0, 0, 0, 0x81, 0, 0)),
channel=1,
is_fd=True,
bitrate_switch=True,
)
bus = _FakeBus([feedback])
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
report = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1).execute(
self.runtime, plan, config
)
result = report["spec"]["results"][0]
self.assertEqual(report["spec"]["status"], "succeeded")
self.assertTrue(result["telemetry"]["feedback_match"])
self.assertEqual(result["telemetry"]["feedback_state"]["ventilation"], "high")
self.assertEqual(result["telemetry"]["feedback_state"]["heating"], "off")
self.assertEqual(result["telemetry"]["feedback_state"]["massage_mode"], "off")
self.assertTrue(bus.closed)
wrong_bus = _FakeBus(
[CanFrame(0x4FA, bytes((0x0A, 0, 0, 0, 0, 0x81, 0, 0)), channel=1)]
)
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
wrong_report = NativeCanExecutor(
lambda: wrong_bus, feedback_timeout_ms=1
).execute(self.runtime, plan, config)
self.assertEqual(wrong_report["spec"]["status"], "failed")
self.assertFalse(
wrong_report["spec"]["results"][0]["telemetry"]["feedback_match"]
)
def test_massage_feedback_matches_mode_and_strength_fields(self) -> None:
cases = (
(
{"action": "mode", "mode": "relax"},
bytes((1, 0, 0, 0, 0, 0x86, 0, 0)),
"relax",
"off",
),
(
{"action": "strength", "strength": 3},
bytes((1, 0, 0, 0, 0, 0xB1, 0, 0)),
"off",
"strong",
),
)
for parameters, payload, mode, strength in cases:
with self.subTest(parameters=parameters):
config, plan = self._compile(
"seat.set_driver_massage_native", parameters
)
bus = _FakeBus([CanFrame(0x4FA, payload, channel=1)])
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
report = NativeCanExecutor(
lambda: bus, feedback_timeout_ms=1
).execute(self.runtime, plan, config)
telemetry = report["spec"]["results"][0]["telemetry"]
self.assertEqual(report["spec"]["status"], "succeeded")
self.assertTrue(telemetry["feedback_match"])
self.assertEqual(telemetry["feedback_state"]["massage_mode"], mode)
self.assertEqual(
telemetry["feedback_state"]["massage_strength"], strength
)
def test_axis_executor_sends_stop_and_reports_0x494_limitation(self) -> None:
config, plan = self._compile(
"seat.set_driver_axis_native",
{"axis": "horizontal", "direction": "forward", "duration_ms": 500},
)
feedback = CanFrame(
0x494,
bytes((0, 0, 0, 0, 0, 0, 0, 0x3C)),
channel=1,
is_fd=True,
bitrate_switch=True,
)
bus = _FakeBus([feedback])
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
report = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1).execute(
self.runtime, plan, config
)
# 0x494 proves only initialization and cannot confirm that the timed
# motion physically occurred, so strict hardware feedback fails closed.
self.assertEqual(report["spec"]["status"], "failed")
self.assertEqual(len(bus.sent), 8)
self.assertTrue(all(frame.is_fd and frame.bitrate_switch for frame in bus.sent))
self.assertEqual(
[frame.data for frame in bus.sent[-3:]],
[bytes((0x05, 0, 0, 0, 0, 0, 0, 0))] * 3,
)
telemetry = report["spec"]["results"][0]["telemetry"]
self.assertIsNone(telemetry["feedback_match"])
self.assertTrue(telemetry["feedback_required"])
self.assertTrue(telemetry["feedback_observed"])
self.assertEqual(
telemetry["feedback_state"]["execution_confirmation"],
"status_frame_only",
)
self.assertTrue(all(telemetry["feedback_state"]["initialized"].values()))
def test_axis_executor_attempts_every_stop_frame_after_motion_fault(self) -> None:
config, plan = self._compile(
"seat.set_driver_axis_native",
{"axis": "basin", "direction": "up", "duration_ms": 500},
)
bus = _FakeBus(fail_on_calls={2})
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
report = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1).execute(
self.runtime, plan, config
)
self.assertEqual(report["spec"]["status"], "failed")
self.assertEqual(bus.send_calls, 5)
self.assertEqual(
[frame.data for frame in bus.sent[-3:]],
[bytes((0x05, 0, 0, 0, 0, 0, 0, 0))] * 3,
)
self.assertIn("motion send failed", report["spec"]["results"][0]["message"])
self.assertTrue(bus.closed)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,291 @@
from __future__ import annotations
import json
from pathlib import Path
import shutil
import subprocess
import unittest
from cognitive_agent.application.conversation_state import ConversationStateStore
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
APP_SOURCE_PATH = REPOSITORY_ROOT / "apps" / "web-console" / "app.js"
EXTENSION_SOURCE_PATH = (
REPOSITORY_ROOT / "apps" / "web-console" / "self-extension.js"
)
def function_source(source: str, signature: str, next_signature: str) -> str:
start = source.index(signature)
end = source.index(next_signature, start)
return source[start:end]
class FrontendVehicleStateSyncContractTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.app_source = APP_SOURCE_PATH.read_text(encoding="utf-8")
cls.extension_source = EXTENSION_SOURCE_PATH.read_text(encoding="utf-8")
def test_both_modules_use_the_same_browser_conversation(self) -> None:
key = "cognitive-agent.conversation-id"
self.assertIn(
f'sessionStorage.getItem("{key}")',
self.app_source,
)
self.assertIn(
f'sessionStorage.setItem("{key}", conversationId)',
self.app_source,
)
self.assertIn(
f'const CONVERSATION_KEY = "{key}"',
self.extension_source,
)
self.assertIn(
"body: JSON.stringify({ command, conversation_id: sharedConversationId() })",
self.extension_source,
)
def test_lan_http_origin_does_not_require_crypto_random_uuid(self) -> None:
id_source = function_source(
self.app_source,
"function createOpaqueId(prefix) {",
"\nfunction createConversationId",
)
self.assertIn("globalThis.crypto?.randomUUID", id_source)
self.assertIn("globalThis.crypto?.getRandomValues", id_source)
self.assertIn("Math.random()", id_source)
self.assertIn('return createOpaqueId("conv")', self.app_source)
self.assertIn('createOpaqueId("hwc")', self.app_source)
def test_extension_accepts_only_explicit_physical_ch1_state(self) -> None:
authority_source = function_source(
self.extension_source,
" function isAuthoritativeVehicleStatePayload(payload) {",
"\n function renderAuthoritativeVehicleState",
)
self.assertIn(
'payload.conversation_id !== sharedConversationId()', authority_source
)
self.assertIn(
"containsAuthoritativePhysicalCh1State(payload.actuator_state)",
authority_source,
)
self.assertIn(
"containsAuthoritativePhysicalCh1State(payload.domain_state)",
authority_source,
)
self.assertNotIn("physical_feedback_confirmed", authority_source)
summary_source = function_source(
self.extension_source,
" function renderAuthoritativeVehicleState(payload) {",
"\n function setMode",
)
self.assertNotIn(
"items.length === 1 && payload.physical_feedback_confirmed",
summary_source,
)
for state_path in (
"ventilation?.feedback_source",
"heating?.feedback_source",
"climate?.feedback_source",
"roof?.feedback_source",
"frontLeftWindow?.feedback_source",
):
self.assertIn(f'{state_path} === "physical_can_ch1"', summary_source)
def test_orchestration_renders_only_the_physical_ch1_patch(self) -> None:
apply_source = function_source(
self.app_source,
"function applyAuthoritativeVehicleState(payload) {",
'\nwindow.addEventListener("cognitive-agent:extension-vehicle-result"',
)
self.assertIn(
"const actuatorPatch = physicalCh1StatePatch(payload.actuator_state)",
apply_source,
)
self.assertIn(
"const domainPatch = physicalCh1StatePatch(payload.domain_state)",
apply_source,
)
self.assertIn("renderSeatState(actuatorPatch)", apply_source)
self.assertIn("renderDomainState(domainPatch)", apply_source)
self.assertIn("if (actuatorPatch?.driver)", apply_source)
self.assertNotIn("renderSeatState(payload.actuator_state)", apply_source)
self.assertNotIn("renderDomainState(payload.domain_state)", apply_source)
render_source = function_source(
self.app_source,
"function renderResult(result) {",
"\nasync function submitRequest",
)
ch1_branch = function_source(
render_source,
" if (ch1HardwareResult) {",
" updateTwinExplicitAngles",
)
self.assertIn("applyAuthoritativeVehicleState(result)", ch1_branch)
self.assertIn("} else {", ch1_branch)
self.assertIn("renderSeatState(result.actuator_state)", ch1_branch)
self.assertIn("renderDomainState(result.domain_state)", ch1_branch)
def test_physical_patch_does_not_promote_unmarked_sibling_domains(self) -> None:
node = shutil.which("node")
if node is None:
self.skipTest("Node.js is required to execute the frontend patch helper")
patch_source = function_source(
self.app_source,
"function physicalCh1StatePatch(value) {",
"\nfunction isAuthoritativeVehicleStatePayload",
)
state = {
"climate": {
"feedback_source": "physical_can_ch1",
"power": "on",
},
"roof": {"shade_position_pct": 100},
"window": {
"positions": {"front_left": {"position_pct": 50}}
},
}
script = (
f"{patch_source}\n"
f"const input = {json.dumps(state)};\n"
"process.stdout.write(JSON.stringify(physicalCh1StatePatch(input)));"
)
completed = subprocess.run(
[node, "-e", script],
check=True,
capture_output=True,
text=True,
)
self.assertEqual(
json.loads(completed.stdout),
{"climate": state["climate"]},
)
def test_ch1_mode_never_polls_the_legacy_ch0_seat_endpoint(self) -> None:
sync_source = function_source(
self.app_source,
"async function synchronizePhysicalSeatState() {",
"\nasync function loadAuthStatus()",
)
guard = sync_source.index(
'target_policy === "ch1_body_can_driver_only"'
)
legacy_request = sync_source.index("/api/hardware/seat-state")
self.assertLess(guard, legacy_request)
self.assertIn("return false", sync_source[guard:legacy_request])
def test_vehicle_selection_cannot_replay_unfiltered_state_in_ch1_mode(self) -> None:
select_source = function_source(
self.app_source,
"function selectVehicle(vehicleId, { silent = false } = {}) {",
"\nfunction populateVehicleSelector",
)
driver_branch = select_source.index("if (isDriverOnlyCh1Runtime())")
filtered_snapshot = select_source.index(
"state.vehicleStateSnapshot?.actuatorState",
driver_branch,
)
unfiltered_result = select_source.index("state.result?.actuator_state")
self.assertLess(driver_branch, filtered_snapshot)
self.assertLess(filtered_snapshot, unfiltered_result)
self.assertIn("} else if (state.result?.actuator_state)", select_source)
def test_driver_only_ch1_has_no_hidden_browser_lease_gate(self) -> None:
submit_source = function_source(
self.app_source,
"async function submitRequest(event) {",
"\nasync function copyArtifact",
)
self.assertIn("const driverOnlyCh1 = isDriverOnlyCh1Runtime()", submit_source)
self.assertIn("&& !driverOnlyCh1", submit_source)
self.assertIn(
'state.executionMode === "hardware" && !driverOnlyCh1',
submit_source,
)
startup_source = self.app_source[
self.app_source.index('document.addEventListener("DOMContentLoaded"') :
]
self.assertIn("if (!isDriverOnlyCh1Runtime())", startup_source)
self.assertIn("loadHardwareAccessStatus", startup_source)
def test_state_results_flow_in_both_directions(self) -> None:
self.assertIn(
'window.addEventListener("cognitive-agent:extension-vehicle-result"',
self.app_source,
)
self.assertIn(
'window.addEventListener("cognitive-agent:result"',
self.extension_source,
)
class PhysicalMarkerInvalidationContractTests(unittest.TestCase):
def test_logical_door_and_window_actions_replace_stale_physical_roots(self) -> None:
store = ConversationStateStore()
conversation_id = "conv-frontend-marker-invalidation"
store.synchronize_physical_ch1_feedback(
conversation_id,
[
{
"source": "physical_can_ch1",
"channel": 1,
"frame_id": "0x3B6",
"feedback_frame_id": "0x3B6",
"valid": True,
"verification": {
"authoritative": True,
"channel": 1,
"simulated": False,
"feedback_required": True,
"feedback_observed": True,
"feedback_match": True,
"frame_id": "0x3B6",
},
"domain_state": {
"door": {"locks": {"rear_left": {"state": "lock"}}},
"window": {
"positions": {
"front_left": {
"position_pct": 20,
"closed": False,
}
}
},
},
}
],
)
physical = store.snapshot(conversation_id)["domain_devices"]
self.assertEqual(physical["door"]["feedback_source"], "physical_can_ch1")
self.assertEqual(physical["window"]["feedback_source"], "physical_can_ch1")
updated = store.apply_successful_actions(
conversation_id,
"logical projection must not look physical",
[
{
"domain": "door",
"capability": "door.set_rear_lock",
"target": "rear",
"parameters": {"state": "unlock"},
},
{
"domain": "window",
"capability": "window.set_position",
"target": "front_left",
"parameters": {"position_pct": 60},
},
],
[],
)["domain_devices"]
self.assertEqual(updated["door"]["feedback_source"], "virtual")
self.assertEqual(updated["window"]["feedback_source"], "virtual")
if __name__ == "__main__":
unittest.main()
+154
View File
@@ -0,0 +1,154 @@
from __future__ import annotations
from copy import deepcopy
import unittest
from cognitive_agent.application.native_can_feedback import (
authoritative_physical_ch1_feedback_state,
decode_feedback,
feedback_matches,
merge_state,
)
from cognitive_agent.infrastructure.can.gateway import CanFrame
class NativeCanFeedbackTests(unittest.TestCase):
def frame(self, frame_id: int, data_hex: str) -> CanFrame:
return CanFrame(frame_id, bytes.fromhex(data_hex), channel=1, is_fd=True)
def test_passive_ch1_samples_decode_stationary_vehicle(self) -> None:
gear = decode_feedback(self.frame(0x342, "00001fb1c1fff778"))
speed = decode_feedback(self.frame(0x4EB, "0700f00001516766"))
self.assertEqual(gear["vehicle_safety"]["gear"], "P")
self.assertEqual(speed["vehicle_safety"]["speed_kmh"], 0)
self.assertEqual(gear["source"], "physical_can_ch1")
self.assertEqual(gear["channel"], 1)
def test_climate_feedback_is_matched_by_signal_value(self) -> None:
feedback = self.frame(0x404, "4369116c1818a2dd")
decoded = decode_feedback(feedback)["domain_state"]["climate"]
self.assertEqual(decoded["power"], "on")
self.assertEqual(decoded["zones"]["driver"]["temperature_c"], 24)
self.assertTrue(feedback_matches("climate_power:on", feedback))
self.assertTrue(feedback_matches("climate_temperature:driver:24", feedback))
self.assertFalse(feedback_matches("climate_temperature:driver:25", feedback))
def test_window_position_match_uses_authoritative_0x388(self) -> None:
feedback = self.frame(0x388, "0000320000005500")
self.assertTrue(feedback_matches("window_position:front_left:50", feedback))
self.assertFalse(feedback_matches("window_position:front_left:80", feedback))
def test_light_feedback_requires_target_value_not_only_frame_id(self) -> None:
passive_feedback = self.frame(0x42E, "11140a03880200c6")
self.assertEqual(
decode_feedback(passive_feedback)["domain_state"]["interior_light"]["power"],
"unknown",
)
feedback = self.frame(0x42E, "11140a03200200c6")
decoded = decode_feedback(feedback)["domain_state"]
self.assertEqual(decoded["interior_light"]["power"], "off")
self.assertEqual(decoded["ambient_light"]["power"], "off")
self.assertTrue(feedback_matches("indoor_light:off", feedback))
self.assertFalse(feedback_matches("indoor_light:on", feedback))
self.assertTrue(feedback_matches("ambient_power:off", feedback))
def test_driver_seat_feedback_uses_0x4fa_not_0x42a(self) -> None:
passive_feedback = self.frame(0x4FA, "0980aa0000810000")
passive_driver = decode_feedback(passive_feedback)["actuator_state"]["driver"]
self.assertEqual(passive_driver["heating"], {"state": "off", "level": 0})
feedback = self.frame(0x4FA, "1180aa0000810000")
driver = decode_feedback(feedback)["actuator_state"]["driver"]
self.assertEqual(driver["ventilation"], {"state": "off", "level": 0})
self.assertEqual(driver["heating"], {"state": "on", "level": 1})
self.assertTrue(feedback_matches("driver_seat_thermal:heating:low", feedback))
self.assertFalse(feedback_matches("driver_seat_thermal:ventilation:high", feedback))
def test_driver_position_feedback_never_claims_absolute_position(self) -> None:
decoded = decode_feedback(
self.frame(0x494, "151515058c28203c")
)["actuator_state"]["driver"]["position_feedback"]
self.assertFalse(decoded["absolute_position_available"])
self.assertEqual(
set(decoded["initialized"]),
{"horizontal", "backrest", "height", "basin"},
)
def test_recursive_state_merge_preserves_other_devices(self) -> None:
target = {"driver": {"heating": {"state": "off"}}, "passenger": {"safe": True}}
update = {"driver": {"heating": {"state": "on", "level": 1}}}
merge_state(target, update)
self.assertEqual(target["driver"]["heating"]["state"], "on")
self.assertEqual(target["passenger"], {"safe": True})
def test_authoritative_state_requires_one_complete_consistent_ch1_frame(
self,
) -> None:
operation = {
"status": "succeeded",
"telemetry": {
"channel": 1,
"simulated": False,
"feedback_required": True,
"feedback_observed": True,
"feedback_match": True,
"feedback_frame": {
"frame_id": "0x404",
"payload_size": 8,
"data_hex": "5B 65 35 6C 18 18 A2 5D",
"timestamp": 12.5,
"channel": 1,
},
"feedback_state": {
"source": "physical_can_ch1",
"channel": 1,
"frame_id": "0x404",
"feedback_frame_id": "0x404",
"valid": True,
"domain_state": {"climate": {"power": "on"}},
},
},
}
state = authoritative_physical_ch1_feedback_state(operation)
self.assertIsNotNone(state)
self.assertTrue(state["verification"]["authoritative"])
self.assertEqual(state["verification"]["frame_id"], "0x404")
for label, mutate in (
(
"wrong frame channel",
lambda item: item["telemetry"]["feedback_frame"].update(
{"channel": 0}
),
),
(
"short payload",
lambda item: item["telemetry"]["feedback_frame"].update(
{"payload_size": 7}
),
),
(
"mismatched decoded id",
lambda item: item["telemetry"]["feedback_state"].update(
{"feedback_frame_id": "0x4FA"}
),
),
(
"empty decoded patch",
lambda item: item["telemetry"]["feedback_state"].pop(
"domain_state"
),
),
):
invalid = deepcopy(operation)
mutate(invalid)
with self.subTest(label=label):
self.assertIsNone(
authoritative_physical_ch1_feedback_state(invalid)
)
if __name__ == "__main__":
unittest.main()
+79
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
import copy
import unittest
from pathlib import Path
from unittest.mock import patch
@@ -140,6 +141,62 @@ class NativeCanIntegrationTests(unittest.TestCase):
).execute(self.runtime, plan, config)
self.assertEqual(report["spec"]["status"], "failed")
def test_executor_ignores_stale_wrong_value_until_target_feedback_arrives(self) -> None:
config, plan = self._compile(
"climate",
[
{
"domain": "climate",
"capability": "climate.set_power",
"target": "vehicle",
"operation": "set",
"parameters": {"state": "on"},
}
],
)
off = CanFrame(0x404, bytes((0, 2, 0, 0, 0, 0, 0, 0)), channel=1)
on = CanFrame(0x404, bytes((0, 1, 0, 0, 0, 0, 0, 0)), channel=1)
bus = _FakeBus([off, on])
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
report = NativeCanExecutor(lambda: bus, feedback_timeout_ms=10).execute(
self.runtime, plan, config
)
telemetry = report["spec"]["results"][0]["telemetry"]
self.assertEqual(report["spec"]["status"], "succeeded")
self.assertTrue(telemetry["feedback_match"])
self.assertEqual(telemetry["feedback_frame"]["data_hex"], "00 01 00 00 00 00 00 00")
self.assertEqual(telemetry["channel"], 1)
def test_executor_refuses_to_send_without_value_match_contract(self) -> None:
config, plan = self._compile(
"climate",
[
{
"domain": "climate",
"capability": "climate.set_power",
"target": "vehicle",
"operation": "set",
"parameters": {"state": "on"},
}
],
)
unsafe_plan = copy.deepcopy(plan)
unsafe_plan["spec"]["commands"][0]["signals"]["expected_feedback"] = ""
bus = _FakeBus()
report = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1).execute(
self.runtime, unsafe_plan, config
)
self.assertEqual(report["spec"]["status"], "failed")
self.assertFalse(bus.sent)
self.assertIn(
"target-value match contract",
report["spec"]["results"][0]["message"],
)
def test_one_sentence_builds_seat_and_vehicle_channel_plans(self) -> None:
service = SeatDemoService(REPOSITORY_ROOT)
result = service.process(
@@ -154,6 +211,28 @@ class NativeCanIntegrationTests(unittest.TestCase):
self.assertTrue(all(item["bus"] == "body_can" for item in body_commands))
self.assertFalse(result["deferred_requests"])
def test_retired_vehicle_lock_intent_returns_explicit_unavailable_result(self) -> None:
service = SeatDemoService(REPOSITORY_ROOT)
for index, command in enumerate(("door vehicle lock", "door vehicle unlock")):
with self.subTest(command=command):
result = service.process(
command,
provider="local",
conversation_id=f"conv-retired-door-lock-{index}",
execution_mode="dry_run",
)
self.assertEqual(result["status"], "capability_unavailable")
self.assertEqual(result["function_configs"], {})
self.assertEqual(result["command_plans"], {})
self.assertEqual(
result["deferred_requests"][0]["rejection_code"],
"CH1_DOOR_LOCK_CONTROL_UNSUPPORTED",
)
self.assertEqual(
result["deferred_requests"][0]["execution_backend"],
"unsupported",
)
def test_vehicle_timing_contract_matches_requested_60mhz_values(self) -> None:
settings = VehicleCanSettings.from_env(Path(REPOSITORY_ROOT))
self.assertTrue(settings.timing_is_verified)
+230
View File
@@ -0,0 +1,230 @@
from __future__ import annotations
import unittest
from cognitive_agent.application.conversation_state import ConversationStateStore
class NativeStateSyncTests(unittest.TestCase):
def setUp(self) -> None:
self.store = ConversationStateStore()
def apply(self, action: dict) -> dict:
return self.store.apply_successful_actions(
"conv-native-sync",
"test",
[{**action, "feedback_source": "physical_can_ch1"}],
[],
)
def test_driver_seat_native_feedback_never_updates_passenger(self) -> None:
state = self.apply(
{
"domain": "seat",
"capability": "seat.set_driver_heating_native",
"target": "driver",
"parameters": {"state": "low"},
}
)
self.assertEqual(state["devices"]["driver"]["heating"]["state"], "on")
self.assertEqual(state["devices"]["driver"]["heating"]["level"], 1)
self.assertEqual(
state["devices"]["driver"]["heating"]["feedback_source"],
"physical_can_ch1",
)
self.assertEqual(state["devices"]["passenger"]["heating"]["state"], "off")
def test_window_feedback_updates_frontend_domain_state(self) -> None:
state = self.apply(
{
"domain": "window",
"capability": "window.set_position",
"target": "front_left",
"parameters": {"position_pct": 25},
}
)
window = state["domain_devices"]["window"]["positions"]["front_left"]
self.assertEqual(window["position_pct"], 25)
self.assertFalse(window["closed"])
self.assertEqual(window["feedback_source"], "physical_can_ch1")
def test_rear_lock_updates_only_rear_locks(self) -> None:
state = self.apply(
{
"domain": "door",
"capability": "door.set_rear_lock",
"target": "rear",
"parameters": {"state": "lock"},
}
)
locks = state["domain_devices"]["door"]["locks"]
self.assertEqual(locks["rear_left"]["state"], "lock")
self.assertEqual(locks["rear_right"]["state"], "lock")
self.assertEqual(locks["front_left"]["state"], "unknown")
def test_vehicle_lock_projection_updates_logical_and_all_door_states(self) -> None:
state = self.apply(
{
"domain": "door",
"capability": "door.set_vehicle_lock",
"target": "vehicle",
"parameters": {"state": "lock"},
}
)
door = state["domain_devices"]["door"]
self.assertEqual(door["vehicle_lock"]["state"], "lock")
self.assertTrue(
all(item["state"] == "lock" for item in door["locks"].values())
)
def test_authoritative_ch1_feedback_overrides_requested_ui_projection(self) -> None:
state = self.store.synchronize_physical_ch1_feedback(
"conv-native-sync",
[
{
"source": "physical_can_ch1",
"channel": 1,
"frame_id": "0x404",
"feedback_frame_id": "0x404",
"valid": True,
"verification": {
"authoritative": True,
"channel": 1,
"simulated": False,
"feedback_required": True,
"feedback_observed": True,
"feedback_match": True,
"frame_id": "0x404",
"payload_size": 8,
},
"actuator_state": {
"driver": {
"ventilation": {"state": "on", "level": 3},
"massage": {"mode": "relax", "strength": "weak"},
}
},
"domain_state": {
"climate": {
"power": "on",
"ac": "on",
"control_mode": "manual",
"cycle_mode": "internal",
"fan_level": 3,
"zones": {
"driver": {
"temperature_c": 24,
"outlet_mode": "defrost",
}
},
},
"roof": {
"shade_position_pct": 2,
"motion_state": "stopped",
},
"ambient_light": {
"power": "on",
"front_brightness_level": 3,
"rear_brightness_level": 3,
"front_color_index": 32,
"rear_color_index": 32,
},
"door": {
"locks": {
"rear_left": {"state": "lock"},
"rear_right": {"state": "lock"},
}
},
"window": {
"positions": {
"front_left": {
"position_pct": 9,
"closed": False,
}
}
},
},
}
],
)
self.assertEqual(state["devices"]["driver"]["ventilation"]["level"], 3)
self.assertEqual(
state["devices"]["driver"]["ventilation"]["feedback_source"],
"physical_can_ch1",
)
climate = state["domain_devices"]["climate"]
self.assertEqual(climate["zones"]["driver"]["outlet_mode"], "defrost")
self.assertEqual(climate["cycle_mode"], "internal")
self.assertEqual(climate["feedback_source"], "physical_can_ch1")
self.assertEqual(state["domain_devices"]["roof"]["shade_position_pct"], 2)
self.assertEqual(state["domain_devices"]["ambient_light"]["brightness_pct"], 100)
self.assertEqual(
state["domain_devices"]["door"]["locks"]["rear_left"]["feedback_source"],
"physical_can_ch1",
)
self.assertEqual(
state["domain_devices"]["window"]["positions"]["front_left"]["position_pct"],
9,
)
self.assertNotIn(
"feedback_source",
state["domain_devices"]["window"]["positions"]["rear_left"],
)
self.assertNotIn(
"feedback_source",
state["domain_devices"]["door"]["locks"]["front_left"],
)
def test_non_authoritative_feedback_is_ignored(self) -> None:
before = self.store.snapshot("conv-native-sync")
after = self.store.synchronize_physical_ch1_feedback(
"conv-native-sync",
[
{
"source": "physical_can_ch0",
"channel": 0,
"valid": True,
"domain_state": {"climate": {"power": "on"}},
}
],
)
self.assertEqual(after, before)
def test_unverified_or_empty_feedback_never_updates_state(self) -> None:
before = self.store.snapshot("conv-native-sync")
for feedback in (
{
"source": "physical_can_ch1",
"channel": 1,
"frame_id": "0x404",
"feedback_frame_id": "0x404",
"valid": True,
"domain_state": {"climate": {"power": "on"}},
},
{
"source": "physical_can_ch1",
"channel": 1,
"frame_id": "0x404",
"feedback_frame_id": "0x404",
"valid": True,
"verification": {
"authoritative": True,
"channel": 1,
"simulated": False,
"feedback_required": True,
"feedback_observed": True,
"feedback_match": True,
"frame_id": "0x404",
"payload_size": 8,
},
},
):
with self.subTest(feedback=feedback):
after = self.store.synchronize_physical_ch1_feedback(
"conv-native-sync", [feedback]
)
self.assertEqual(after, before)
if __name__ == "__main__":
unittest.main()
+675 -6
View File
@@ -1,20 +1,36 @@
from __future__ import annotations
from collections.abc import Callable
from copy import deepcopy
import json
import os
import socket
import threading
import time
import unittest
from unittest.mock import Mock, patch
from unittest.mock import MagicMock, Mock, patch
from cognitive_agent.application.self_extension import (
ExtensionIntegrationError,
GatewayError,
SelfExtensionService,
SimulationGateway,
TcpGateway,
build_gateway_command,
validate_slot,
)
class SelfExtensionTests(unittest.TestCase):
@staticmethod
def _wait_until(predicate: Callable[[], bool], timeout: float = 3.0) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return True
time.sleep(0.02)
return predicate()
def test_gateway_command_contracts_and_slot_validation(self) -> None:
command = build_gateway_command("learn", {"slot": 17})
self.assertEqual(command["type"], "infrared")
@@ -59,7 +75,7 @@ class SelfExtensionTests(unittest.TestCase):
{
"kind": "vehicle",
"capability_id": "",
"command": "打开全部阅读灯",
"command": "light reading all on",
"label": "车内灯光 · 阅读灯",
},
],
@@ -82,21 +98,149 @@ class SelfExtensionTests(unittest.TestCase):
"risk": "low",
}
],
"vehicle_devices": [],
"vehicle_devices": [
{
"name": "车内灯光",
"actions": [
{
"name": "阅读灯",
"command": "light reading all on",
}
],
}
],
}
)
self.assertEqual(len(result["steps"]), 2)
self.assertEqual(result["steps"][0]["capability_id"], "cap-light-on")
self.assertEqual(result["steps"][1]["command"], "打开全部阅读灯")
self.assertEqual(result["steps"][1]["command"], "light reading all on")
def test_plan_rejects_vehicle_command_outside_current_catalog(self) -> None:
service = SelfExtensionService(SimulationGateway())
proposed = {
"summary": "unsafe target",
"needs_confirmation": False,
"question": "",
"steps": [
{
"kind": "vehicle",
"capability_id": "",
"command": "seat passenger vent high",
"label": "wrong seat",
"delay_ms": 0,
}
],
}
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
with patch.object(
service,
"_request_gpt_plan",
return_value=json.dumps(proposed),
):
with self.assertRaisesRegex(ExtensionIntegrationError, "CH1"):
service.plan(
{
"utterance": "打开座椅通风",
"peripherals": [],
"vehicle_devices": [
{
"name": "主驾座椅",
"actions": [
{
"name": "通风高档",
"command": "seat driver vent high",
}
],
}
],
}
)
def test_gpt_plan_honors_streaming_responses_configuration(self) -> None:
proposed = json.dumps(
{
"summary": "关闭空调",
"needs_confirmation": False,
"question": "",
"steps": [
{
"kind": "vehicle",
"capability_id": "",
"command": "ac off",
"label": "关闭空调",
}
],
},
ensure_ascii=False,
)
stream_body = "\n".join(
(
"event: response.output_text.delta",
"data: "
+ json.dumps(
{
"type": "response.output_text.delta",
"delta": proposed[:20],
},
ensure_ascii=False,
),
"event: response.output_text.done",
"data: "
+ json.dumps(
{"type": "response.output_text.done", "text": proposed},
ensure_ascii=False,
),
"data: [DONE]",
)
)
response = MagicMock()
response.__enter__.return_value = response
response.read.return_value = stream_body.encode("utf-8")
with patch.dict(os.environ, {"OPENAI_STREAM": "true"}, clear=False):
service = SelfExtensionService(SimulationGateway())
with patch(
"cognitive_agent.application.self_extension.urlopen",
return_value=response,
) as open_request:
output = service._request_gpt_plan(
"关闭空调",
[],
[
{
"name": "空调",
"actions": [{"name": "关闭空调", "command": "ac off"}],
}
],
"test-key",
)
self.assertEqual(json.loads(output)["steps"][0]["command"], "ac off")
request_payload = json.loads(
open_request.call_args.args[0].data.decode("utf-8")
)
self.assertIs(request_payload["stream"], True)
self.assertEqual(request_payload["model"], "gpt-5.6-sol")
command_schema = request_payload["text"]["format"]["schema"]["properties"][
"steps"
]["items"]["properties"]["command"]
self.assertEqual(command_schema["enum"], ["", "ac off"])
def test_vehicle_execution_reuses_the_same_orchestration_service(self) -> None:
service = SelfExtensionService(SimulationGateway())
with patch.dict(
os.environ,
{"COGNITIVE_AGENT_EXECUTION_MODE": "dry_run"},
clear=False,
):
service = SelfExtensionService(SimulationGateway())
vehicle_service = Mock()
vehicle_service.process.return_value = {
"status": "succeeded",
"provider_used": "local_rules",
"request_id": "req-extension-1",
"conversation_id": "conv-extension-shared",
"conversation_turn": 3,
"decision_summary": ["打开阅读灯"],
"validation": [],
"execution_report": {"spec": {"results": [{"status": "succeeded"}]}},
@@ -104,17 +248,542 @@ class SelfExtensionTests(unittest.TestCase):
}
result = service.execute_vehicle(
{"command": "打开全部阅读灯", "vehicle_id": "denza_n7"},
{
"command": "打开全部阅读灯",
"conversation_id": "conv-extension-shared",
"vehicle_id": "denza_n7",
},
vehicle_service,
hardware_access_bypass=True,
)
self.assertTrue(result["success"])
self.assertEqual(result["request_id"], "req-extension-1")
self.assertEqual(result["conversation_id"], "conv-extension-shared")
self.assertEqual(result["conversation_turn"], 3)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(
result["execution_report"], vehicle_service.process.return_value["execution_report"]
)
self.assertEqual(result["operation_results"], [{"status": "succeeded"}])
self.assertFalse(result["physical_feedback_required"])
self.assertFalse(result["physical_feedback_confirmed"])
vehicle_service.process.assert_called_once()
self.assertEqual(vehicle_service.process.call_args.args[0], "打开全部阅读灯")
self.assertEqual(
vehicle_service.process.call_args.args[2], "conv-extension-shared"
)
self.assertTrue(vehicle_service.process.call_args.kwargs["hardware_access_bypass"])
def test_vehicle_execution_only_marks_exact_complete_status_as_success(self) -> None:
service = SelfExtensionService(SimulationGateway())
for status in (
"validation_failed",
"needs_confirmation",
"capability_unavailable",
"partially_succeeded",
):
with self.subTest(status=status):
vehicle_service = Mock()
vehicle_service.process.return_value = {
"status": status,
"provider_used": "local_rules",
"request_id": f"req-{status}",
"decision_summary": [],
"validation": [],
"execution_report": {
"spec": {"results": [{"status": "succeeded"}]}
},
"deferred_requests": [],
}
result = service.execute_vehicle(
{"command": "打开全部阅读灯"},
vehicle_service,
hardware_access_bypass=True,
)
self.assertFalse(result["success"])
self.assertEqual(result["status"], status)
def test_vehicle_execution_rejects_failed_or_missing_operation_results(self) -> None:
service = SelfExtensionService(SimulationGateway())
for operation_results in (
[],
[{"status": "succeeded"}, {"status": "failed", "message": "timeout"}],
):
with self.subTest(operation_results=operation_results):
vehicle_service = Mock()
vehicle_service.process.return_value = {
"status": "succeeded",
"execution_report": {"spec": {"results": operation_results}},
"domain_state": {"roof": {"shade_position_pct": 31}},
"actuator_state": {"driver": {"heating": {"state": "off"}}},
}
result = service.execute_vehicle(
{"command": "打开遮阳帘"},
vehicle_service,
hardware_access_bypass=True,
)
self.assertFalse(result["success"])
self.assertEqual(
result["domain_state"], {"roof": {"shade_position_pct": 31}}
)
self.assertEqual(
result["actuator_state"],
{"driver": {"heating": {"state": "off"}}},
)
def test_hardware_vehicle_execution_requires_authoritative_feedback_per_item(
self,
) -> None:
with patch.dict(
os.environ,
{"COGNITIVE_AGENT_EXECUTION_MODE": "hardware"},
clear=False,
):
service = SelfExtensionService(SimulationGateway())
vehicle_service = Mock()
vehicle_service.process.return_value = {
"status": "succeeded",
"provider_used": "local_rules",
"request_id": "req-extension-hardware",
"execution_report": {
"spec": {
"status": "succeeded",
"physical_state": {
"source": "physical_can_ch1",
"complete": True,
},
"results": [
{
"ref": "operation-1",
"domain": "climate",
"status": "succeeded",
"telemetry": {
"channel": 1,
"simulated": False,
"feedback_required": True,
"feedback_observed": True,
"feedback_match": True,
"feedback_frame": {
"frame_id": "0x404",
"channel": 1,
"payload_size": 8,
"data_hex": "00 02 00 00 18 18 00 00",
},
"feedback_state": {
"source": "physical_can_ch1",
"channel": 1,
"frame_id": "0x404",
"feedback_frame_id": "0x404",
"valid": True,
"domain_state": {"climate": {"power": "on"}},
},
},
},
{
"ref": "operation-2",
"domain": "seat",
"status": "succeeded",
"telemetry": {
"channel": 1,
"simulated": False,
"feedback_required": True,
"feedback_observed": True,
"feedback_match": True,
"feedback_frame": {
"frame_id": "0x4FA",
"channel": 1,
"payload_size": 8,
"data_hex": "02 00 00 00 00 00 00 00",
},
"feedback_state": {
"source": "physical_can_ch1",
"channel": 1,
"frame_id": "0x4FA",
"feedback_frame_id": "0x4FA",
"valid": True,
"actuator_state": {
"driver": {
"ventilation": {
"state": "on",
"level": 1,
}
}
},
},
},
},
],
}
},
"domain_state": {"climate": {"power": "on"}},
"actuator_state": {"driver": {"ventilation": {"state": "on"}}},
}
confirmed = service.execute_vehicle(
{"command": "打开空调"},
vehicle_service,
hardware_access_bypass=True,
)
self.assertTrue(confirmed["success"])
self.assertTrue(confirmed["physical_feedback_required"])
self.assertTrue(confirmed["physical_feedback_confirmed"])
self.assertEqual(confirmed["physical_state"]["source"], "physical_can_ch1")
self.assertEqual(len(confirmed["physical_feedback"]), 2)
self.assertTrue(all(item["confirmed"] for item in confirmed["physical_feedback"]))
self.assertTrue(all(item["required"] for item in confirmed["physical_feedback"]))
self.assertTrue(all(item["observed"] for item in confirmed["physical_feedback"]))
self.assertTrue(all(item["match"] is True for item in confirmed["physical_feedback"]))
self.assertTrue(all(item["channel"] == 1 for item in confirmed["physical_feedback"]))
self.assertEqual(
confirmed["physical_feedback"][0]["state"]["source"],
"physical_can_ch1",
)
operation = vehicle_service.process.return_value["execution_report"]["spec"][
"results"
][1]
valid_telemetry = deepcopy(operation["telemetry"])
operation["telemetry"] = {**valid_telemetry, "feedback_match": None}
unconfirmed = service.execute_vehicle(
{"command": "打开空调"},
vehicle_service,
hardware_access_bypass=True,
)
self.assertFalse(unconfirmed["success"])
self.assertFalse(unconfirmed["physical_feedback_confirmed"])
self.assertIn("未逐项获得权威物理反馈", unconfirmed["message"])
invalid_cases: list[tuple[str, dict]] = []
for field, value in (
("channel", 0),
("simulated", True),
("feedback_required", False),
("feedback_observed", False),
("feedback_match", False),
("feedback_frame", None),
("feedback_state", None),
):
invalid_cases.append((field, {**valid_telemetry, field: value}))
wrong_frame_channel = deepcopy(valid_telemetry)
wrong_frame_channel["feedback_frame"]["channel"] = 0
invalid_cases.append(("feedback_frame.channel", wrong_frame_channel))
missing_frame_payload = deepcopy(valid_telemetry)
missing_frame_payload["feedback_frame"].pop("data_hex")
invalid_cases.append(("feedback_frame.data_hex", missing_frame_payload))
incomplete_frame_payload = deepcopy(valid_telemetry)
incomplete_frame_payload["feedback_frame"]["payload_size"] = 7
invalid_cases.append(
("feedback_frame.payload_size", incomplete_frame_payload)
)
wrong_source = deepcopy(valid_telemetry)
wrong_source["feedback_state"]["source"] = "physical_can_ch0"
invalid_cases.append(("feedback_state.source", wrong_source))
missing_decoded_state = deepcopy(valid_telemetry)
missing_decoded_state["feedback_state"].pop("actuator_state")
invalid_cases.append(("feedback_state.actuator_state", missing_decoded_state))
mismatched_frame = deepcopy(valid_telemetry)
mismatched_frame["feedback_state"]["frame_id"] = "0x404"
invalid_cases.append(("feedback_state.frame_id", mismatched_frame))
invalid_cases.append(
(
"legacy_ch0",
{
"channel": 0,
"simulated": False,
"hardware_feedback": True,
"feedback_frame": {"frame_id": "0x302"},
},
)
)
for label, insufficient_feedback in invalid_cases:
with self.subTest(insufficient_feedback=label):
operation["telemetry"] = insufficient_feedback
rejected = service.execute_vehicle(
{"command": "打开空调"},
vehicle_service,
hardware_access_bypass=True,
)
self.assertFalse(rejected["success"])
self.assertFalse(rejected["physical_feedback_confirmed"])
def test_vehicle_execution_ignores_browser_routing_overrides(self) -> None:
with patch.dict(
os.environ,
{
"COGNITIVE_AGENT_EXECUTION_MODE": "dry_run",
"COGNITIVE_AGENT_PROVIDER": "local_rules",
"COGNITIVE_AGENT_VEHICLE_ID": "denza_n7",
},
clear=False,
):
service = SelfExtensionService(SimulationGateway())
vehicle_service = Mock()
vehicle_service.process.return_value = {
"status": "succeeded",
"execution_report": {"spec": {"results": [{"status": "succeeded"}]}},
}
service.execute_vehicle(
{
"command": "seat driver vent low",
"execution_mode": "hardware",
"provider": "browser-selected-provider",
"vehicle_id": "t2g_primary_seat",
"simulation_fault": "adapter_disconnect",
"channel": "0",
},
vehicle_service,
hardware_access_bypass=True,
)
args = vehicle_service.process.call_args.args
kwargs = vehicle_service.process.call_args.kwargs
self.assertEqual(args[1], "local_rules")
self.assertEqual(args[3], "dry_run")
self.assertEqual(kwargs["vehicle_id"], "denza_n7")
self.assertEqual(kwargs["simulation_fault"], "normal")
def test_tcp_gateway_recovers_from_unavailable_address_and_tracks_peers(self) -> None:
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
probe.bind(("127.0.0.1", 0))
port = int(probe.getsockname()[1])
probe.close()
class UnavailableAddressSocket:
def setsockopt(self, *args: object) -> None:
del args
def bind(self, address: object) -> None:
del address
raise OSError(10049, "address is not available")
def close(self) -> None:
return
socket_attempts = 0
def new_server_socket() -> object:
nonlocal socket_attempts
socket_attempts += 1
if socket_attempts == 1:
return UnavailableAddressSocket()
return socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clients: list[socket.socket] = []
with patch.dict(
os.environ,
{
"GSCU_TCP_LISTEN_HOST": "127.0.0.1",
"GSCU_TCP_PORT": str(port),
"GSCU_TCP_BIND_RETRY_SECONDS": "0.05",
},
):
gateway = TcpGateway()
with patch.object(gateway, "_new_server_socket", side_effect=new_server_socket):
gateway.start()
try:
self.assertTrue(
self._wait_until(lambda: gateway.status()["listening"]),
gateway.status(),
)
self.assertGreaterEqual(socket_attempts, 2)
waiting = gateway.status()
self.assertFalse(waiting["connected"])
self.assertIn("等待设备接入", waiting["message"])
waiting_health = SelfExtensionService(gateway).health()
self.assertTrue(waiting_health["listening"])
self.assertFalse(waiting_health["peer_connected"])
first = socket.create_connection(("127.0.0.1", port), timeout=1)
clients.append(first)
self.assertTrue(
self._wait_until(lambda: gateway.status()["connected"]),
gateway.status(),
)
first_peer = gateway.status()["peer"]
self.assertTrue(str(first_peer).startswith("127.0.0.1:"))
connected_health = SelfExtensionService(gateway).health()
self.assertTrue(connected_health["connected"])
self.assertEqual(connected_health["peer"], first_peer)
# Replacing an old peer must not let its reader mark the new
# GSCU1B session offline when the old socket closes.
second = socket.create_connection(("127.0.0.1", port), timeout=1)
clients.append(second)
self.assertTrue(
self._wait_until(
lambda: gateway.status()["peer"] not in {None, first_peer}
),
gateway.status(),
)
time.sleep(0.1)
self.assertTrue(gateway.status()["connected"])
second.close()
clients.remove(second)
self.assertTrue(
self._wait_until(lambda: not gateway.status()["connected"]),
gateway.status(),
)
self.assertTrue(gateway.status()["listening"])
third = socket.create_connection(("127.0.0.1", port), timeout=1)
clients.append(third)
self.assertTrue(
self._wait_until(lambda: gateway.status()["connected"]),
gateway.status(),
)
finally:
for client in clients:
client.close()
gateway.close()
stopped = gateway.status()
self.assertFalse(stopped["listening"])
self.assertFalse(stopped["connected"])
def test_tcp_gateway_recovers_after_port_conflict_is_released(self) -> None:
blocker = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
blocker.bind(("127.0.0.1", 0))
blocker.listen(1)
port = int(blocker.getsockname()[1])
with patch.dict(
os.environ,
{
"GSCU_TCP_LISTEN_HOST": "127.0.0.1",
"GSCU_TCP_PORT": str(port),
"GSCU_TCP_BIND_RETRY_SECONDS": "0.05",
},
):
gateway = TcpGateway()
gateway.start()
try:
self.assertTrue(
self._wait_until(lambda: "监听失败" in gateway.status().get("message", "")),
gateway.status(),
)
self.assertFalse(gateway.status()["listening"])
blocker.close()
self.assertTrue(
self._wait_until(lambda: gateway.status()["listening"]),
gateway.status(),
)
self.assertNotIn("失败", gateway.status()["message"])
finally:
blocker.close()
gateway.close()
def test_tcp_gateway_can_restart_immediately_after_close(self) -> None:
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
probe.bind(("127.0.0.1", 0))
port = int(probe.getsockname()[1])
probe.close()
with patch.dict(
os.environ,
{
"GSCU_TCP_LISTEN_HOST": "127.0.0.1",
"GSCU_TCP_PORT": str(port),
"GSCU_TCP_BIND_RETRY_SECONDS": "0.05",
},
):
gateway = TcpGateway()
try:
gateway.start()
self.assertTrue(self._wait_until(lambda: gateway.status()["listening"]))
gateway.close()
gateway.start()
self.assertTrue(
self._wait_until(lambda: gateway.status()["listening"]),
gateway.status(),
)
finally:
gateway.close()
def test_tcp_gateway_rejects_peer_accepted_during_close(self) -> None:
settimeout_entered = threading.Event()
allow_settimeout_to_finish = threading.Event()
accepted_socket_closed = threading.Event()
unexpected_reader_started = threading.Event()
allow_unexpected_reader_to_finish = threading.Event()
class AcceptedSocket:
def settimeout(self, timeout: float) -> None:
del timeout
settimeout_entered.set()
allow_settimeout_to_finish.wait(timeout=2)
def setsockopt(self, *args: object) -> None:
del args
def recv(self, size: int) -> bytes:
del size
unexpected_reader_started.set()
allow_unexpected_reader_to_finish.wait(timeout=2)
return b""
def close(self) -> None:
accepted_socket_closed.set()
accepted_socket = AcceptedSocket()
class ServerSocket:
def setsockopt(self, *args: object) -> None:
del args
def bind(self, address: object) -> None:
del address
def listen(self, backlog: int) -> None:
del backlog
def settimeout(self, timeout: float) -> None:
del timeout
def accept(self) -> tuple[AcceptedSocket, tuple[str, int]]:
return accepted_socket, ("127.0.0.1", 4444)
def close(self) -> None:
return
with patch.dict(
os.environ,
{
"GSCU_TCP_LISTEN_HOST": "127.0.0.1",
"GSCU_TCP_PORT": "4444",
"GSCU_TCP_BIND_RETRY_SECONDS": "0.05",
},
):
gateway = TcpGateway()
with patch.object(gateway, "_new_server_socket", return_value=ServerSocket()):
gateway.start()
self.assertTrue(settimeout_entered.wait(timeout=1))
close_thread = threading.Thread(target=gateway.close)
close_thread.start()
try:
self.assertTrue(self._wait_until(gateway._stop.is_set))
finally:
allow_settimeout_to_finish.set()
close_thread.join(timeout=3)
try:
self.assertFalse(close_thread.is_alive())
self.assertTrue(accepted_socket_closed.is_set())
self.assertFalse(unexpected_reader_started.is_set())
self.assertFalse(gateway.status()["connected"])
self.assertIsNone(gateway.status()["peer"])
finally:
allow_unexpected_reader_to_finish.set()
if __name__ == "__main__":
unittest.main()
+286
View File
@@ -0,0 +1,286 @@
from __future__ import annotations
import json
import os
import unittest
from unittest.mock import MagicMock, patch
from cognitive_agent.application.self_extension import (
ExtensionIntegrationError,
MAX_PLAN_DELAY_MS,
MAX_TOTAL_PLAN_DELAY_MS,
SelfExtensionService,
SimulationGateway,
)
class SelfExtensionDelayPlanTests(unittest.TestCase):
def setUp(self) -> None:
self.service = SelfExtensionService(SimulationGateway())
@staticmethod
def _vehicle(command: str) -> dict[str, object]:
return {
"kind": "vehicle",
"capability_id": "",
"command": command,
"label": command,
"delay_ms": 0,
}
@staticmethod
def _delay(value: object) -> dict[str, object]:
return {
"kind": "delay",
"capability_id": "",
"command": "",
"label": "wait",
"delay_ms": value,
}
def _plan(
self,
steps: list[object],
*,
peripherals: list[dict[str, object]] | None = None,
) -> dict[str, object]:
proposed = {
"summary": "test plan",
"needs_confirmation": False,
"question": "",
"steps": steps,
}
vehicle_commands = [
str(step.get("command"))
for step in steps
if isinstance(step, dict)
and step.get("kind") == "vehicle"
and step.get("command")
]
with patch.dict(os.environ, {"OPENAI_API_KEY": "offline-test-key"}, clear=False):
with patch.object(
self.service,
"_request_gpt_plan",
return_value=json.dumps(proposed, ensure_ascii=False),
):
return self.service.plan(
{
"utterance": "test utterance",
"peripherals": peripherals or [],
"vehicle_devices": [
{
"name": "test vehicle device",
"actions": [
{"name": command, "command": command}
for command in vehicle_commands
],
}
],
}
)
def test_preserves_action_delay_action_order_and_canonicalizes_label(self) -> None:
peripherals = [
{
"id": "cactus-on",
"status": "learned",
"device_name": "跳舞仙人掌",
"action": "开启",
"risk": "low",
},
{
"id": "cactus-off",
"status": "learned",
"device_name": "跳舞仙人掌",
"action": "关闭",
"risk": "low",
},
]
steps = [
{
"kind": "peripheral",
"capability_id": "cactus-on",
"command": "",
"label": "ignored",
"delay_ms": 0,
},
self._delay(5_000),
{
"kind": "peripheral",
"capability_id": "cactus-off",
"command": "",
"label": "ignored",
"delay_ms": 0,
},
]
result = self._plan(steps, peripherals=peripherals)
self.assertEqual(
[step["kind"] for step in result["steps"]],
["peripheral", "delay", "peripheral"],
)
self.assertEqual(result["steps"][1]["delay_ms"], 5_000)
self.assertEqual(result["steps"][1]["label"], "等待 5 秒")
self.assertEqual(result["steps"][1]["risk"], "low")
def test_rejects_non_integer_or_out_of_range_delay_values(self) -> None:
invalid_values: tuple[object, ...] = (
"5000",
True,
False,
1.5,
None,
0,
-1,
MAX_PLAN_DELAY_MS + 1,
)
for value in invalid_values:
with self.subTest(value=value):
with self.assertRaises(ExtensionIntegrationError):
self._plan(
[
self._vehicle("light ambient on"),
self._delay(value),
self._vehicle("light ambient off"),
]
)
def test_rejects_delay_at_plan_boundaries_or_next_to_another_delay(self) -> None:
invalid_plans = (
[self._delay(1), self._vehicle("ac on")],
[self._vehicle("ac on"), self._delay(1)],
[
self._vehicle("ac on"),
self._delay(1),
self._delay(1),
self._vehicle("ac off"),
],
)
for steps in invalid_plans:
with self.subTest(steps=steps):
with self.assertRaises(ExtensionIntegrationError):
self._plan(steps)
def test_rejects_delay_that_becomes_boundary_after_invalid_action_is_removed(self) -> None:
with self.assertRaisesRegex(ExtensionIntegrationError, "两个可执行动作之间"):
self._plan(
[
{
"kind": "peripheral",
"capability_id": "not-learned",
"command": "",
"label": "invalid",
"delay_ms": 0,
},
self._delay(1_000),
self._vehicle("ac off"),
]
)
def test_enforces_cumulative_delay_limit(self) -> None:
accepted = self._plan(
[
self._vehicle("ac on"),
self._delay(MAX_PLAN_DELAY_MS),
self._vehicle("ac temp driver 24"),
self._delay(MAX_PLAN_DELAY_MS),
self._vehicle("ac off"),
]
)
self.assertEqual(
sum(
step.get("delay_ms", 0)
for step in accepted["steps"]
if step["kind"] == "delay"
),
MAX_TOTAL_PLAN_DELAY_MS,
)
with self.assertRaisesRegex(ExtensionIntegrationError, "累计延时"):
self._plan(
[
self._vehicle("ac on"),
self._delay(MAX_PLAN_DELAY_MS),
self._vehicle("ac temp driver 24"),
self._delay(MAX_PLAN_DELAY_MS),
self._vehicle("ac fan 3"),
self._delay(1),
self._vehicle("ac off"),
]
)
def test_rejects_device_fields_on_delay_step(self) -> None:
for field_name, value in (("command", "wait 5 seconds"), ("capability_id", "cap-1")):
with self.subTest(field_name=field_name):
delay_step = self._delay(5_000)
delay_step[field_name] = value
with self.assertRaisesRegex(ExtensionIntegrationError, "不能包含"):
self._plan(
[
self._vehicle("ac on"),
delay_step,
self._vehicle("ac off"),
]
)
def test_rejects_implicit_or_unknown_delay_representation(self) -> None:
non_delay_with_wait = self._vehicle("ac on")
non_delay_with_wait["delay_ms"] = 5_000
with self.assertRaisesRegex(ExtensionIntegrationError, "非延时步骤"):
self._plan([non_delay_with_wait, self._vehicle("ac off")])
with self.assertRaisesRegex(ExtensionIntegrationError, "未知步骤类型"):
self._plan(
[
self._vehicle("ac on"),
{
"kind": "wait",
"capability_id": "",
"command": "",
"label": "wait",
"delay_ms": 5_000,
},
self._vehicle("ac off"),
]
)
def test_gpt_schema_and_prompt_define_millisecond_delay_contract(self) -> None:
response = MagicMock()
response.__enter__.return_value = response
response.read.return_value = json.dumps({"output_text": "{}"}).encode("utf-8")
with patch.object(self.service, "openai_streaming", False):
with patch(
"cognitive_agent.application.self_extension.urlopen",
return_value=response,
) as open_request:
self.service._request_gpt_plan(
"开启跳舞仙人掌,5s 后关闭", [], [], "test-key"
)
request_payload = json.loads(open_request.call_args.args[0].data.decode("utf-8"))
plan_format = request_payload["text"]["format"]
step_schema = plan_format["schema"]["properties"]["steps"]["items"]
self.assertTrue(plan_format["strict"])
self.assertIn("delay", step_schema["properties"]["kind"]["enum"])
self.assertIn("delay_ms", step_schema["required"])
self.assertEqual(
step_schema["properties"]["delay_ms"]["maximum"],
MAX_PLAN_DELAY_MS,
)
prompt = "\n".join(
content["text"]
for message in request_payload["input"]
if message.get("role") == "system"
for content in message.get("content", [])
if content.get("type") == "input_text"
)
self.assertIn("delay_ms=5000", prompt)
self.assertIn(str(MAX_TOTAL_PLAN_DELAY_MS), prompt)
self.assertIn("不能位于首尾", prompt)
self.assertIn("绝不能把等待时间写进 command", prompt)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,58 @@
from __future__ import annotations
from pathlib import Path
import unittest
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
SOURCE_PATH = REPOSITORY_ROOT / "apps" / "web-console" / "self-extension.js"
class SelfExtensionFrontendDelayContractTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.source = SOURCE_PATH.read_text(encoding="utf-8")
hydrate_start = cls.source.index(" function hydratePlan(steps) {")
hydrate_end = cls.source.index("\n function setExecutionState", hydrate_start)
cls.hydrate_source = cls.source[hydrate_start:hydrate_end]
execute_start = cls.source.index(" async function executePlan(plan) {")
execute_end = cls.source.index("\n function addHistory", execute_start)
cls.execute_source = cls.source[execute_start:execute_end]
def test_hydration_recognizes_and_strictly_validates_delay_steps(self) -> None:
self.assertIn('if (step.kind === "delay")', self.hydrate_source)
self.assertIn("Number.isInteger(step.delay_ms)", self.hydrate_source)
self.assertIn("step.delay_ms < MIN_PLAN_DELAY_MS", self.hydrate_source)
self.assertIn("step.delay_ms > MAX_PLAN_DELAY_MS", self.hydrate_source)
self.assertIn("totalDelayMs > MAX_TOTAL_PLAN_DELAY_MS", self.hydrate_source)
self.assertIn("index === 0 || index === plan.length - 1", self.hydrate_source)
self.assertIn('plan[index - 1].kind === "delay"', self.hydrate_source)
self.assertIn("step.delay_ms !== undefined && step.delay_ms !== 0", self.hydrate_source)
self.assertNotIn(".filter(Boolean)", self.hydrate_source)
def test_hydration_displays_a_canonical_five_second_label(self) -> None:
self.assertIn("`等待 ${step.delay_ms / 1000} 秒`", self.hydrate_source)
self.assertIn("delayMs: step.delay_ms", self.hydrate_source)
def test_execution_routes_delay_only_to_the_timer(self) -> None:
delay_branch = self.execute_source.index('if (step.kind === "delay")')
vehicle_branch = self.execute_source.index('else if (step.kind === "vehicle")')
peripheral_branch = self.execute_source.index('await sendGatewayCommand("emit"')
self.assertLess(delay_branch, vehicle_branch)
self.assertLess(vehicle_branch, peripheral_branch)
between_delay_and_vehicle = self.execute_source[delay_branch:vehicle_branch]
self.assertIn("await delay(step.delayMs);", between_delay_and_vehicle)
self.assertNotIn("executeVehicle", between_delay_and_vehicle)
self.assertNotIn("sendGatewayCommand", between_delay_and_vehicle)
def test_explicit_delay_does_not_stack_with_default_action_spacing(self) -> None:
self.assertIn("const nextStep = plan[index + 1];", self.execute_source)
self.assertIn(
'nextStep && step.kind !== "delay" && nextStep.kind !== "delay"',
self.execute_source,
)
self.assertIn("await delay(state.settings.actionDelayMs);", self.execute_source)
if __name__ == "__main__":
unittest.main()
+141
View File
@@ -0,0 +1,141 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from cognitive_agent.application.native_can_feedback import (
VehicleSafetyPreflightError,
observe_vehicle_safety,
require_stationary_vehicle,
)
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.infrastructure.can.gateway import CanFrame
from cognitive_agent.infrastructure.can.hardware import HardwareSafetyError
class _ReceiveOnlyBus:
def __init__(self, frames: list[CanFrame]) -> None:
self.frames = list(frames)
self.send_called = False
def send(self, *args, **kwargs): # pragma: no cover - a safety tripwire
self.send_called = True
raise AssertionError("passive safety preflight must never transmit")
def receive(self, timeout_ms: int, *, include_fd: bool = False):
return self.frames.pop(0) if self.frames else None
def close(self) -> None:
return None
class _Bridge:
def __init__(self, bus: _ReceiveOnlyBus) -> None:
self.bus = bus
self.closed = False
def channel_bus(self, channel: int) -> _ReceiveOnlyBus:
if channel != 1:
raise AssertionError(f"unexpected channel {channel}")
return self.bus
def close(self) -> None:
self.closed = True
def _gear(raw: int, *, channel: int = 1) -> CanFrame:
data = bytearray(8)
data[3] = raw & 0x0F
return CanFrame(0x342, bytes(data), channel=channel)
def _speed(raw: int, *, channel: int = 1) -> CanFrame:
data = bytearray(8)
data[0] = 0x07
data[1] = raw & 0xFF
data[2] = (raw >> 8) & 0x0F
return CanFrame(0x4EB, bytes(data), channel=channel)
class VehicleSafetyPreflightTests(unittest.TestCase):
def test_fresh_ch1_p_and_zero_speed_pass_without_transmitting(self) -> None:
bus = _ReceiveOnlyBus([_gear(1), _speed(0)])
snapshot = observe_vehicle_safety(bus, timeout_ms=100)
require_stationary_vehicle(snapshot)
self.assertTrue(snapshot["stationary"])
self.assertEqual(snapshot["evidence"]["gear"]["frame_id"], "0x342")
self.assertEqual(snapshot["evidence"]["speed"]["frame_id"], "0x4EB")
self.assertFalse(bus.send_called)
def test_ch0_frames_cannot_satisfy_the_gate(self) -> None:
snapshot = observe_vehicle_safety(
_ReceiveOnlyBus([_gear(1, channel=0), _speed(0, channel=0)]),
timeout_ms=100,
)
self.assertFalse(snapshot["complete"])
self.assertEqual(snapshot["ignored_wrong_channel"], 2)
with self.assertRaisesRegex(
VehicleSafetyPreflightError, "0x342 gear evidence"
):
require_stationary_vehicle(snapshot)
def test_drive_or_nonzero_speed_fails_closed(self) -> None:
for frames, message in (
([_gear(4), _speed(0)], "not confirmed in P"),
([_gear(1), _speed(10)], "not confirmed at 0 km/h"),
):
with self.subTest(message=message):
snapshot = observe_vehicle_safety(
_ReceiveOnlyBus(frames), timeout_ms=100
)
with self.assertRaisesRegex(VehicleSafetyPreflightError, message):
require_stationary_vehicle(snapshot)
def test_policy_cannot_be_redirected_to_ch0(self) -> None:
with self.assertRaisesRegex(VehicleSafetyPreflightError, "only CH1"):
observe_vehicle_safety(
_ReceiveOnlyBus([_gear(1, channel=0), _speed(0, channel=0)]),
timeout_ms=100,
required_channel=0,
)
def test_service_refuses_before_any_command_when_vehicle_is_not_in_p(self) -> None:
service = SeatDemoService(REPOSITORY_ROOT)
action = {
"domain": "climate",
"capability": "climate.set_power",
"target": "vehicle",
"operation": "set",
"parameters": {"state": "on"},
}
config = service.native_can_runtime.build_function_config(
"climate", [action], "req-safety-preflight", "local_rules"
)
plan = service.native_can_runtime.compile(config)
bus = _ReceiveOnlyBus([_gear(4), _speed(0)])
bridge = _Bridge(bus)
with patch.object(
type(service.vehicle_can_settings),
"bridge",
return_value=bridge,
):
with self.assertRaisesRegex(HardwareSafetyError, "not confirmed in P"):
service._execute_orchestrated_reports(
{"native_climate": config},
{"native_climate": plan},
{"native_climate"},
{},
hardware_request=True,
simulated_hardware_request=False,
simulation_fault="normal",
)
self.assertFalse(bus.send_called)
self.assertTrue(bridge.closed)
if __name__ == "__main__":
unittest.main()
+608
View File
@@ -0,0 +1,608 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from cognitive_agent.application.native_can_feedback import (
decode_feedback,
feedback_matches,
)
from cognitive_agent.application.native_can_intent import (
_parse_exact_command,
merge_native_can_intent,
)
from cognitive_agent.application.native_can_runtime import (
NativeCanCompileError,
NativeCanExecutor,
NativeCanRuntime,
)
from cognitive_agent.application.seat_service import REPOSITORY_ROOT
from cognitive_agent.infrastructure.can.gateway import CanFrame
from cognitive_agent.runtime.contracts import ContractValidator
class _FakeBus:
def __init__(
self,
received: list[CanFrame] | None = None,
*,
fail_send_calls: set[int] | None = None,
) -> None:
self.received = list(received or [])
self.fail_send_calls = set(fail_send_calls or set())
self.send_attempts: list[tuple[int, bytes, bool, bool]] = []
self.sent: list[CanFrame] = []
self.events: list[str] = []
self.closed = False
def send(
self,
arbitration_id: int,
data: bytes,
*,
is_fd: bool = False,
bitrate_switch: bool = False,
) -> CanFrame:
self.events.append("send")
self.send_attempts.append(
(arbitration_id, bytes(data), is_fd, bitrate_switch)
)
call_number = len(self.send_attempts)
if call_number in self.fail_send_calls:
raise RuntimeError(f"injected send failure {call_number}")
frame = CanFrame(
arbitration_id,
bytes(data),
channel=1,
is_fd=is_fd,
bitrate_switch=bitrate_switch,
)
self.sent.append(frame)
return frame
def receive(self, timeout_ms: int, *, include_fd: bool = False) -> CanFrame | None:
self.events.append("receive")
return self.received.pop(0) if self.received else None
def close(self) -> None:
self.closed = True
class VerifiedBodyCanTests(unittest.TestCase):
def setUp(self) -> None:
self.runtime = NativeCanRuntime(REPOSITORY_ROOT)
def compile_action(
self, domain: str, capability: str, target: str, parameters: dict
) -> dict:
config = self.runtime.build_function_config(
domain,
[
{
"domain": domain,
"capability": capability,
"target": target,
"operation": "set",
"parameters": parameters,
}
],
"verified-body-can",
"local_rules",
)
return self.runtime.compile(config)["spec"]["commands"][0]
@staticmethod
def shade_feedback(position: int, motion: int = 1) -> CanFrame:
data = bytearray(8)
data[1] = position
data[3] = motion << 3
return CanFrame(0x41A, bytes(data), channel=1)
def assert_fd_brs_triplet(self, command: dict, frame_id: str) -> None:
self.assertEqual(command["signals"]["frame_id"], frame_id)
self.assertTrue(command["signals"]["is_fd"])
self.assertTrue(command["signals"]["bitrate_switch"])
self.assertEqual(command["repeat"], {"count": 3, "period_ms": 100})
def test_indoor_and_all_reading_lights_use_only_verified_4f5(self) -> None:
cases = (
("interior_light.set_indoor_power", "vehicle", "on", "00 00 03 FF FF 00 08 00"),
("interior_light.set_indoor_power", "vehicle", "off", "00 00 03 FF FF 00 04 00"),
("interior_light.set_reading_power", "all", "on", "00 00 03 FF FF 00 08 00"),
("interior_light.set_reading_power", "all", "off", "00 00 03 FF FF 00 04 00"),
)
for capability, target, state, expected_hex in cases:
with self.subTest(capability=capability, state=state):
command = self.compile_action(
"interior_light", capability, target, {"state": state}
)
self.assert_fd_brs_triplet(command, "0x4F5")
self.assertEqual(command["signals"]["data_hex"], expected_hex)
self.assertEqual(command["signals"]["feedback_ids"], "0x42E")
self.assertEqual(
command["signals"]["expected_feedback"],
f"indoor_light:{state}",
)
def test_unverified_light_controls_are_not_hardware_capabilities(self) -> None:
rejected = (
("interior_light.set_wall_power", "vehicle", {"state": "on"}),
("ambient_light.set_mode", "vehicle", {"mode": "cinema"}),
(
"ambient_light.set_illumination",
"vehicle",
{"color_hex": "#203040", "brightness_pct": 40},
),
)
for capability, target, parameters in rejected:
action = {"capability": capability, "target": target, "parameters": parameters}
with self.subTest(capability=capability):
self.assertFalse(self.runtime.supports(action))
with self.assertRaises(NativeCanCompileError):
self.runtime.build_function_config(
capability.split(".", 1)[0],
[action],
"reject-unverified-light",
"local_rules",
)
zoned = {
"capability": "interior_light.set_reading_power",
"target": "front_left",
"parameters": {"state": "on"},
}
self.assertFalse(self.runtime.supports(zoned))
with self.assertRaises(NativeCanCompileError):
config = self.runtime.build_function_config(
"interior_light", [zoned], "reject-zoned-reading", "local_rules"
)
self.runtime.compile(config)
def test_exact_parser_exposes_only_verified_light_vocabulary(self) -> None:
self.assertEqual(_parse_exact_command("light wall on"), [])
self.assertEqual(_parse_exact_command("light reading front-left on"), [])
self.assertEqual(_parse_exact_command("light ambient scene cinema"), [])
self.assertEqual(_parse_exact_command("light ambient rgb 32 64 96 40"), [])
color = _parse_exact_command("light ambient color 70")
self.assertEqual(color[0]["capability"], "ambient_light.set_color_index")
self.assertEqual(color[0]["parameters"], {"color_index": 70})
brightness = _parse_exact_command("light ambient brightness 5")
self.assertEqual(
brightness[0]["capability"], "ambient_light.set_brightness_level"
)
self.assertEqual(brightness[0]["parameters"], {"brightness_level": 5})
def test_fully_exact_ac_power_is_the_only_executable_plan(self) -> None:
for requested_state in ("on", "off"):
opposite_state = "off" if requested_state == "on" else "on"
for model_capability, model_state in (
("climate.set_ac", requested_state),
("climate.set_ac", opposite_state),
("climate.set_power", opposite_state),
):
with self.subTest(
requested_state=requested_state,
model_capability=model_capability,
model_state=model_state,
):
model_request = {
"domain": "climate",
"capability": model_capability,
"target_scope": "vehicle",
"operation": "set",
"parameters": [{"name": "state", "value": model_state}],
}
routed_action = {
"domain": "climate",
"capability": model_capability,
"target": "vehicle",
"operation": "set",
"parameters": {"state": model_state},
}
parsed = {
"detected_domains": ["climate", "window", "seat"],
"actions": [
{
"target": "passenger",
"function": "ventilation",
"state": "on",
}
],
"model_requests": [model_request],
"domain_actions": [
routed_action,
{
"domain": "window",
"capability": "window.set_motion",
"target": "front_left",
"operation": "set",
"parameters": {"motion": "open"},
},
],
"native_can_actions": [routed_action],
"deferred_requests": [routed_action],
"preset_actions": [{"target": "passenger"}],
"preset_events": [{"action": "apply"}],
"ambiguities": [
{"field": "requests[0].target", "question": "hallucinated"}
],
}
merged = merge_native_can_intent(
parsed, f"ac {requested_state}"
)
self.assertEqual(merged["actions"], [])
self.assertEqual(merged["domain_actions"], [])
self.assertEqual(merged["preset_actions"], [])
self.assertEqual(merged["preset_events"], [])
self.assertEqual(merged["ambiguities"], [])
self.assertEqual(merged["detected_domains"], ["climate"])
self.assertEqual(len(merged["model_requests"]), 1)
self.assertEqual(len(merged["deferred_requests"]), 1)
self.assertEqual(len(merged["native_can_actions"]), 1)
for key in (
"model_requests",
"deferred_requests",
"native_can_actions",
):
self.assertEqual(
merged[key][0]["capability"], "climate.set_power"
)
parameters = merged[key][0]["parameters"]
if isinstance(parameters, list):
parameters = {
item["name"]: item["value"] for item in parameters
}
self.assertEqual(parameters, {"state": requested_state})
natural_model_action = {
"domain": "climate",
"capability": "climate.set_ac",
"target": "vehicle",
"operation": "set",
"parameters": {"state": "on"},
}
natural = merge_native_can_intent(
{
"model_requests": [natural_model_action],
"domain_actions": [natural_model_action],
"native_can_actions": [],
"deferred_requests": [],
},
"打开空调并关闭遮阳帘",
)
self.assertIn(natural_model_action, natural["domain_actions"])
self.assertGreaterEqual(len(natural["native_can_actions"]), 2)
mixed_shade_action = {
"domain": "roof",
"capability": "roof.set_shade_motion",
"target": "vehicle",
"operation": "set",
"parameters": {"motion": "close"},
}
mixed = merge_native_can_intent(
{
"model_requests": [],
"domain_actions": [mixed_shade_action],
"native_can_actions": [],
"deferred_requests": [],
},
"ac on,关闭遮阳帘",
)
self.assertIn(mixed_shade_action, mixed["domain_actions"])
self.assertEqual(
[item["capability"] for item in mixed["native_can_actions"]],
["climate.set_power"],
)
def test_vehicle_lock_is_observational_only_and_fails_closed(self) -> None:
action = {
"domain": "door",
"capability": "door.set_vehicle_lock",
"target": "vehicle",
"operation": "set",
"parameters": {"state": "lock"},
}
catalog_entry = self.runtime.capabilities[action["capability"]]
self.assertEqual(catalog_entry["execution_support"], "observational_only")
self.assertFalse(catalog_entry["physical_control_supported"])
self.assertNotIn(action["capability"], self.runtime.supported_capabilities)
self.assertFalse(self.runtime.supports(action))
with self.assertRaisesRegex(
NativeCanCompileError,
r"unsupported.*current CH1.*0x496.*observational only",
):
self.runtime.build_function_config(
"door", [action], "retired-vehicle-lock", "local_rules"
)
with self.assertRaisesRegex(
NativeCanCompileError, r"unsupported.*current CH1.*observational only"
):
self.runtime._frame_specs({"capability": action["capability"]})
vehicle = _parse_exact_command("door vehicle lock")
self.assertEqual(vehicle[0]["capability"], "door.set_vehicle_lock")
self.assertEqual(vehicle[0]["target"], "vehicle")
def test_legacy_rear_lock_is_retired_before_compile(self) -> None:
action = {
"domain": "door",
"capability": "door.set_rear_lock",
"target": "rear",
"operation": "set",
"parameters": {"state": "lock"},
}
catalog_entry = self.runtime.capabilities[action["capability"]]
self.assertEqual(catalog_entry["execution_support"], "observational_only")
self.assertFalse(catalog_entry["physical_control_supported"])
self.assertNotIn(action["capability"], self.runtime.supported_capabilities)
self.assertFalse(self.runtime.supports(action))
with self.assertRaisesRegex(
NativeCanCompileError,
r"unsupported.*current CH1.*0x32B.*observational only",
):
self.runtime.build_function_config(
"door", [action], "retired-rear-lock", "local_rules"
)
with self.assertRaisesRegex(
NativeCanCompileError, r"unsupported.*current CH1.*observational only"
):
self.runtime._frame_specs({"capability": action["capability"]})
self.assertEqual(_parse_exact_command("door rear lock"), [])
def test_ambient_controls_use_only_verified_3fc_layouts(self) -> None:
cases = (
("ambient_light.set_power", {"state": "on"}, "00 36 00 75 00 80 00 00", "ambient_power:on"),
("ambient_light.set_power", {"state": "off"}, "00 31 00 75 00 80 00 00", "ambient_power:off"),
("ambient_light.set_color_index", {"color_index": 70}, "46 30 00 75 00 00 00 00", "ambient_color:70"),
("ambient_light.set_brightness_level", {"brightness_level": 1}, "00 32 00 75 00 00 00 00", "ambient_brightness:1"),
("ambient_light.set_brightness_level", {"brightness_level": 5}, "00 36 00 75 00 00 00 00", "ambient_brightness:5"),
)
for capability, parameters, payload, expected_feedback in cases:
with self.subTest(capability=capability, parameters=parameters):
command = self.compile_action(
"ambient_light", capability, "vehicle", parameters
)
self.assert_fd_brs_triplet(command, "0x3FC")
self.assertEqual(command["signals"]["data_hex"], payload)
self.assertEqual(command["signals"]["feedback_ids"], "0x42E")
self.assertEqual(
command["signals"]["expected_feedback"], expected_feedback
)
def test_verified_ambient_function_configs_pass_domain_schema(self) -> None:
validator = ContractValidator(REPOSITORY_ROOT)
for capability, parameters in (
("ambient_light.set_color_index", {"color_index": 70}),
("ambient_light.set_brightness_level", {"brightness_level": 5}),
):
with self.subTest(capability=capability):
config = self.runtime.build_function_config(
"ambient_light",
[
{
"domain": "ambient_light",
"capability": capability,
"target": "vehicle",
"operation": "set",
"parameters": parameters,
}
],
"verified-ambient-schema",
"local_rules",
)
self.assertEqual(validator.validate(config), [])
def test_ambient_ranges_and_parameter_shapes_fail_closed(self) -> None:
for capability, parameters in (
("ambient_light.set_color_index", {"color_index": 0}),
("ambient_light.set_color_index", {"color_index": 128}),
("ambient_light.set_brightness_level", {"brightness_level": 0}),
("ambient_light.set_brightness_level", {"brightness_level": 6}),
(
"ambient_light.set_color_index",
{"color_index": 70, "brightness_level": 3},
),
):
with self.subTest(capability=capability, parameters=parameters):
with self.assertRaises(NativeCanCompileError):
self.compile_action("ambient_light", capability, "vehicle", parameters)
def test_shade_presets_and_arbitrary_position_never_use_4f5(self) -> None:
for position, payload in (
(0, "20 00 00 00 00 00 00 00"),
(50, "30 00 00 00 00 00 00 00"),
(100, "10 00 00 00 00 00 00 00"),
):
with self.subTest(position=position):
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": position}
)
self.assertEqual(command["signals"]["frame_id"], "0x431")
self.assertEqual(command["signals"]["data_hex"], payload)
self.assertFalse(command["signals"]["is_fd"])
self.assertEqual(command["signals"]["feedback_ids"], "0x41A")
self.assertEqual(command["signals"]["special_handler"], "shade_preset")
arbitrary = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 37}
)
self.assertEqual(arbitrary["signals"]["frame_id"], "0x431")
self.assertNotEqual(arbitrary["signals"]["frame_id"], "0x4F5")
self.assertEqual(arbitrary["signals"]["special_handler"], "shade_position")
special = arbitrary["signals"]["special_parameters"]
self.assertIn('"fresh_feedback_timeout_ms": 2500', special)
self.assertIn('"stop_data_hex": "40 00 00 00 00 00 00 00"', special)
def test_shade_preset_executor_waits_for_authoritative_target(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 100}
)
reached = self.shade_feedback(99, motion=1)
bus = _FakeBus([reached])
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
sent, feedback = executor._execute_shade_preset(bus, command)
self.assertIs(feedback, reached)
self.assertEqual(len(sent), 1)
self.assertEqual(
bus.send_attempts,
[(0x431, bytes.fromhex("10 00 00 00 00 00 00 00"), False, False)],
)
def test_shade_preset_forces_stop_when_target_feedback_is_still_moving(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 0}
)
reached_while_closing = self.shade_feedback(2, motion=3)
stopped = self.shade_feedback(2, motion=1)
bus = _FakeBus([reached_while_closing, stopped])
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
sent, feedback = executor._execute_shade_preset(bus, command)
self.assertIs(feedback, stopped)
self.assertEqual(len(sent), 2)
self.assertEqual(
[attempt[1] for attempt in bus.send_attempts],
[
bytes.fromhex("20 00 00 00 00 00 00 00"),
bytes.fromhex("40 00 00 00 00 00 00 00"),
],
)
def test_arbitrary_shade_position_reads_fresh_state_then_stops(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 37}
)
before = self.shade_feedback(20, motion=1)
reached = self.shade_feedback(38, motion=2)
stopped = self.shade_feedback(38, motion=1)
bus = _FakeBus([before, reached, stopped])
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
sent, feedback = executor._execute_shade_position(bus, command)
self.assertIs(feedback, stopped)
self.assertEqual(bus.events[0], "receive")
self.assertEqual(
[attempt[1] for attempt in bus.send_attempts],
[
bytes.fromhex("10 00 00 00 00 00 00 00"),
bytes.fromhex("40 00 00 00 00 00 00 00"),
],
)
self.assertTrue(all(not attempt[2] and not attempt[3] for attempt in bus.send_attempts))
self.assertEqual(len(sent), 2)
def test_arbitrary_shade_position_always_stops_after_feedback_fault(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 37}
)
bus = _FakeBus()
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
before = self.shade_feedback(20, motion=1)
with patch.object(
executor,
"_wait_for_id",
side_effect=[before, RuntimeError("injected receive failure")],
):
with self.assertRaisesRegex(RuntimeError, "injected receive failure"):
executor._execute_shade_position(bus, command)
self.assertEqual(
[attempt[1] for attempt in bus.send_attempts],
[
bytes.fromhex("10 00 00 00 00 00 00 00"),
bytes.fromhex("40 00 00 00 00 00 00 00"),
],
)
def test_arbitrary_shade_start_failure_still_attempts_stop(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 37}
)
bus = _FakeBus([self.shade_feedback(20, motion=1)], fail_send_calls={1})
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
with self.assertRaisesRegex(RuntimeError, "injected send failure"):
executor._execute_shade_position(bus, command)
self.assertEqual(
[attempt[1] for attempt in bus.send_attempts],
[
bytes.fromhex("10 00 00 00 00 00 00 00"),
bytes.fromhex("40 00 00 00 00 00 00 00"),
],
)
def test_arbitrary_shade_missing_or_invalid_feedback_never_starts_motion(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 37}
)
executor = NativeCanExecutor(lambda: _FakeBus(), feedback_timeout_ms=1)
for before in (None, self.shade_feedback(255, motion=1)):
bus = _FakeBus()
with self.subTest(before=before):
with patch.object(executor, "_wait_for_id", return_value=before):
with self.assertRaisesRegex(
RuntimeError, "0x41A|新鲜|有效|fresh|valid"
):
executor._execute_shade_position(bus, command)
self.assertEqual(bus.send_attempts, [])
def test_door_feedback_enums_and_shade_stop_match_reference(self) -> None:
door_data = bytearray(8)
# 0x407: front-left unlock, front-right lock, rear-left lock,
# rear-right unlock; whole-vehicle logical state lock.
door_data[0] = 0b01101001
door_data[1] = 0b00001000
decoded = decode_feedback(CanFrame(0x407, bytes(door_data), channel=1))
locks = decoded["domain_state"]["door"]["locks"]
self.assertEqual(locks["front_left"]["state"], "unlock")
self.assertEqual(locks["front_right"]["state"], "lock")
self.assertEqual(locks["rear_left"]["state"], "lock")
self.assertEqual(locks["rear_right"]["state"], "unlock")
self.assertEqual(
decoded["domain_state"]["door"]["vehicle_lock"]["state"], "lock"
)
all_locked = bytearray(8)
all_locked[0] = 0xAA
all_locked[1] = 0x08
locked_frame = CanFrame(0x407, bytes(all_locked), channel=1)
self.assertTrue(feedback_matches("vehicle_lock:lock", locked_frame))
logical_only = bytearray(all_locked)
logical_only[0] = 0xA9
self.assertFalse(
feedback_matches(
"vehicle_lock:lock",
CanFrame(0x407, bytes(logical_only), channel=1),
)
)
all_unlocked = bytearray(8)
all_unlocked[0] = 0x55
all_unlocked[1] = 0x04
self.assertTrue(
feedback_matches(
"vehicle_lock:unlock",
CanFrame(0x407, bytes(all_unlocked), channel=1),
)
)
shade_data = bytearray(8)
shade_data[1] = 37
shade_data[3] = 1 << 3
shade = CanFrame(0x41A, bytes(shade_data), channel=1)
self.assertTrue(feedback_matches("shade_stopped", shade))
if __name__ == "__main__":
unittest.main()