优化前端,优化前端指令,优化自定义控制

This commit is contained in:
minghanchen
2026-08-24 09:40:16 +08:00
parent fff7657fea
commit 8d05815773
130 changed files with 5543 additions and 2206 deletions
+175 -36
View File
@@ -175,6 +175,108 @@ operations:
finally:
self.service.mode_profiles = original_registry
def test_mode_card_editor_endpoint_persists_generated_can_flow(self) -> None:
original_registry = self.service.mode_profiles
original_artifact_root = self.service.artifact_root
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
preset_root = base / "presets"
user_root = base / "user-modes"
preset = preset_root / "vehicles" / "denza_n7" / "office.yaml"
preset.parent.mkdir(parents=True)
preset.write_text(
"""
id: office
name: 办公模式
scope:
vehicle_ids: [denza_n7]
operations:
- domain: climate
capability: climate.set_temperature
target_scope: passenger
parameters:
temperature_c: 24
""".strip()
+ "\n",
encoding="utf-8",
)
request_id = "req-mode-editor-api-001"
instruction = "副驾空调温度设置为24度"
artifact_root = base / "runs"
request_dir = artifact_root / request_id
request_dir.mkdir(parents=True)
result = {
"request_id": request_id,
"utterance": instruction,
"execution_mode": "dry_run",
"control_transport": "wireless_mqtt",
"function_configs": {
"native_climate": {
"spec": {
"domain": "climate",
"operations": [
{
"id": "passenger_temperature",
"capability": "climate.set_temperature",
"target": {"zone": "passenger"},
"parameters": {"temperature_c": 24},
}
],
}
}
},
"command_plans": {
"native_climate": {
"spec": {
"commands": [
{
"message": "Native_0x1DE",
"signals": {
"frame_id": "0x1DE",
"data_hex": "00 00 00 00 00 18 00 00",
},
}
]
}
}
},
"execution_report": {"spec": {"results": []}},
}
(request_dir / "result.json").write_text(
json.dumps(result, ensure_ascii=False), encoding="utf-8"
)
self.service.mode_profiles = ModeProfileRegistry(
preset_root, user_root=user_root
)
self.service.artifact_root = artifact_root
try:
request = urllib.request.Request(
f"{self.base_url}/api/mode-profiles/office",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"name": "专注办公模式",
"instruction": instruction,
"source_request_id": request_id,
"vehicle_id": "denza_n7",
},
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(request, timeout=3) as response:
payload = json.loads(response.read().decode("utf-8"))
self.assertEqual(response.status, 200)
self.assertEqual(payload["storage"], "persistent_yaml")
self.assertEqual(payload["mode_profile"]["origin"], "customized")
self.assertEqual(payload["mode_profile"]["instruction"], instruction)
self.assertTrue(
(user_root / "vehicles" / "denza_n7" / "user" / "office.yaml").is_file()
)
finally:
self.service.mode_profiles = original_registry
self.service.artifact_root = original_artifact_root
def test_head_unit_display_sync_tracks_tasks_and_usb_catalog(self) -> None:
task_request = urllib.request.Request(
f"{self.base_url}/api/requests",
@@ -230,18 +332,18 @@ operations:
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=dual-seat-90", timeout=3) as response:
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-101", 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-8", timeout=3
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
) as response:
gesture_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.js?v=sync-layout-27", timeout=3
f"{self.base_url}/console-v2.js?v=sync-layout-41", timeout=3
) as response:
console_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.css?v=sync-layout-27", timeout=3
f"{self.base_url}/console-v2.css?v=sync-layout-40", timeout=3
) as response:
console_styles = response.read().decode("utf-8")
with self.opener.open(
@@ -263,7 +365,7 @@ operations:
with self.opener.open(f"{self.base_url}/api/runtime", timeout=3) as response:
runtime = json.loads(response.read().decode("utf-8"))
self.assertIn("Cognitive Agent", html)
self.assertIn("511工程-智能座舱", html)
self.assertIn('id="seatTwinCanvas"', html)
self.assertIn('data-vehicle-model="byd-tang-2024-reference"', html)
self.assertIn("整车孪生界面", html)
@@ -276,12 +378,15 @@ operations:
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=dual-seat-90"', html)
self.assertIn('href="/styles.css?v=dual-seat-10"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-8"', html)
self.assertIn('src="/app.js?v=dual-seat-101"', html)
self.assertIn('href="/styles.css?v=dual-seat-16"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
self.assertNotIn('class="vehicle-system-hud"', html)
self.assertIn('src="/console-v2.js?v=sync-layout-27"', html)
self.assertIn('href="/console-v2.css?v=sync-layout-27"', html)
self.assertIn('src="/console-v2.js?v=sync-layout-41"', html)
self.assertIn('href="/console-v2.css?v=sync-layout-40"', html)
self.assertIn('<body class="console-v2">', html)
self.assertIn('id="v2BootScreen"', html)
self.assertIn('正在加载车机控制与实时状态', html)
self.assertIn('src="/self-extension.js?v=sync-layout-26"', html)
self.assertIn('href="/self-extension.css?v=sync-layout-26"', html)
self.assertNotIn("self-calibration", html)
@@ -291,7 +396,7 @@ operations:
self.assertIn('aria-label="执行车舱控制指令"', html)
self.assertNotIn('"admin"', console_script)
self.assertNotIn("v2RoleSelect", console_script)
self.assertIn('id="v2SidebarToggle"', console_script)
self.assertIn('sidebarToggle.id = "v2SidebarToggle"', console_script)
self.assertIn('id = "v2ModuleSwitcher"', console_script)
self.assertIn(
'panelHeading.classList.add("v2-operation-heading", "v2-module-heading")',
@@ -316,28 +421,33 @@ operations:
)
self.assertIn('511工程-智能座舱', console_script)
self.assertIn('网页控制链路', console_script)
self.assertIn('<span>Hi,</span><strong>user</strong>', console_script)
self.assertIn('["self-orchestration", "self-extension"]', console_script)
self.assertNotIn('label: "自诊断"', console_script)
self.assertIn('<small>手势模型</small>', console_script)
self.assertIn('id="v2GestureModelValue">MediaPipe GR</strong>', console_script)
self.assertIn('id="v2GestureModelNote">Tasks Vision · 0.10.14</small>', console_script)
self.assertIn('<span class="v2-field-caption">配置信息</span>', console_script)
self.assertIn('state.id = "v2VehicleState"', console_script)
self.assertIn('fetch("/api/extension/health"', console_script)
self.assertNotIn('runtimeControls.id = "v2RuntimeControls"', console_script)
self.assertIn('function prepareRuntimeControls(originalForm)', console_script)
self.assertIn('executionField.hidden = false', console_script)
self.assertIn('insertVisibleControl(executionField, "v2-sync-execution-field")', console_script)
self.assertIn('insertVisibleControl(simulationFaultField, "v2-sync-simulation-fault-field")', console_script)
self.assertNotIn('class="v2-profile"', console_script)
self.assertNotIn('class="v2-control-switch"', console_script)
self.assertNotIn('class="v2-config-stack"', console_script)
self.assertNotIn('id="v2GestureModelValue"', console_script)
self.assertNotIn('id="v2VehicleState"', console_script)
self.assertIn('actionRail.append(cameraToggle)', console_script)
self.assertIn("function enhanceConfiguredDropdowns()", console_script)
self.assertNotIn("function enhanceConfiguredDropdowns()", console_script)
self.assertIn("function updateModeEdgeHints()", console_script)
self.assertIn('classList.toggle("v2-twin-active", definition.index === 0)', console_script)
self.assertIn('label: "执行结果与硬件回读"', console_script)
self.assertIn("function requestUserSafeExecutionMode()", console_script)
self.assertIn('const dryRunButton = $(\'[data-execution-mode="dry_run"]\')', console_script)
self.assertIn('if (!dryRunButton.classList.contains("active")) dryRunButton.click()', console_script)
self.assertIn('userTargetText.textContent = "执行目标:虚拟 CAN"', console_script)
self.assertIn("user 固定使用虚拟 CAN 安全执行环境", console_script)
self.assertNotIn("user 演示固定使用真实台架", console_script)
self.assertIn("function keepRunButtonAvailable()", console_script)
self.assertIn('window.setTimeout(() => $("#v2BootScreen")?.remove(), 220)', console_script)
self.assertNotIn("function requestUserSafeExecutionMode()", console_script)
self.assertNotIn('dryRunButton.click()', console_script)
self.assertIn('const DEFAULT_EXECUTION_MODE = "hardware"', app_script)
self.assertIn('preferredExecutionMode: DEFAULT_EXECUTION_MODE', app_script)
self.assertIn('selectExecutionMode(modeAvailable ? preferredMode : "dry_run", { remember: false })', app_script)
self.assertIn('class="mode-option active" id="hardwareMode"', html)
self.assertIn('grid-template-columns: clamp(245px, 18.5vw, 310px)', console_styles)
self.assertIn('.v2-control-switch {', console_styles)
self.assertNotIn('.v2-control-switch {', console_styles)
self.assertIn('.v2-module-switcher {', console_styles)
self.assertIn('grid-template-columns: repeat(2, minmax(0, 1fr))', console_styles)
self.assertIn(
@@ -364,7 +474,7 @@ operations:
self.assertIn('width: clamp(48px, 45%, 72px)', console_styles)
self.assertIn('font-weight: 800', console_styles)
self.assertIn('white-space: nowrap', console_styles)
self.assertIn('.v2-gesture-model-field', console_styles)
self.assertNotIn('.v2-gesture-model-field', console_styles)
self.assertIn('.v2-inline-gesture .v2-camera-toggle', console_styles)
self.assertIn('.v2-inline-gesture .v2-gesture-action-rail', console_styles)
self.assertIn('grid-template-rows: 42px 10px 42px', console_styles)
@@ -373,9 +483,8 @@ operations:
self.assertIn('.v2-inline-gesture #gestureStopButton', console_styles)
self.assertIn('.v2-inline-gesture .v2-gesture-result-stack', console_styles)
self.assertIn('.v2-inline-gesture .v2-gesture-execution-slot', console_styles)
self.assertIn('grid-template-columns: 1fr;', console_styles)
self.assertIn('.v2-page-rail', console_styles)
self.assertIn('.v2-dropdown-menu', console_styles)
self.assertNotIn('.v2-dropdown-menu', console_styles)
self.assertIn('.v2-edge-fade-left', console_styles)
self.assertIn('.v2-edge-fade-right', console_styles)
self.assertIn('.mode-group-divider', console_styles)
@@ -385,8 +494,8 @@ operations:
self.assertIn('.joint-seat-panel .thermal-states', console_styles)
self.assertIn('.joint-twin-shell .domain-readouts', console_styles)
self.assertIn('grid-template-columns: repeat(2, minmax(0, 1fr))', console_styles)
self.assertIn('.v2-config-section > .v2-field-caption', console_styles)
self.assertIn('.console-v2 .v2-config-card {', console_styles)
self.assertNotIn('.v2-config-section', console_styles)
self.assertNotIn('.console-v2 .v2-config-card {', console_styles)
self.assertIn('.console-v2.v2-sidebar-collapsed .workspace', console_styles)
self.assertNotIn('id="hardwareAccessField"', html)
self.assertNotIn('id="hardwareClientLabel"', html)
@@ -406,8 +515,14 @@ operations:
self.assertIn('data-control-transport="wireless_mqtt"', html)
self.assertIn('control_transport: state.controlTransport', app_script)
self.assertIn('const transportField = $("#vehicleTransportField", originalForm)', console_script)
self.assertIn('configStack.append(transportField)', console_script)
self.assertNotIn('runtimeControls.append(transportField)', console_script)
self.assertIn('insertVisibleControl(transportField, "v2-sync-transport-field")', console_script)
self.assertIn('.v2-sync-execution-field .execution-mode-options', console_styles)
self.assertIn('grid-template-columns: repeat(3, minmax(0, 1fr))', console_styles)
self.assertNotIn('class="v2-sync-protocol"', console_script)
self.assertIn('.console-v2 .v2-transport-field .vehicle-transport-options', console_styles)
self.assertIn('.v2-sync-transport-field .mode-option', console_styles)
self.assertIn('overflow-wrap: anywhere', console_styles)
self.assertIn('button.disabled = false', app_script)
self.assertIn('button.setAttribute("aria-pressed", String(selected))', app_script)
self.assertNotIn(
@@ -497,7 +612,7 @@ operations:
self.assertIn('client_type: "web_console"', app_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v179"', service_worker)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v202"', service_worker)
self.assertIn('"/assets/pets/self-extension-cat.png?v=3"', service_worker)
self.assertIn(".then(() => self.skipWaiting())", service_worker)
self.assertNotIn("const cached = await caches.match(request)", service_worker)
@@ -544,6 +659,13 @@ operations:
self.assertIn('id="modeGallery"', html)
self.assertIn('id="modeGalleryGroups"', html)
self.assertIn("全部功能模式", html)
self.assertIn('id="modeEditorBackdrop"', html)
self.assertIn('id="modeEditorName"', html)
self.assertIn('id="modeEditorInstruction"', html)
self.assertIn('id="modeEditorExecute"', html)
self.assertIn('id="modeEditorComplete"', html)
self.assertIn('id="modeEditorFlowPreview"', html)
self.assertIn('class="mode-editor-flow-preview empty"', html)
self.assertIn('id="saveModeSource"', html)
self.assertNotIn("主驾 24℃", html)
populate_modes = app_script[
@@ -568,6 +690,17 @@ operations:
self.assertIn('deleteUserModeProfile(deleteButton.dataset.modeDelete)', app_script)
self.assertIn('method: "DELETE"', app_script)
self.assertIn('deleteButton.dataset.modeDelete = mode.id', app_script)
self.assertIn('editButton.dataset.modeEdit = mode.id', app_script)
self.assertIn("function openModeEditor", app_script)
self.assertIn("function executeModeEditorInstruction", app_script)
self.assertIn("function completeModeEditor", app_script)
self.assertIn("function modeEditorReadableCommands", app_script)
self.assertIn("function modeEditorMessagesForOperation", app_script)
self.assertIn("function modeEditorAppendCommandCard", app_script)
self.assertIn('title.textContent = flow?.control_transport === "wireless_mqtt" ? "MQTT 消息序列"', app_script)
self.assertIn("message.payload", app_script)
self.assertIn("source_request_id: editor.sourceRequestId", app_script)
self.assertIn('method: "POST"', app_script)
self.assertIn('data-domain-panel="climate"', html)
self.assertIn('data-domain-panel="roof"', html)
self.assertIn('data-domain-panel="ambient_light"', html)
@@ -624,7 +757,10 @@ operations:
self.assertNotIn('id="transmittanceValue"', html)
self.assertNotIn('id="roofMotorState"', html)
self.assertIn('id="ambientBrightness"', html)
self.assertIn("坐垫脚托夹角133度", html)
self.assertNotIn('class="example-group"', html)
self.assertNotIn('id="safetyStrip"', html)
self.assertNotIn('class="pipeline"', html)
self.assertNotIn('class="topbar"', html)
self.assertNotIn('data-axis="height"', html)
self.assertEqual(runtime["domain"], "cabin")
self.assertEqual(
@@ -632,6 +768,7 @@ operations:
(
runtime["hardware"]["configured"]
or runtime["vehicle_can"]["configured"]
or runtime["vehicle_mqtt"]["configured"]
),
)
self.assertEqual(runtime["hardware"]["supported_targets"], ["front_right"])
@@ -686,11 +823,13 @@ operations:
for item in runtime["capabilities"]
if item["name"].startswith("seat.")
and not item["name"].startswith("seat.set_driver_")
and item["name"] != "seat.recall_driver_memory_native"
]
driver_seat_capabilities = [
item
for item in runtime["capabilities"]
if item["name"].startswith("seat.set_driver_")
or item["name"] == "seat.recall_driver_memory_native"
]
self.assertTrue(passenger_seat_capabilities)
self.assertTrue(
@@ -713,7 +852,7 @@ operations:
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/app.js?v=dual-seat-90", timeout=3
f"{self.base_url}/app.js?v=dual-seat-101", timeout=3
) as response:
app_script = response.read().decode("utf-8")
@@ -1152,7 +1291,7 @@ operations:
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"utterance": "打开副驾座椅通风",
"utterance": "打开副驾座椅通风,同时播放音乐",
"provider": "openai",
"conversation_id": "conv-e2e-gpt-fallback",
"execution_mode": "dry_run",
+2 -6
View File
@@ -1,11 +1,11 @@
{
"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.",
"description": "Offline-only fail-closed contract for unsupported whole-vehicle CH1 door-lock commands. Rear-door 0x32B candidates remain contract-only in the inherited catalog and are not live cases in this 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.",
"retirement_reason": "Whole-vehicle 0x496 verification failed; the distinct rear-only 0x32B command remains an offline contract candidate until current-vehicle HIL evidence is recorded.",
"live_execution_permitted_by_fixture_alone": false
},
"modules": {
@@ -21,8 +21,6 @@
"REJECT-RAW-CAN",
"REJECT-VEHICLE-LOCK",
"REJECT-VEHICLE-UNLOCK",
"REJECT-REAR-LOCK",
"REJECT-REAR-UNLOCK",
"REJECT-WALL-LIGHT",
"REJECT-READING-ZONE",
"REJECT-AMBIENT-SCENE",
@@ -41,8 +39,6 @@
"REJECT-RAW-CAN",
"REJECT-VEHICLE-LOCK",
"REJECT-VEHICLE-UNLOCK",
"REJECT-REAR-LOCK",
"REJECT-REAR-UNLOCK",
"REJECT-WALL-LIGHT",
"REJECT-READING-ZONE",
"REJECT-AMBIENT-SCENE",
+10 -4
View File
@@ -58,6 +58,7 @@
"case_ids": [
"CLIMATE-POWER-ON", "CLIMATE-POWER-OFF", "CLIMATE-FAN-3",
"CLIMATE-MODE-DEFROST", "CLIMATE-TEMP-DRIVER-24", "CLIMATE-CONTROL-AUTO",
"DOOR-REAR-LOCK", "DOOR-REAR-UNLOCK",
"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",
@@ -75,7 +76,6 @@
"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"
]
},
@@ -84,6 +84,7 @@
"case_ids": [
"CLIMATE-POWER-ON", "CLIMATE-POWER-OFF", "CLIMATE-FAN-3",
"CLIMATE-MODE-DEFROST", "CLIMATE-TEMP-DRIVER-24", "CLIMATE-CONTROL-AUTO",
"DOOR-REAR-LOCK", "DOOR-REAR-UNLOCK",
"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",
@@ -101,7 +102,6 @@
"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"
]
}
@@ -131,6 +131,14 @@
"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": "DOOR-REAR-LOCK", "command": "door rear lock", "hil": false,
"expected": {"config_key": "native_door", "domain": "door", "capability": "door.set_rear_lock", "target": "rear", "frame_ids": ["0x32B"], "feedback_ids": ["0x407"], "state_assertions": [{"path": "domain_state.door.locks.rear_left.state", "equals": "lock"}, {"path": "domain_state.door.locks.rear_right.state", "equals": "lock"}]}
},
{
"id": "DOOR-REAR-UNLOCK", "command": "door rear unlock", "hil": false,
"expected": {"config_key": "native_door", "domain": "door", "capability": "door.set_rear_lock", "target": "rear", "frame_ids": ["0x32B"], "feedback_ids": ["0x407"], "state_assertions": [{"path": "domain_state.door.locks.rear_left.state", "equals": "unlock"}, {"path": "domain_state.door.locks.rear_right.state", "equals": "unlock"}]}
},
{
"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}]}
@@ -243,8 +251,6 @@
{"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"},
@@ -231,9 +231,9 @@ class Ch1VehicleHilContractTests(unittest.TestCase):
orchestration = suite_cases(self.suite, "self_orchestration")
extension = suite_cases(self.suite, "self_extension")
self.assertEqual([case["id"] for case in orchestration], [case["id"] for case in extension])
self.assertEqual(len(orchestration), 43)
self.assertEqual(len(orchestration), 45)
self.assertEqual(
len(suite_rejections(self.suite, "self_orchestration")), 14
len(suite_rejections(self.suite, "self_orchestration")), 12
)
self.assertEqual(sum(case.get("hil", True) for case in orchestration), 30)
self.assertEqual(len(self.suite["cleanup"]), 9)
@@ -271,8 +271,6 @@ class Ch1VehicleHilContractTests(unittest.TestCase):
retired = {
"REJECT-VEHICLE-LOCK": "door vehicle lock",
"REJECT-VEHICLE-UNLOCK": "door vehicle unlock",
"REJECT-REAR-LOCK": "door rear lock",
"REJECT-REAR-UNLOCK": "door rear unlock",
}
positive_commands = {case["command"] for case in self.suite["case_catalog"]}
rejection_by_id = {
@@ -400,8 +398,6 @@ class Ch1VehicleHilContractTests(unittest.TestCase):
"REJECT-RAW-CAN",
"REJECT-VEHICLE-LOCK",
"REJECT-VEHICLE-UNLOCK",
"REJECT-REAR-LOCK",
"REJECT-REAR-UNLOCK",
"REJECT-WALL-LIGHT",
"REJECT-READING-ZONE",
"REJECT-AMBIENT-SCENE",
@@ -657,9 +653,9 @@ class Ch1VehicleHilContractTests(unittest.TestCase):
hardware_access_bypass=True,
)
interpret.assert_called_once()
interpret.assert_not_called()
self.assertEqual(vehicle_service.process.call_args.args[1], "openai")
self.assertEqual(response["provider_used"], "openai:gpt-5.6-sol")
self.assertEqual(response["provider_used"], "local_rules")
self.assertTrue(response["success"])
self.assertEqual(len(response["operation_results"]), 1)
self.assertEqual(len(captured), 1)
@@ -742,7 +738,9 @@ class Ch1VehicleHilContractTests(unittest.TestCase):
interpret.assert_called_once()
self.assertEqual(vehicle_service.process.call_args.args[1], "openai")
self.assertEqual(response["provider_used"], "openai:gpt-5.6-sol")
self.assertEqual(
response["provider_used"], "local_rules+openai:gpt-5.6-sol"
)
self.assertTrue(response["success"])
self.assertEqual(len(response["operation_results"]), 2)
self.assertEqual(len(captured), 1)
@@ -758,8 +756,6 @@ class Ch1VehicleHilContractTests(unittest.TestCase):
retired_door_ids = {
"REJECT-VEHICLE-LOCK",
"REJECT-VEHICLE-UNLOCK",
"REJECT-REAR-LOCK",
"REJECT-REAR-UNLOCK",
}
for index, case in enumerate(
suite_rejections(self.suite, "self_orchestration")
@@ -1081,7 +1077,7 @@ class Ch1VehicleHilContractTests(unittest.TestCase):
report["summary"]["offline_rejection_cases"],
2 * len(fixture["rejection_catalog"]),
)
self.assertEqual(len(fixture["rejection_catalog"]), 14)
self.assertEqual(len(fixture["rejection_catalog"]), 12)
self.assertEqual(
[(path, method) for path, method, _ in api.calls],
[("/api/runtime", "GET")],
@@ -510,6 +510,88 @@ class MultiDomainServiceTests(unittest.TestCase):
[],
)
def test_mode_card_editor_persists_only_server_generated_command_flow(self) -> None:
mode_root = Path(self.temporary_directory.name) / "mode-card-presets"
user_root = Path(self.temporary_directory.name) / "mode-card-user"
preset = mode_root / "vehicles" / "denza_n7" / "office.yaml"
preset.parent.mkdir(parents=True)
preset.write_text(
"""
id: office
name: 办公模式
icon: briefcase
scope:
vehicle_ids: [denza_n7]
operations:
- domain: climate
capability: climate.set_temperature
target_scope: passenger
parameters:
temperature_c: 24
""".strip()
+ "\n",
encoding="utf-8",
)
self.service.mode_profiles = ModeProfileRegistry(
mode_root, user_root=user_root
)
model_output = {
"detected_domains": ["climate"],
"requests": [
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "passenger",
"operation": "set",
"parameters": [{"name": "temperature_c", "value": 24}],
"reason": "办公温度。",
}
],
"ambiguities": [],
"decision_summary": ["副驾温度设置为24度。"],
"confidence": 0.97,
}
instruction = "副驾空调温度设置为24度"
with patch.object(self.service.openai, "interpret", return_value=model_output):
generated = self.service.process(
instruction,
"openai",
"conv-mode-card-editor",
vehicle_id="denza_n7",
)
saved = self.service.save_mode_profile_card(
"office",
"专注办公模式",
instruction,
generated["request_id"],
"denza_n7",
)
self.assertEqual(saved["status"], "succeeded")
self.assertEqual(saved["storage"], "persistent_yaml")
self.assertEqual(saved["mode_profile"]["origin"], "customized")
self.assertEqual(saved["mode_profile"]["instruction"], instruction)
self.assertTrue(saved["mode_profile"]["command_flow"]["plans"])
self.assertTrue(
saved["mode_profile"]["command_flow"]["function_configs"]
)
reloaded = ModeProfileRegistry(mode_root, user_root=user_root)
persisted = next(
item
for item in reloaded.public_modes("denza_n7")
if item["id"] == "office"
)
self.assertEqual(persisted["name"], "专注办公模式")
with self.assertRaisesRegex(ValueError, "重新点击执行"):
self.service.save_mode_profile_card(
"office",
"专注办公模式",
"副驾空调温度设置为25度",
generated["request_id"],
"denza_n7",
)
def test_build_named_mode_executes_and_persists_in_one_turn(self) -> None:
mode_root = Path(self.temporary_directory.name) / "modes"
self.service.mode_profiles = ModeProfileRegistry(mode_root)
@@ -46,7 +46,12 @@ class SeatFrontendInputCoverageTests(unittest.TestCase):
cls.temporary_directory = tempfile.TemporaryDirectory()
with patch.dict(
os.environ,
{"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false"},
{
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_MODE_PROFILE_ROOT": str(
Path(cls.temporary_directory.name) / "mode-profiles"
),
},
clear=False,
):
cls.service = SeatDemoService(REPOSITORY_ROOT)
+3 -3
View File
@@ -266,7 +266,7 @@ class Ch1DefaultTargetTests(unittest.TestCase):
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:
def test_local_rules_skip_gpt_and_apply_safe_ch1_defaults(self) -> None:
cases = (
(
"车窗开启一半",
@@ -336,8 +336,8 @@ class Ch1DefaultTargetTests(unittest.TestCase):
conversation_id=f"conv-ch1-default-openai-{index}",
)
interpret.assert_called_once()
self.assertEqual(result["provider_used"], "openai:gpt-5.6-sol")
interpret.assert_not_called()
self.assertEqual(result["provider_used"], "local_rules")
self.assertEqual(result["status"], "succeeded")
self.assertEqual(set(result["function_configs"]), {config_key})
self.assertNotIn("seat", result["function_configs"])
+154
View File
@@ -1,6 +1,8 @@
from __future__ import annotations
import json
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
@@ -109,6 +111,8 @@ class DriverSeatNativeCanTests(unittest.TestCase):
"seat.set_driver_heating_native",
"seat.set_driver_massage_native",
"seat.set_driver_axis_native",
"seat.set_driver_position_native",
"seat.recall_driver_memory_native",
},
)
for capability in capabilities.values():
@@ -151,6 +155,16 @@ class DriverSeatNativeCanTests(unittest.TestCase):
{"axis": "height", "direction": "stop", "duration_ms": 500},
"driver",
),
"seat driver position 3": (
"seat.set_driver_position_native",
{"slot": 3},
"driver",
),
"seat driver memory recall 2": (
"seat.recall_driver_memory_native",
{"slot": 2},
"driver",
),
}
for utterance, expected in cases.items():
with self.subTest(utterance=utterance):
@@ -196,6 +210,10 @@ class DriverSeatNativeCanTests(unittest.TestCase):
"seat.set_driver_axis_native",
{"axis": "height", "direction": "up", "duration_ms": 500},
),
"主驾座椅调整到记忆位置3": (
"seat.recall_driver_memory_native",
{"slot": 3},
),
}
for utterance, expected in cases.items():
with self.subTest(utterance=utterance):
@@ -356,6 +374,142 @@ class DriverSeatNativeCanTests(unittest.TestCase):
self.assertEqual(command["repeat"], {"count": 3, "period_ms": 100})
self.assertNotIn("special_handler", command["signals"])
def test_memory_aliases_compile_the_same_learned_twenty_frame_sequence(self) -> None:
commands = []
for capability in (
"seat.set_driver_position_native",
"seat.recall_driver_memory_native",
):
with self.subTest(capability=capability):
_, plan = self._compile(capability, {"slot": 3})
command = plan["spec"]["commands"][0]
commands.append(command)
self.assertEqual(command["signals"]["frame_id"], "0x4C1")
self.assertEqual(
command["signals"]["special_handler"],
"driver_seat_memory_recall",
)
special = json.loads(command["signals"]["special_parameters"])
self.assertEqual(len(special["frame_sequence"]), 20)
self.assertEqual(
special["frame_sequence"][0::2],
["16 9A 66 3C 76 04 06 00"] * 10,
)
self.assertEqual(
special["frame_sequence"][1::2],
["15 02 C6 3E 2A C8 FF FF"] * 10,
)
self.assertEqual(special["period_ms"], 50)
public = self.runtime.command_frame_public(command)
self.assertEqual(public["sequence"]["frame_count"], 20)
self.assertTrue(
public["sequence"]["requires_fresh_initialized_0x494"]
)
self.assertEqual(
commands[0]["signals"]["special_parameters"],
commands[1]["signals"]["special_parameters"],
)
def test_memory_executor_requires_initialized_status_and_alternates_frames(self) -> None:
config, plan = self._compile(
"seat.recall_driver_memory_native", {"slot": 2}
)
initialized = CanFrame(
0x494,
bytes((0, 0, 0, 0, 0, 0, 0, 0x3C)),
channel=1,
is_fd=True,
bitrate_switch=True,
)
bus = _FakeBus([initialized, initialized])
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
report = NativeCanExecutor(
lambda: bus, feedback_timeout_ms=1, feedback_required=False
).execute(self.runtime, plan, config)
self.assertEqual(report["spec"]["status"], "succeeded")
self.assertEqual(len(bus.sent), 20)
self.assertTrue(all(frame.is_fd and frame.bitrate_switch for frame in bus.sent))
self.assertEqual(
[frame.data[0] for frame in bus.sent], [0x16, 0x15] * 10
)
self.assertEqual(
[frame.data for frame in bus.sent[0::2]],
[bytes.fromhex("16 9A 66 3C 76 04 06 00")] * 10,
)
self.assertEqual(
[frame.data for frame in bus.sent[1::2]],
[bytes.fromhex("15 02 A6 AE 00 C8 FF FF")] * 10,
)
telemetry = report["spec"]["results"][0]["telemetry"]
self.assertFalse(telemetry["physical_effect_confirmed"])
self.assertEqual(len(telemetry["sent_frames"]), 20)
uninitialized_bus = _FakeBus(
[CanFrame(0x494, bytes(8), channel=1, is_fd=True)]
)
rejected = NativeCanExecutor(
lambda: uninitialized_bus,
feedback_timeout_ms=1,
feedback_required=False,
).execute(self.runtime, plan, config)
self.assertEqual(rejected["spec"]["status"], "failed")
self.assertFalse(uninitialized_bus.sent)
def test_memory_sequence_stops_immediately_on_send_failure(self) -> None:
config, plan = self._compile(
"seat.set_driver_position_native", {"slot": 1}
)
initialized = CanFrame(
0x494, bytes((0, 0, 0, 0, 0, 0, 0, 0x3C)), channel=1
)
bus = _FakeBus([initialized], fail_on_calls={4})
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
report = NativeCanExecutor(
lambda: bus, feedback_timeout_ms=1, feedback_required=False
).execute(self.runtime, plan, config)
self.assertEqual(report["spec"]["status"], "failed")
self.assertEqual(bus.send_calls, 4)
self.assertEqual(len(bus.sent), 3)
def test_memory_compile_rejects_missing_or_malformed_learned_slots(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "driver_memory.json"
path.write_text(
json.dumps(
{
"version": 1,
"positions": {
"1": {
"payload_15": "15 00",
"payload_16": "16 00 00 00 00 00 00 00",
}
},
}
),
encoding="utf-8",
)
with patch.dict(
os.environ,
{"COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH": str(path)},
):
config = self.runtime.build_function_config(
"seat",
[_native_action("seat.recall_driver_memory_native", {"slot": 1})],
"req-bad-memory",
"local_rules",
)
with self.assertRaisesRegex(NativeCanCompileError, "8-byte"):
self.runtime.compile(config)
missing = self.runtime.build_function_config(
"seat",
[_native_action("seat.recall_driver_memory_native", {"slot": 2})],
"req-missing-memory",
"local_rules",
)
with self.assertRaisesRegex(NativeCanCompileError, "has not been learned"):
self.runtime.compile(missing)
def test_compiler_fails_closed_for_wrong_seat_or_invalid_parameters(self) -> None:
for target in ("passenger", "front_right"):
action = _native_action(
@@ -128,18 +128,18 @@ class FrontendVehicleStateSyncContractTests(unittest.TestCase):
):
self.assertIn(f'{state_path} === "physical_can_ch1"', summary_source)
def test_orchestration_renders_only_the_physical_ch1_patch(self) -> None:
def test_orchestration_renders_only_the_authoritative_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)",
"const actuatorPatch = authoritativeCh1StatePatch(payload.actuator_state)",
apply_source,
)
self.assertIn(
"const domainPatch = physicalCh1StatePatch(payload.domain_state)",
"const domainPatch = authoritativeCh1StatePatch(payload.domain_state)",
apply_source,
)
self.assertIn("renderSeatState(actuatorPatch)", apply_source)
@@ -163,13 +163,18 @@ class FrontendVehicleStateSyncContractTests(unittest.TestCase):
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:
def test_authoritative_patch_accepts_wired_and_wireless_without_promoting_siblings(self) -> None:
node = shutil.which("node")
if node is None:
self.skipTest("Node.js is required to execute the frontend patch helper")
source_helper = function_source(
self.app_source,
"function isAuthoritativeCh1Source(value) {",
"\nfunction renderCabinLiveStatus",
)
patch_source = function_source(
self.app_source,
"function physicalCh1StatePatch(value) {",
"function authoritativeCh1StatePatch(value) {",
"\nfunction isAuthoritativeVehicleStatePayload",
)
state = {
@@ -178,14 +183,20 @@ class FrontendVehicleStateSyncContractTests(unittest.TestCase):
"power": "on",
},
"roof": {"shade_position_pct": 100},
"ambient_light": {
"feedback_source": "wireless_mqtt_ch1",
"power": "on",
},
"window": {
"positions": {"front_left": {"position_pct": 50}}
},
}
script = (
'const AUTHORITATIVE_CH1_SOURCES = new Set(["physical_can_ch1", "wireless_mqtt_ch1"]);\n'
f"{source_helper}\n"
f"{patch_source}\n"
f"const input = {json.dumps(state)};\n"
"process.stdout.write(JSON.stringify(physicalCh1StatePatch(input)));"
"process.stdout.write(JSON.stringify(authoritativeCh1StatePatch(input)));"
)
completed = subprocess.run(
[node, "-e", script],
@@ -195,7 +206,10 @@ class FrontendVehicleStateSyncContractTests(unittest.TestCase):
)
self.assertEqual(
json.loads(completed.stdout),
{"climate": state["climate"]},
{
"climate": state["climate"],
"ambient_light": state["ambient_light"],
},
)
def test_dual_channel_runtime_polls_each_seat_without_aliasing(self) -> None:
@@ -249,6 +263,24 @@ class FrontendVehicleStateSyncContractTests(unittest.TestCase):
self.assertIn("}, physicalStatePollIntervalMs());", self.app_source)
self.assertIn("restartPhysicalStatePolling();", self.app_source)
def test_transport_switch_clears_stale_vehicle_values_before_new_feedback(self) -> None:
select_source = function_source(
self.app_source,
"function selectControlTransport(transport) {",
"\nfunction renderHardwareAccess",
)
reset_source = function_source(
self.app_source,
"function resetVehicleStatusDisplay() {",
"\nfunction mergeSeatActuatorState",
)
self.assertIn("resetVehicleStatusDisplay();", select_source)
self.assertIn('setText("#windowState", "UNKNOWN")', reset_source)
self.assertIn('setText("#frontLightState", "UNKNOWN")', reset_source)
self.assertIn('setText("#cabinSummaryDoors", "状态未知")', reset_source)
self.assertIn('meter.dataset.state = "unknown"', reset_source)
def test_vehicle_selection_cannot_replay_unfiltered_state_in_ch1_mode(self) -> None:
select_source = function_source(
self.app_source,
+79
View File
@@ -356,6 +356,85 @@ operations:
[{"name": "transmittance_pct", "value": 20}],
)
def test_mode_card_override_persists_instruction_and_can_command_flow(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "presets"
user_root = Path(directory) / "persistent-user-modes"
preset = root / "vehicles" / "denza_n7" / "office.yaml"
preset.parent.mkdir(parents=True)
preset.write_text(
"""
id: office
name: 办公模式
icon: briefcase
scope:
vehicle_ids: [denza_n7]
operations:
- domain: climate
capability: climate.set_temperature
target_scope: passenger
parameters:
temperature_c: 24
""".strip()
+ "\n",
encoding="utf-8",
)
registry = ModeProfileRegistry(root, user_root=user_root)
flow = {
"schema": "cognitive-agent-mode-command-flow/v1",
"source_request_id": "req-mode-editor-001",
"plans": {
"native_climate": {
"spec": {
"commands": [
{
"message": "Native_0x1DE",
"signals": {
"frame_id": "0x1DE",
"data_hex": "00 00 00 00 18 00 00 00",
},
}
]
}
}
},
}
registry.save_mode(
"专注办公模式",
[
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "passenger",
"parameters": {"temperature_c": 24},
}
],
mode_id="office",
vehicle_id="denza_n7",
icon="briefcase",
instruction="副驾空调设置为24度",
command_flow=flow,
)
reloaded = ModeProfileRegistry(root, user_root=user_root)
mode = next(
item
for item in reloaded.public_modes("denza_n7")
if item["id"] == "office"
)
self.assertEqual(mode["name"], "专注办公模式")
self.assertEqual(mode["origin"], "customized")
self.assertTrue(mode["preset_override"])
self.assertEqual(mode["instruction"], "副驾空调设置为24度")
self.assertEqual(
mode["command_flow"]["source_request_id"],
"req-mode-editor-001",
)
self.assertTrue(
(user_root / "vehicles" / "denza_n7" / "user" / "office.yaml").is_file()
)
def test_session_overlay_ignores_stale_repository_user_modes(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "profiles"
+335 -32
View File
@@ -1,11 +1,15 @@
from __future__ import annotations
import json
import tempfile
import time
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from cognitive_agent.application.native_can_runtime import NativeCanRuntime
from cognitive_agent.application.seat_service import SeatDemoService
from cognitive_agent.infrastructure.can.gateway import CanFrame
from cognitive_agent.infrastructure.can.hardware import (
HardwareSafetyError,
@@ -18,6 +22,9 @@ from cognitive_agent.infrastructure.can.mqtt_transport import (
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
class _PublishResult:
rc = 0
@@ -72,7 +79,7 @@ def _settings():
"host": "127.0.0.1",
"port": 1883,
"condition_topic": "n7/n7-headunit/can/state",
"control_topic": "n7/can_control",
"control_topic": "n7/n7-headunit/can/command",
"qos": 1,
"keepalive_s": 30,
"username": None,
@@ -86,12 +93,32 @@ def _settings():
"state_configured": True,
"backend": "mqtt",
"condition_topic": "n7/n7-headunit/can/state",
"control_topic": "n7/can_control",
"control_topic": "n7/n7-headunit/can/command",
}
return SimpleNamespace(**values)
class MqttCanPayloadTests(unittest.TestCase):
def test_default_topics_and_public_contracts_match_headunit(self) -> None:
with patch.dict("os.environ", {}, clear=True):
settings = VehicleMqttSettings.from_env()
info = settings.public_info()
self.assertEqual(settings.condition_topic, "n7/n7-headunit/can/state")
self.assertEqual(settings.control_topic, "n7/n7-headunit/can/command")
self.assertFalse(settings.approval_required)
self.assertEqual(info["state_contract"], "n7-can-signals/v1")
self.assertEqual(info["control_contract"], "n7-headunit-can-command/v1")
def test_explicit_shared_approval_policy_still_locks_mqtt_control(self) -> None:
with patch.dict(
"os.environ",
{"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED": "true"},
clear=True,
):
settings = VehicleMqttSettings.from_env()
self.assertTrue(settings.approval_required)
self.assertFalse(settings.configured)
def test_accepts_one_canonical_frame_and_common_receive_aliases(self) -> None:
canonical = parse_mqtt_can_payload(
json.dumps(
@@ -172,8 +199,8 @@ class MqttCanPayloadTests(unittest.TestCase):
self.assertEqual(frame.arbitration_id, 0x42E)
self.assertTrue(frame.is_fd)
def test_accepts_nested_can0_seat_signals_with_can0_default(self) -> None:
frame = parse_mqtt_can_payload(
def test_ignores_legacy_can0_state_outside_seat_ota_wire_protocol(self) -> None:
frames = parse_mqtt_can_payload(
json.dumps(
{
"schema": "n7-can-signals/v1",
@@ -188,10 +215,8 @@ class MqttCanPayloadTests(unittest.TestCase):
},
}
)
)[0]
self.assertEqual(frame.arbitration_id, 0x302)
self.assertEqual(frame.channel, 0)
self.assertFalse(frame.is_fd)
)
self.assertEqual(frames, [])
def test_translates_seat_ota_info_responses_into_four_position_states(self) -> None:
values = {
@@ -199,10 +224,6 @@ class MqttCanPayloadTests(unittest.TestCase):
2: (373 << 16) | 266,
3: (917 << 16) | 451,
4: (647 << 16) | 575,
5: 0x11,
6: 0x11,
7: 0x11,
8: 0x11,
}
signals = []
for selector, value in values.items():
@@ -243,7 +264,7 @@ class MqttCanPayloadTests(unittest.TestCase):
"fd",
{
"channel": "can0",
"canId": "0x302",
"canId": "0x6A0",
"frameType": "fd",
"bytes": [0] * 8,
},
@@ -252,7 +273,7 @@ class MqttCanPayloadTests(unittest.TestCase):
"brs",
{
"channel": "can0",
"canId": "0x302",
"canId": "0x6A2",
"frameType": "classic",
"brs": True,
"bytes": [0] * 8,
@@ -262,7 +283,7 @@ class MqttCanPayloadTests(unittest.TestCase):
"dlc",
{
"channel": "can0",
"canId": "0x302",
"canId": "0x6A2",
"frameType": "classic",
"bytes": [0] * 7,
},
@@ -292,7 +313,7 @@ class MqttCanPayloadTests(unittest.TestCase):
)
self.assertEqual(
[(frame.channel, frame.arbitration_id) for frame in frames],
[(0, 0x302), (1, 0x342)],
[(1, 0x342)],
)
def test_rejects_decoded_state_invalid_channels_and_channel_conflicts(self) -> None:
@@ -322,7 +343,7 @@ class MqttCanPayloadTests(unittest.TestCase):
host="127.0.0.1",
port=1883,
condition_topic="n7/n7-headunit/can/state",
control_topic="n7/can_control",
control_topic="n7/n7-headunit/can/command",
qos=1,
keepalive_s=30,
username=None,
@@ -377,6 +398,12 @@ class MqttCanTransportTests(unittest.TestCase):
for state in snapshot.feedback_states
)
)
self.assertTrue(
all(
state["source"] == "wireless_mqtt_ch1"
for state in snapshot.feedback_states
)
)
self.assertEqual(
self.client.subscriptions,
[("n7/n7-headunit/can/state", 1)],
@@ -404,6 +431,53 @@ class MqttCanTransportTests(unittest.TestCase):
self.assertLess(time.perf_counter() - started, 0.05)
self.assertEqual(snapshot.observed_ids, frozenset({0x388}))
def test_vehicle_envelope_four_frames_decode_from_raw_can1_bytes(self) -> None:
self.client.emit(
"n7/n7-headunit/can/state",
{
"schema": "n7-can-signals/v1",
"sequence": 335,
"signals": [
{"channel": "can1", "canId": frame_id, "frameType": "fd",
"brs": True, "data": data}
for frame_id, data in (
("0x388", "00 00 00 02 01 00 59 00"),
("0x41A", "00 5C 85 08 20 42 C1 FD"),
("0x42E", "11 64 32 03 90 02 00 C6"),
("0x3FF", "5B 09 FF 0F 32 61 04 12"),
)
],
# This summary is deliberately inconsistent. Only the raw CAN
# bytes above are allowed to update authoritative UI state.
"can1Vehicle": {"parsed": {"speedKph": 123, "gear": 4}},
},
)
snapshot = self.transport.snapshot()
self.assertEqual(
snapshot.observed_ids,
frozenset({0x388, 0x3FF, 0x41A, 0x42E}),
)
by_id = {state["frame_id"]: state for state in snapshot.feedback_states}
self.assertEqual(
by_id["0x388"]["domain_state"]["window"]["positions"]["rear_left"]["position_pct"],
2,
)
self.assertEqual(
by_id["0x41A"]["domain_state"]["roof"]["shade_position_pct"],
92,
)
self.assertIsNone(
by_id["0x42E"]["domain_state"]["ambient_light"]["front_color_index"]
)
self.assertEqual(
by_id["0x3FF"]["domain_state"]["interior_light"]["ambient_illuminance_lux"],
510,
)
self.assertTrue(
all(state["source"] == "wireless_mqtt_ch1" for state in by_id.values())
)
def test_stale_safety_signal_cannot_enter_snapshot_or_executor_bus(self) -> None:
bus = self.transport.bus(channel=1)
self.client.emit(
@@ -470,8 +544,9 @@ class MqttCanTransportTests(unittest.TestCase):
self.assertGreater(verification["cache_age_ms"], 50_000)
self.assertIsNone(bus.receive(0, include_fd=True))
def test_bus_publishes_only_canonical_frame_contract(self) -> None:
def test_bus_publishes_only_headunit_command_contract(self) -> None:
bus = self.transport.bus()
bus.set_debug_command("shade position 20")
sent = bus.send(
0x431,
bytes.fromhex("10 00 00 00 00 00 00 00"),
@@ -482,25 +557,253 @@ class MqttCanTransportTests(unittest.TestCase):
self.assertEqual(len(self.client.published), 1)
topic, raw_payload, qos, retain = self.client.published[0]
payload = json.loads(raw_payload)
self.assertEqual(topic, "n7/can_control")
self.assertEqual(topic, "n7/n7-headunit/can/command")
self.assertEqual(qos, 1)
self.assertFalse(retain)
self.assertEqual(payload["schema"], MQTT_CAN_SCHEMA)
self.assertEqual(payload["arbitration_id"], 0x431)
self.assertEqual(payload["data_hex"], "10 00 00 00 00 00 00 00")
self.assertTrue(payload["is_fd"])
self.assertTrue(payload["bitrate_switch"])
self.assertRegex(payload["requestId"], r"^cognitive-agent-[0-9a-f]{32}$")
self.assertEqual(payload["command"], "shade position 20")
self.assertEqual(payload["channel"], "can1")
self.assertEqual(payload["canId"], "0x431")
self.assertEqual(payload["frameType"], "fd")
self.assertTrue(payload["brs"])
self.assertFalse(payload["extended"])
self.assertFalse(payload["remote"])
self.assertEqual(payload["data"], "10 00 00 00 00 00 00 00")
self.assertEqual(payload["repeat"], 1)
self.assertEqual(payload["intervalMs"], 0)
self.assertEqual(payload["responseTimeoutMs"], 100)
publication = bus.published_messages[0]
self.assertEqual(publication["topic"], topic)
self.assertEqual(publication["qos"], qos)
self.assertEqual(publication["retain"], retain)
self.assertEqual(publication["payload"], payload)
self.assertTrue(
{
"schema",
"arbitration_id",
"frame_id",
"data_hex",
"is_fd",
"bitrate_switch",
}.isdisjoint(payload)
)
bus.send(
0x431,
bytes.fromhex("10 00 00 00 00 00 00 00"),
is_fd=True,
bitrate_switch=True,
)
second_payload = json.loads(self.client.published[1][1])
self.assertNotEqual(payload["requestId"], second_payload["requestId"])
with self.assertRaisesRegex(Exception, "FD command requires BRS"):
bus.send(0x431, bytes(8), is_fd=True, bitrate_switch=False)
with self.assertRaisesRegex(Exception, "classic CAN command cannot enable BRS"):
bus.send(0x431, bytes(8), is_fd=False, bitrate_switch=True)
with self.assertRaisesRegex(Exception, "approved CAN1 control set"):
bus.send(0x555, bytes(8), is_fd=True)
self.assertEqual(len(self.client.published), 1)
with self.assertRaisesRegex(Exception, "at most 256"):
bus.set_debug_command("x" * 257)
self.assertEqual(len(self.client.published), 2)
self.assertEqual(len(bus.published_messages), 2)
def test_can1_whitelist_publishes_rear_lock_candidate_with_debug_command(self) -> None:
bus = self.transport.bus(channel=1)
bus.set_debug_command("door rear lock")
sent = bus.send(0x32B, bytes.fromhex("0A 40 00 00 00 00 00 00"))
self.assertEqual(sent.arbitration_id, 0x32B)
self.assertFalse(sent.is_fd)
topic, raw_payload, qos, retain = self.client.published[-1]
payload = json.loads(raw_payload)
self.assertEqual(topic, "n7/n7-headunit/can/command")
self.assertEqual(qos, 1)
self.assertFalse(retain)
self.assertEqual(payload["command"], "door rear lock")
self.assertEqual(payload["channel"], "can1")
self.assertEqual(payload["canId"], "0x32B")
self.assertEqual(payload["frameType"], "classic")
self.assertFalse(payload["brs"])
self.assertEqual(payload["data"], "0A 40 00 00 00 00 00 00")
def test_wireless_whitelisted_climate_plan_publishes_without_safety_or_feedback_frames(
self,
) -> None:
service = SeatDemoService(REPOSITORY_ROOT)
service._vehicle_mqtt_transport = self.transport
service.vehicle_mqtt_settings = SimpleNamespace(feedback_timeout_ms=1)
actions = [
{
"domain": "climate",
"capability": "climate.set_temperature",
"target": "driver",
"operation": "set",
"parameters": {"temperature_c": 24},
},
{
"domain": "climate",
"capability": "climate.set_fan_level",
"target": "vehicle",
"operation": "set",
"parameters": {"level": 2},
},
]
runtime = NativeCanRuntime(REPOSITORY_ROOT)
config = runtime.build_function_config(
"climate", actions, "req-wireless-climate", "local_rules"
)
plan = runtime.compile(config)
with (
patch(
"cognitive_agent.application.seat_service.observe_vehicle_safety",
side_effect=AssertionError(
"wireless whitelist publish must not require 0x342/0x4EB"
),
),
patch("cognitive_agent.application.native_can_runtime.time.sleep"),
):
reports = service._execute_orchestrated_reports(
{"native_climate": config},
{"native_climate": plan},
{"native_climate"},
{},
hardware_request=True,
simulated_hardware_request=False,
simulation_fault="normal",
control_transport="wireless_mqtt",
)
report = reports["native_climate"]
self.assertEqual(report["spec"]["status"], "succeeded")
self.assertEqual(
report["spec"]["vehicle_safety_preflight"]["policy"],
"mqtt_whitelist_publish",
)
self.assertEqual(len(self.client.published), 6)
payloads = [json.loads(item[1]) for item in self.client.published]
self.assertTrue(
all(
topic == "n7/n7-headunit/can/command"
for topic, _payload, _qos, _retain in self.client.published
)
)
self.assertTrue(all(payload["channel"] == "can1" for payload in payloads))
self.assertTrue(all(payload["canId"] == "0x1DE" for payload in payloads))
self.assertEqual(
[payload["command"] for payload in payloads],
["ac temp driver 24"] * 3 + ["ac fan 2"] * 3,
)
self.assertEqual(
[payload["data"] for payload in payloads],
[
"00 00 00 00 18 00 00 00",
"00 00 00 00 18 00 00 00",
"00 00 00 00 18 00 00 00",
"20 00 00 00 00 00 00 00",
"20 00 00 00 00 00 00 00",
"20 00 00 00 00 00 00 00",
],
)
for result in report["spec"]["results"]:
self.assertEqual(result["status"], "succeeded")
self.assertEqual(result["telemetry"]["delivery_status"], "published")
self.assertFalse(result["telemetry"]["feedback_required"])
self.assertFalse(result["telemetry"]["physical_effect_confirmed"])
self.assertEqual(len(result["telemetry"]["mqtt_messages"]), 3)
self.assertTrue(
all(
message["topic"] == "n7/n7-headunit/can/command"
for message in result["telemetry"]["mqtt_messages"]
)
)
def test_model_added_nonwhitelisted_action_does_not_block_whitelisted_publish(
self,
) -> None:
service = SeatDemoService(REPOSITORY_ROOT)
service._vehicle_mqtt_transport = self.transport
service.vehicle_mqtt_settings = SimpleNamespace(
enabled=True,
configured=True,
feedback_timeout_ms=1,
authorize=lambda _token: None,
)
model_output = {
"detected_domains": ["climate", "ambient_light"],
"requests": [
{
"domain": "ambient_light",
"capability": "ambient_light.set_mode",
"target_scope": "vehicle",
"operation": "set",
"parameters": [{"name": "mode", "value": "cinema"}],
"reason": "模型根据观影状态补充了氛围灯模式。",
},
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "driver",
"operation": "set",
"parameters": [{"name": "temperature_c", "value": 24}],
"reason": "用户明确指定温度。",
},
{
"domain": "climate",
"capability": "climate.set_fan_level",
"target_scope": "vehicle",
"operation": "set",
"parameters": [{"name": "level", "value": 2}],
"reason": "用户明确指定风量。",
},
],
"ambiguities": [],
"decision_summary": ["执行用户明确指定的观影空调设置。"],
"confidence": 0.96,
}
with tempfile.TemporaryDirectory() as temporary_directory:
service.artifact_root = Path(temporary_directory)
service.openai.api_key = "test-key"
with (
patch.object(service.openai, "interpret", return_value=model_output),
patch("cognitive_agent.application.native_can_runtime.time.sleep"),
):
result = service.process(
"进入观影状态:空调24度、风量2档,",
provider="openai",
conversation_id="conv-wireless-movie-climate",
execution_mode="hardware",
control_transport="wireless_mqtt",
)
self.assertEqual(result["status"], "partially_succeeded")
self.assertEqual(
[item["capability"] for item in result["deferred_requests"]],
["ambient_light.set_mode"],
)
self.assertEqual(
result["deferred_requests"][0]["rejection_code"],
"WIRELESS_MQTT_CONTROL_NOT_WHITELISTED",
)
self.assertEqual(len(self.client.published), 6)
self.assertTrue(
all(
topic == "n7/n7-headunit/can/command"
and json.loads(payload)["canId"] == "0x1DE"
for topic, payload, _qos, _retain in self.client.published
)
)
def test_can0_bus_publishes_only_classic_passenger_seat_control(self) -> None:
bus = self.transport.bus(channel=0)
sent = bus.send(0x6A0, bytes.fromhex("C7 01 64 00 F4 01 00 00"))
payload = json.loads(self.client.published[-1][1])
self.assertEqual(sent.channel, 0)
self.assertEqual(payload["channel"], 0)
self.assertFalse(payload["is_fd"])
self.assertEqual(payload["channel"], "can0")
self.assertEqual(payload["canId"], "0x6A0")
self.assertEqual(payload["command"], "")
self.assertEqual(payload["frameType"], "classic")
self.assertFalse(payload["brs"])
with self.assertRaisesRegex(Exception, "approved CAN0 control set"):
bus.send(0x431, bytes(8))
with self.assertRaisesRegex(Exception, "requires classic CAN"):
@@ -524,7 +827,10 @@ class MqttCanTransportTests(unittest.TestCase):
"n7/n7-headunit/can/state",
{
"signals": [
{"channel": "can0", "canId": "0x302", "bytes": [0] * 8},
{"channel": "can0", "canId": "0x6A0", "bytes":
[0xC7, 0x21, 0x60, 1, 0, 0, 0, 0]},
{"channel": "can0", "canId": "0x6A2", "bytes":
[0x7C, 0x21, 0x60, 0, 0xE0, 0x01, 0xE0, 0x01]},
{
"channel": "can1",
"canId": "0x342",
@@ -534,6 +840,7 @@ class MqttCanTransportTests(unittest.TestCase):
]
},
)
self.assertEqual(can0.receive(0).arbitration_id, 0x6A2)
self.assertEqual(can0.receive(0).arbitration_id, 0x302)
self.assertEqual(can1.receive(0, include_fd=True).arbitration_id, 0x342)
self.assertIsNone(can0.receive(0))
@@ -546,10 +853,6 @@ class MqttCanTransportTests(unittest.TestCase):
2: (373 << 16) | 266,
3: (917 << 16) | 451,
4: (647 << 16) | 575,
5: 0x11,
6: 0x11,
7: 0x11,
8: 0x11,
}.items():
sequence = 0x40 + selector
signals.extend(
+67
View File
@@ -8,6 +8,7 @@ from cognitive_agent.application.mqtt_ota_seat_executor import (
MqttOtaSeatExecutor,
)
from cognitive_agent.infrastructure.can.gateway import CanFrame
from seat_domain import parse_local_intent
def _axis_frame(axis: int, value_x100: int, *, valid: bool = True) -> CanFrame:
@@ -34,8 +35,12 @@ class _FakeOtaBus:
def __init__(self, initial_frames: list[CanFrame]) -> None:
self.frames = deque(initial_frames)
self.sent: list[CanFrame] = []
self.debug_commands: list[str] = []
self.closed = False
def set_debug_command(self, command: str) -> None:
self.debug_commands.append(command)
def send(
self,
arbitration_id: int,
@@ -110,6 +115,14 @@ def _function_config(capability: str, parameters: dict[str, object]) -> dict[str
class MqttOtaSeatExecutorTests(unittest.TestCase):
def test_seat_height_language_maps_to_internal_axis_one_capability(self) -> None:
parsed = parse_local_intent("副驾座椅高度调到60%")
self.assertEqual(parsed["ambiguities"], [])
self.assertEqual(parsed["actions"][0]["axis"], "cushion_tilt")
self.assertEqual(parsed["actions"][0]["position_pct"], 60)
self.assertIn("目标:副驾座椅高度位置60%。", parsed["decision_summary"])
def test_absolute_move_uses_runtime_move_and_fresh_position_confirmation(self) -> None:
bus = _FakeOtaBus([_axis_frame(0, 4_000)])
executor = MqttOtaSeatExecutor( # type: ignore[arg-type]
@@ -146,6 +159,10 @@ class MqttOtaSeatExecutorTests(unittest.TestCase):
self.assertEqual(bus.sent[0].data[2], 0x64)
self.assertEqual(bus.sent[0].data[3], 0)
self.assertEqual(int.from_bytes(bus.sent[0].data[4:8], "little"), 500)
self.assertEqual(
bus.debug_commands,
["seat passenger longitudinal position 50"],
)
self.assertEqual(
report["spec"]["results"][0]["telemetry"]["effective_position_pct"],
50,
@@ -213,6 +230,56 @@ class MqttOtaSeatExecutorTests(unittest.TestCase):
self.assertIn("does not provide wireless thermal control", report["spec"]["results"][0]["message"])
self.assertEqual(bus.sent, [])
def test_dynamic_semantic_modes_use_seat_ota_firmware_selectors(self) -> None:
for semantic_mode, firmware_selector, mode_name in (
(1, 2, "木马模式"),
(2, 1, "动态摇篮模式"),
):
with self.subTest(semantic_mode=semantic_mode):
bus = _FakeOtaBus([])
executor = MqttOtaSeatExecutor( # type: ignore[arg-type]
_StatusExecutor(),
lambda: bus,
)
plan = {
"spec": {
"commands": [
{
"operation_ref": "operation-1",
"message": "T2G_Seat_Move_Command_300",
"signals": {"Opcode": 0x0E, "Motor_Selector": 0},
}
]
}
}
report = executor.execute(
plan,
_function_config(
"seat.start_dynamic_mode",
{"mode_id": semantic_mode},
),
)
self.assertEqual(report["spec"]["status"], "succeeded")
self.assertEqual(bus.sent[0].data[2], 0x66)
self.assertEqual(bus.sent[0].data[3], firmware_selector)
self.assertEqual(
bus.debug_commands,
[
"seat passenger dynamic start "
+ ("rocking-horse" if semantic_mode == 1 else "cradle")
],
)
self.assertEqual(
int.from_bytes(bus.sent[0].data[4:8], "little"),
300,
)
telemetry = report["spec"]["results"][0]["telemetry"]
self.assertEqual(telemetry["dynamic_mode_id"], semantic_mode)
self.assertEqual(telemetry["ota_dynamic_selector"], firmware_selector)
self.assertEqual(telemetry["dynamic_mode_name"], mode_name)
if __name__ == "__main__":
unittest.main()
+28
View File
@@ -35,6 +35,13 @@ class NativeCanFeedbackTests(unittest.TestCase):
self.assertTrue(feedback_matches("climate_temperature:driver:24", feedback))
self.assertFalse(feedback_matches("climate_temperature:driver:25", feedback))
def test_climate_feedback_decodes_front_defrost(self) -> None:
decoded = decode_feedback(
self.frame(0x404, "43A9116c1818a2dd")
)["domain_state"]["climate"]
self.assertEqual(decoded["front_defrost"], "on")
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))
@@ -90,6 +97,27 @@ class NativeCanFeedbackTests(unittest.TestCase):
self.assertTrue(feedback_matches("driver_seat_thermal:heating:low", feedback))
self.assertFalse(feedback_matches("driver_seat_thermal:ventilation:high", feedback))
def test_driver_seat_feedback_supports_all_three_thermal_levels(self) -> None:
driver = decode_feedback(
self.frame(0x4FA, "1C00000000010000")
)["actuator_state"]["driver"]
self.assertEqual(driver["ventilation"], {"state": "on", "level": 3})
self.assertEqual(driver["heating"], {"state": "on", "level": 2})
def test_38a_matrix_decodes_front_lighting_feedback(self) -> None:
lights = decode_feedback(
self.frame(0x38A, "1969000000000000")
)["domain_state"]["front_light"]
self.assertEqual(lights["position_light"]["state"], "on")
self.assertEqual(lights["low_beam"]["state"], "off")
self.assertEqual(lights["high_beam"]["state"], "on")
self.assertEqual(lights["fog_light"]["state"], "on")
self.assertEqual(lights["brake_light"]["state"], "flashing")
self.assertEqual(lights["reverse_light"]["state"], "off")
self.assertEqual(lights["all_weather_light"]["state"], "on")
def test_driver_position_feedback_never_claims_absolute_position(self) -> None:
decoded = decode_feedback(
self.frame(0x494, "151515058c28203c")
+53 -1
View File
@@ -157,7 +157,7 @@ class NativeStateSyncTests(unittest.TestCase):
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"]["ambient_light"]["brightness_pct"], 60)
self.assertEqual(
state["domain_devices"]["door"]["locks"]["rear_left"]["feedback_source"],
"physical_can_ch1",
@@ -190,6 +190,58 @@ class NativeStateSyncTests(unittest.TestCase):
)
self.assertEqual(after, before)
def test_verified_wireless_ch1_feedback_updates_realtime_state(self) -> None:
state = self.store.synchronize_physical_ch1_feedback(
"conv-native-sync",
[{
"source": "wireless_mqtt_ch1",
"channel": 1,
"frame_id": "0x41A",
"feedback_frame_id": "0x41A",
"valid": True,
"verification": {
"authoritative": True,
"channel": 1,
"simulated": False,
"read_only": True,
"transport": "wireless_mqtt",
"frame_id": "0x41A",
"payload_size": 8,
},
"domain_state": {
"roof": {"shade_position_pct": 92, "motion_state": "stopped"}
},
}],
)
roof = state["domain_devices"]["roof"]
self.assertEqual(roof["shade_position_pct"], 92)
self.assertEqual(roof["feedback_source"], "wireless_mqtt_ch1")
self.assertIn("wireless_mqtt_ch1_state_updated_at", state)
def test_wireless_source_without_mqtt_transport_verification_is_ignored(self) -> None:
before = self.store.snapshot("conv-native-sync")
after = self.store.synchronize_physical_ch1_feedback(
"conv-native-sync",
[{
"source": "wireless_mqtt_ch1",
"channel": 1,
"frame_id": "0x41A",
"feedback_frame_id": "0x41A",
"valid": True,
"verification": {
"authoritative": True,
"channel": 1,
"simulated": False,
"read_only": True,
"frame_id": "0x41A",
"payload_size": 8,
},
"domain_state": {"roof": {"shade_position_pct": 92}},
}],
)
self.assertEqual(after, before)
def test_verified_read_only_ch1_feedback_updates_only_driver(self) -> None:
state = self.store.synchronize_physical_ch1_feedback(
"conv-native-sync",
@@ -1,5 +1,8 @@
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
@@ -35,6 +38,37 @@ def test_frontend_only_exposes_user_orchestration_and_extension() -> None:
assert "v2RoleSelect" not in console
def test_retired_layout_is_removed_instead_of_permanently_hidden() -> None:
html = source("index.html")
app = source("app.js")
console = source("console-v2.js")
styles = source("console-v2.css")
for retired_markup in (
'class="topbar"',
'class="example-group"',
'id="safetyStrip"',
'class="pipeline"',
'id="gestureBackdrop"',
):
assert retired_markup not in html
for retired_runtime_hook in (
"v2-legacy-parking",
"v2-legacy-hidden",
"#connectionBadge",
"#modelBadge",
"#hardwareBadge",
"#executionBadge",
"#logoutButton",
"#gestureToggleButton",
"#pwaBadge",
):
assert retired_runtime_hook not in app
assert retired_runtime_hook not in console
assert retired_runtime_hook not in styles
def test_extension_pet_and_transient_vehicle_state_restart_contract() -> None:
extension = source("self-extension.js")
styles = source("self-extension.css")
@@ -138,6 +172,278 @@ def test_vehicle_device_status_cards_prioritize_seats_and_expose_manual_controls
assert ".v2-device-control-apply" in styles
def test_passenger_can0_status_ui_uses_only_seat_ota_four_axis_contract() -> None:
app = source("app.js")
console = source("console-v2.js")
assert 'new Set(["longitudinal", "height", "backrest_recline", "legrest_height"])' in app
assert 'if (thermalRow) thermalRow.hidden = seat === "passenger";' in app
assert 'height: seatHeight,' in app
assert '"seat.set_cushion_tilt": "调节座椅高度"' in app
assert 'rows: () => [["前后", seatAxisValue("passenger", "longitudinal")], ["高度", seatAxisValue("passenger", "cushion_tilt")]' in console
assert 'rangeControl("cushion_tilt", "高度"' in console
assert 'rangeControl("legrest_extension"' not in console
def test_left_sidebar_exposes_collapsed_parsed_can_command_panel() -> None:
console = source("console-v2.js")
styles = source("console-v2.css")
assert 'make("details", "v2-can-command-panel"' in console
assert 'canCommandPanel.id = "v2CanCommandPanel"' in console
assert 'id="v2CanCommandCount">0</strong>' in console
assert 'id="v2CanCommandList"' in console
assert "canCommandPanel.open" not in console
assert "function parsedCanCommandForOperation(operation = {})" in console
assert "function parsedCanCommands(result = {})" in console
assert "getParsedCanCommands: (result) => parsedCanCommands(result)" in console
assert "const configurations = result.function_configs || {};" in console
assert "const plans = result.command_plans || {};" in console
assert 'const command = (category, text, channel = "CAN1", frame = "")' in console
assert 'data-can-channel="${escapeHtml(item.channel)}"' in console
assert 'class="v2-can-command-channel"' in console
assert 'window.addEventListener("cognitive-agent:result"' in console
assert '$("#requestForm")?.addEventListener("submit"' in console
for command_contract in (
"door rear ${action}",
"door vehicle ${lockState}",
"window ${target} ${motion}",
"window ${target} position ${position}",
"ac ${onOff}",
"ac fan ${level}",
"ac mode ${mode}",
"ac temp ${target} ${temperature}",
"ac control ${mode}",
"ac cycle ${mode}",
"outlet ${target} percent ${horizontal} ${vertical}",
"outlet ${target} position ${horizontal} ${vertical}",
"seat driver ${feature} ${setting}",
"seat driver massage strength ${strength}",
"seat driver ${axis} ${direction} ${duration}",
"seat driver memory recall ${position}",
"seat driver position ${position}",
"light indoor ${onOff}",
"light reading all ${onOff}",
"light ambient color ${color}",
"light ambient brightness ${level}",
"shade position ${position}",
"seat passenger ${axis[1]} position ${position}",
"seat passenger dynamic start ${dynamic.name}",
"seat passenger dynamic stop",
"otaCan0Frame(0x64, axis[2], targetPermille)",
"otaCan0Frame(0x66, dynamic.selector, 300)",
"otaCan0Frame(0x63, 0xFF, 0)",
):
assert command_contract in console
assert ".v2-can-command-panel[open]" in styles
assert ".v2-can-command-list li code" in styles
assert ".v2-can-command-channel" in styles
assert 'li[data-can-channel="CAN0"]' in styles
assert ".v2-sidebar-collapsed .v2-can-command-panel" in styles
def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands() -> None:
node = shutil.which("node")
if node is None:
return
console = source("console-v2.js")
start = console.index(" const normalizedCanTarget")
end = console.index("\n\n function renderParsedCanCommands", start)
helpers = console[start:end]
sample = {
"function_configs": {
"seat": {
"spec": {
"operations": [
{
"capability": "seat.set_backrest_recline",
"target": {"zone": "front_right"},
"parameters": {"position_pct": 36, "angle_deg": 120},
},
]
}
},
"native_climate": {
"spec": {
"operations": [
{
"capability": "climate.set_temperature",
"target": {"zone": "driver"},
"parameters": {"temperature_c": 24},
}
]
}
},
},
"command_plans": {
"seat": {"spec": {"commands": [{}]}},
"native_climate": {"spec": {"commands": [{}]}},
},
}
script = (
f"{helpers}\n"
f"const sample = {json.dumps(sample)};\n"
"process.stdout.write(JSON.stringify(parsedCanCommands(sample)));"
)
completed = subprocess.run(
[node, "-e", script],
check=True,
capture_output=True,
text=True,
)
assert json.loads(completed.stdout) == [
{
"channel": "CAN0",
"category": "副驾座椅靠背调节",
"command": "seat passenger backrest position 36",
"frame": "0x6A0 · C7 <SEQ> 64 02 68 01 00 00",
},
{
"channel": "CAN1",
"category": "空调温度",
"command": "ac temp driver 24",
},
]
def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences() -> None:
app = source("app.js")
styles = source("styles.css")
html = source("index.html")
assert 'class="mode-editor-flow-preview empty"' in html
assert "function modeEditorReadableCommands(flow)" in app
assert "function modeEditorMessagesForOperation(flow, readable, operationRef, plan)" in app
assert 'command?.signals?.special_handler === "driver_seat_memory_recall"' in app
assert "Array.isArray(special?.frame_sequence)" in app
assert "function modeEditorAppendCommandCard(preview, item, flow)" in app
assert 'card.className = "mode-editor-command-card"' in app
assert 'title.textContent = flow?.control_transport === "wireless_mqtt" ? "MQTT 消息序列"' in app
assert 'source: "published"' in app
assert 'source: flow?.control_transport === "wireless_mqtt" ? "projected" : "can"' in app
assert "function_configs: functionConfigs" in app
assert "execution_steps: executionSteps" in app
assert ".mode-editor-command-card" in styles
assert ".mode-editor-command-meta" in styles
assert ".mode-editor-message-sequence" in styles
assert '.mode-editor-command-card[data-can-channel="CAN0"]' in styles
def test_mode_editor_prioritizes_can_flow_display_area() -> None:
app = source("app.js")
html = source("index.html")
styles = source("styles.css")
assert 'class="mode-editor-setup"' in html
assert 'class="mode-editor-inputs"' in html
assert "grid-template-rows: minmax(0, 30%) minmax(0, 1fr) auto auto;" in styles
assert "grid-template-columns: minmax(150px, 0.32fr) minmax(0, 1fr);" in styles
assert ".mode-editor-instruction-field textarea" in styles
assert "resize: none;" in styles
assert ".mode-editor-flow-preview" in styles
assert "overflow: auto;" in styles
assert 'id="modeEditorFlowExpand"' in html
assert ".mode-editor.flow-expanded" in styles
assert ".mode-editor-backdrop.flow-expanded" in styles
assert "width: 100vw;" in styles
assert "height: 100dvh;" in styles
assert "grid-template-rows: minmax(0, 1fr);" in styles
assert "touch-action: pan-y;" in styles
assert "scrollbar-gutter: stable;" in styles
assert "max-height: 220px;" not in styles
assert "function setModeEditorFlowExpanded(expanded)" in app
assert '$("#modeEditorFlowExpand").addEventListener("click", toggleModeEditorFlowExpanded);' in app
def test_mode_editor_prepares_one_to_many_mqtt_message_sequences() -> None:
node = shutil.which("node")
if node is None:
return
app = source("app.js")
console = source("console-v2.js")
console_start = console.index(" const normalizedCanTarget")
console_end = console.index("\n\n function renderParsedCanCommands", console_start)
console_helpers = console[console_start:console_end]
app_start = app.index("function modeEditorCommandCategory")
app_end = app.index("\n\nfunction modeEditorAppendCommandCard", app_start)
app_helpers = app[app_start:app_end]
sample = {
"control_transport": "wireless_mqtt",
"function_configs": {
"native_climate": {
"spec": {
"operations": [
{
"id": "temperature",
"capability": "climate.set_temperature",
"target": {"zone": "driver"},
"parameters": {"temperature_c": 25},
}
]
}
}
},
"plans": {
"native_climate": {
"spec": {
"commands": [
{
"operation_ref": "temperature",
"signals": {
"frame_id": "0x1DE",
"data_hex": "00 00 00 00 19 00 00 00",
"is_fd": False,
"bitrate_switch": False,
"debug_command": "ac temp driver 25",
},
"send_offset_ms": 20,
"repeat": {"count": 3, "period_ms": 100},
}
]
}
}
},
"execution_steps": [],
}
script = (
f"{console_helpers}\n"
"const state = {runtime:{vehicle_mqtt:{control_topic:"
'"n7/n7-headunit/can/command",qos:1,feedback_timeout_ms:1500}}};\n'
"const window = {CognitiveAgentConsoleV2:{getParsedCanCommands:"
"(result)=>parsedCanCommands(result)}};\n"
f"{app_helpers}\n"
f"const flow = {json.dumps(sample)};\n"
"process.stdout.write(JSON.stringify(modeEditorReadableCommands(flow)));"
)
completed = subprocess.run(
[node, "-e", script],
check=True,
capture_output=True,
text=True,
)
commands = json.loads(completed.stdout)
assert len(commands) == 1
assert commands[0]["channel"] == "CAN1"
assert commands[0]["category"] == "空调温度"
assert commands[0]["command"] == "ac temp driver 25"
assert [message["offset_ms"] for message in commands[0]["messages"]] == [
20,
120,
220,
]
assert all(
message["topic"] == "n7/n7-headunit/can/command"
and message["payload"]["canId"] == "0x1DE"
and message["payload"]["command"] == "ac temp driver 25"
for message in commands[0]["messages"]
)
def test_interactive_console_keeps_actions_inside_composer_and_collapsed_rails_aligned() -> None:
console = source("console-v2.js")
styles = source("console-v2.css")
@@ -162,15 +468,22 @@ def test_left_sync_rail_can_collapse_and_log_aligns_with_lower_workspace() -> No
assert 'make("section", "v2-sync-sidebar"' in console
assert '<div class="v2-sync-sidebar-overview">' in console
assert '$(".v2-sync-sidebar-overview", syncSidebar)?.append(sidebarFooter);' in console
assert "syncOverview?.append(sidebarFooter);" in console
assert '$(".v2-sync-sidebar-heading", syncSidebar)?.append(sidebarToggle);' in console
assert "function syncSidebarLogBaseline()" in console
assert "const requiredOverviewHeight = overviewRect" in console
assert "overviewBottom - overviewRect.top + overviewPaddingBottom + 10" in console
assert 'sidebar.style.setProperty("--v2-sync-log-top"' in console
assert 'window.addEventListener("resize", syncSidebarLogBaseline);' in console
assert "requestAnimationFrame(syncSidebarLogBaseline);" in console
assert "--v2-sync-log-top: 291px;" in styles
assert "grid-template-rows: calc(var(--v2-sync-log-top) - 10px) minmax(0, 1fr);" in styles
assert "grid-auto-rows: max-content;" in styles
assert "align-content: start;" in styles
assert "min-height: max-content;" in styles
assert "align-self: start;" in styles
assert "overflow: visible;" in styles
assert ".v2-sync-sidebar > .v2-sync-log" in styles
assert "grid-template-columns: 64px minmax(0, 1fr);" in styles
assert ".v2-sidebar-collapsed #v2SidebarToggle" in styles
+8 -1
View File
@@ -199,7 +199,14 @@ class SelfExtensionTests(unittest.TestCase):
response.__enter__.return_value = response
response.read.return_value = stream_body.encode("utf-8")
with patch.dict(os.environ, {"OPENAI_STREAM": "true"}, clear=False):
with patch.dict(
os.environ,
{
"OPENAI_STREAM": "true",
"IR_OPENAI_MODEL": "gpt-5.6-sol",
},
clear=False,
):
service = SelfExtensionService(SimulationGateway())
with patch(
"cognitive_agent.application.self_extension.urlopen",
+82 -17
View File
@@ -33,8 +33,12 @@ class _FakeBus:
self.send_attempts: list[tuple[int, bytes, bool, bool]] = []
self.sent: list[CanFrame] = []
self.events: list[str] = []
self.debug_commands: list[str] = []
self.closed = False
def set_debug_command(self, command: str) -> None:
self.debug_commands.append(command)
def send(
self,
arbitration_id: int,
@@ -326,7 +330,7 @@ class VerifiedBodyCanTests(unittest.TestCase):
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:
def test_rear_lock_compiles_only_the_matrix_backed_second_row_request(self) -> None:
action = {
"domain": "door",
"capability": "door.set_rear_lock",
@@ -335,23 +339,55 @@ class VerifiedBodyCanTests(unittest.TestCase):
"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(
catalog_entry["execution_support"], "matrix_candidate_with_feedback"
)
self.assertTrue(catalog_entry["physical_control_supported"])
self.assertIn(action["capability"], self.runtime.supported_capabilities)
self.assertTrue(self.runtime.supports(action))
cases = (
("lock", "0A 40 00 00 00 00 00 00"),
("unlock", "0A 80 00 00 00 00 00 00"),
)
for state, payload in cases:
with self.subTest(state=state):
command = self.compile_action(
"door", "door.set_rear_lock", "rear", {"state": state}
)
self.assertEqual(command["signals"]["frame_id"], "0x32B")
self.assertEqual(command["signals"]["data_hex"], payload)
self.assertFalse(command["signals"]["is_fd"])
self.assertFalse(command["signals"]["bitrate_switch"])
self.assertNotIn("repeat", command)
self.assertEqual(command["signals"]["feedback_ids"], "0x407")
self.assertEqual(
command["signals"]["expected_feedback"], f"rear_lock:{state}"
)
exact = _parse_exact_command(f"door rear {state}")
self.assertEqual(exact[0]["capability"], "door.set_rear_lock")
self.assertEqual(exact[0]["target"], "rear")
self.assertEqual(_parse_exact_command("door rear lock"), [])
def test_climate_cycle_uses_verified_1de_bits_and_404_feedback(self) -> None:
cases = (
("internal", "00 00 08 00 00 00 00 00"),
("external", "00 00 10 00 00 00 00 00"),
)
for mode, payload in cases:
with self.subTest(mode=mode):
command = self.compile_action(
"climate",
"climate.set_cycle_mode",
"vehicle",
{"mode": mode},
)
self.assertEqual(command["signals"]["frame_id"], "0x1DE")
self.assertEqual(command["signals"]["data_hex"], payload)
self.assertEqual(command["repeat"], {"count": 3, "period_ms": 100})
self.assertEqual(command["signals"]["feedback_ids"], "0x404")
self.assertEqual(
command["signals"]["expected_feedback"],
f"climate_cycle:{mode}",
)
def test_ambient_controls_use_only_verified_3fc_layouts(self) -> None:
cases = (
@@ -426,6 +462,10 @@ class VerifiedBodyCanTests(unittest.TestCase):
self.assertFalse(command["signals"]["is_fd"])
self.assertEqual(command["signals"]["feedback_ids"], "0x41A")
self.assertEqual(command["signals"]["special_handler"], "shade_preset")
self.assertEqual(
command["signals"]["debug_command"],
f"shade position {position}",
)
arbitrary = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 37}
@@ -433,10 +473,35 @@ class VerifiedBodyCanTests(unittest.TestCase):
self.assertEqual(arbitrary["signals"]["frame_id"], "0x431")
self.assertNotEqual(arbitrary["signals"]["frame_id"], "0x4F5")
self.assertEqual(arbitrary["signals"]["special_handler"], "shade_position")
self.assertEqual(arbitrary["signals"]["debug_command"], "shade position 37")
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_executor_attaches_compiled_debug_command_to_mqtt_style_bus(self) -> None:
config = self.runtime.build_function_config(
"roof",
[
{
"domain": "roof",
"capability": "roof.set_shade_position",
"target": "vehicle",
"operation": "set",
"parameters": {"position_pct": 100},
}
],
"shade-debug-command",
"local_rules",
)
plan = self.runtime.compile(config)
bus = _FakeBus([self.shade_feedback(100, motion=1)])
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
report = executor.execute(self.runtime, plan, config)
self.assertEqual(report["spec"]["status"], "succeeded")
self.assertEqual(bus.debug_commands, ["shade position 100"])
def test_shade_preset_executor_waits_for_authoritative_target(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 100}