feat(n7web): enhance control workflows and status feedback

This commit is contained in:
Miayui
2026-09-16 12:52:07 +08:00
parent 84a3cd73bb
commit 0e4328f548
45 changed files with 7317 additions and 1001 deletions
+68
View File
@@ -277,6 +277,7 @@ class FakeVehicleService:
self.calls: list[dict[str, object]] = []
self.mode_save_calls: list[dict[str, object]] = []
self.mode_trial_calls: list[dict[str, object]] = []
self.mode_scene_publish_calls: list[dict[str, object]] = []
self.fail = False
def preview_native_can(self, **kwargs: object) -> dict[str, object]:
@@ -481,6 +482,44 @@ class FakeVehicleService:
"source_request_id": source_request_id,
}
def save_mode_profile_card(
self,
mode_id: str,
name: str,
instruction: str,
source_request_id: str,
vehicle_id: str | None = None,
) -> dict[str, object]:
return {
"status": "succeeded",
"mode_profile": {
"id": mode_id,
"name": name,
"origin": "user",
"operation_count": 1,
"instruction": instruction,
},
"vehicle_id": vehicle_id,
"storage": "persistent_yaml",
"source_request_id": source_request_id,
}
def publish_saved_mode_scene(
self,
mode_id: str,
vehicle_id: str | None = None,
) -> dict[str, object]:
self.mode_scene_publish_calls.append(
{"mode_id": mode_id, "vehicle_id": vehicle_id}
)
return {
"state": "queued",
"topic": "from_pc/scenes",
"scene_id": mode_id,
"burst_count": 3,
"burst_window_ms": 1_000,
}
def trial_mode_profile_draft(
self,
name: str,
@@ -548,6 +587,7 @@ class TestCodexAssistantWebApi:
AssistantApiHandler.service.calls.clear()
AssistantApiHandler.service.mode_save_calls.clear()
AssistantApiHandler.service.mode_trial_calls.clear()
AssistantApiHandler.service.mode_scene_publish_calls.clear()
AssistantApiHandler.service.fail = False
FAKE_CODEX.available = True
FAKE_CODEX.turn_calls.clear()
@@ -1386,6 +1426,34 @@ class TestCodexAssistantWebApi:
assert saved["status"] == "succeeded"
assert saved["mode_profile"]["origin"] == "user"
assert saved["mode_profile"]["operation_count"] == 9
assert AssistantApiHandler.service.mode_scene_publish_calls == [
{"mode_id": "mode-movie", "vehicle_id": "denza_n7"}
]
assert saved["mode_scene_mqtt"] == {
"state": "queued",
"topic": "from_pc/scenes",
"scene_id": "mode-movie",
"burst_count": 3,
"burst_window_ms": 1_000,
}
update_status, _content_type, updated = self._request_json(
"/api/mode-profiles/mode-movie",
payload["mode_profile_draft"],
)
assert update_status == 200
assert updated["mode_profile"]["id"] == "mode-movie"
assert updated["mode_scene_mqtt"] == {
"state": "queued",
"topic": "from_pc/scenes",
"scene_id": "mode-movie",
"burst_count": 3,
"burst_window_ms": 1_000,
}
assert AssistantApiHandler.service.mode_scene_publish_calls == [
{"mode_id": "mode-movie", "vehicle_id": "denza_n7"},
{"mode_id": "mode-movie", "vehicle_id": "denza_n7"},
]
def test_vehicle_failure_is_recorded_and_never_overwritten_by_codex_success(self) -> None:
AssistantApiHandler.service.fail = True
+292 -143
View File
@@ -4,6 +4,7 @@ import json
import os
import tempfile
import threading
import time
import unittest
import urllib.error
import urllib.request
@@ -18,6 +19,7 @@ from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.pet_cards import PetCardService
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.application.self_extension import (
ExtensionIntegrationError,
GatewayError,
SelfExtensionService,
SimulationGateway,
@@ -453,12 +455,55 @@ operations:
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(request, timeout=3) as response:
payload = json.loads(response.read().decode("utf-8"))
updated_scenes: list[dict[str, object]] = []
updated_at: list[float] = []
update_burst_complete = threading.Event()
def capture_updated_scene(
scene: dict[str, object],
) -> dict[str, object]:
updated_scenes.append(json.loads(json.dumps(scene)))
updated_at.append(time.monotonic())
if len(updated_scenes) == 3:
update_burst_complete.set()
return {
"delivery_status": "published",
"message_id": len(updated_scenes),
}
with patch.object(
self.service._vehicle_mqtt_transport,
"publish_mode_scene",
side_effect=capture_updated_scene,
):
with self.opener.open(request, timeout=3) as response:
payload = json.loads(response.read().decode("utf-8"))
self.assertTrue(update_burst_complete.wait(timeout=2))
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.assertEqual(
payload["mode_scene_mqtt"],
{
"state": "queued",
"topic": "from_pc/scenes",
"scene_id": "office",
"burst_count": 3,
"burst_window_ms": 1_000,
},
)
expected_updated_scene = next(
scene
for scene in self.service.mode_scene_catalog("denza_n7")["scenes"]
if scene["id"] == "office"
)
self.assertEqual(len(updated_scenes), 3)
self.assertLess(updated_at[-1] - updated_at[0], 1.0)
self.assertTrue(
all(scene == expected_updated_scene for scene in updated_scenes)
)
self.assertTrue(all("scenes" not in scene for scene in updated_scenes))
self.assertTrue(
(user_root / "vehicles" / "denza_n7" / "user" / "office.yaml").is_file()
)
@@ -477,11 +522,50 @@ operations:
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(create_request, timeout=3) as response:
created = json.loads(response.read().decode("utf-8"))
published_scenes: list[dict[str, object]] = []
published_at: list[float] = []
burst_complete = threading.Event()
def capture_scene(scene: dict[str, object]) -> dict[str, object]:
published_scenes.append(json.loads(json.dumps(scene)))
published_at.append(time.monotonic())
if len(published_scenes) == 3:
burst_complete.set()
return {
"delivery_status": "published",
"message_id": len(published_scenes),
}
with patch.object(
self.service._vehicle_mqtt_transport,
"publish_mode_scene",
side_effect=capture_scene,
):
with self.opener.open(create_request, timeout=3) as response:
created = json.loads(response.read().decode("utf-8"))
self.assertTrue(burst_complete.wait(timeout=2))
self.assertEqual(response.status, 201)
self.assertEqual(created["mode_profile"]["origin"], "user")
self.assertEqual(created["mode_profile"]["name"], "通勤模式")
self.assertEqual(
created["mode_scene_mqtt"],
{
"state": "queued",
"topic": "from_pc/scenes",
"scene_id": created["mode_profile"]["id"],
"burst_count": 3,
"burst_window_ms": 1_000,
},
)
expected_scene = next(
scene
for scene in self.service.mode_scene_catalog("denza_n7")["scenes"]
if scene["id"] == created["mode_profile"]["id"]
)
self.assertEqual(len(published_scenes), 3)
self.assertLess(published_at[-1] - published_at[0], 1.0)
self.assertTrue(all(scene == expected_scene for scene in published_scenes))
self.assertTrue(all("scenes" not in scene for scene in published_scenes))
self.assertTrue(
(
user_root
@@ -575,7 +659,7 @@ operations:
snapshot=lambda: {
"revision": 1,
"reader": {"online": True},
"card": {"action": "present", "uid": "2CC8B103"},
"card": None,
}
)
try:
@@ -629,7 +713,17 @@ operations:
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(save_request, timeout=3) as response:
with patch.object(
self.service,
"publish_saved_mode_scene",
return_value={
"state": "queued",
"topic": "from_pc/scenes",
"scene_id": "office",
"burst_count": 3,
"burst_window_ms": 1_000,
},
), self.opener.open(save_request, timeout=3) as response:
saved = json.loads(response.read().decode("utf-8"))
execute_request = urllib.request.Request(
@@ -721,14 +815,35 @@ operations:
handler = object.__new__(SeatDemoHandler)
handler.extension = extension
handler.nfc = SimpleNamespace(
snapshot=lambda: {
"card": {"action": "present", "uid": "2CC8B103"}
}
snapshot=lambda: {"card": None}
)
profile = SimpleNamespace(
command_flow={
"function_configs": {
"local_self_extension": {
"spec": {
"domain": "self_extension",
"operations": [
{
"capability": "infrared.emit",
"target": {
"device": "tony-light-001",
"card_uid": "2CC8B103",
},
"parameters": {"action": "打开", "slot": 1},
}
],
}
}
}
}
)
entries = handler._saved_mode_infrared_entries(profile)
missing_binding = SimpleNamespace(
command_flow={
"function_configs": {
"self_extension": {
"spec": {
"domain": "self_extension",
"operations": [
@@ -743,8 +858,11 @@ operations:
}
}
)
entries = handler._saved_mode_infrared_entries(profile)
with self.assertRaisesRegex(
ExtensionIntegrationError,
"缺少持久 NFC 绑定",
):
handler._saved_mode_infrared_entries(missing_binding)
self.assertEqual([item["capability"]["slot"] for item in entries], [1])
self.assertEqual(
@@ -807,14 +925,21 @@ 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-140", timeout=3) as response:
web_console_root = REPOSITORY_ROOT / "apps" / "web-console"
template = (web_console_root / "src" / "App.vue").read_text(
encoding="utf-8"
)
vite_entry = (web_console_root / "src" / "main.js").read_text(
encoding="utf-8"
)
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-141", 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-9", timeout=3
) as response:
gesture_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.js?v=local-first-70", timeout=3
f"{self.base_url}/console-v2.js?v=local-first-76", timeout=3
) as response:
console_script = response.read().decode("utf-8")
with self.opener.open(
@@ -842,7 +967,7 @@ operations:
) as response:
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
with self.opener.open(
f"{self.base_url}/assistant-chat.js?v=codex-assistant-33", timeout=3
f"{self.base_url}/assistant-chat.js?v=codex-assistant-34", timeout=3
) as response:
assistant_script = response.read().decode("utf-8")
with self.opener.open(
@@ -854,7 +979,7 @@ operations:
) as response:
energy_styles = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/codepen-glass-theme.css?v=creative-cloud-2", timeout=3
f"{self.base_url}/codepen-glass-theme.css?v=creative-cloud-5", timeout=3
) as response:
glass_theme = response.read().decode("utf-8")
with self.opener.open(
@@ -869,50 +994,57 @@ operations:
f"{self.base_url}/api/extension/nfc/status", timeout=3
) as response:
nfc_status = json.loads(response.read().decode("utf-8"))
with self.opener.open(
f"{self.base_url}/api/extension/control/status"
"?after_revision=-1&timeout_ms=0",
timeout=3,
) as response:
extension_control_status = json.loads(response.read().decode("utf-8"))
with self.opener.open(f"{self.base_url}/api/runtime", timeout=3) as response:
runtime = json.loads(response.read().decode("utf-8"))
with self.opener.open(f"{self.base_url}/api/speech/status", timeout=3) as response:
speech_status = json.loads(response.read().decode("utf-8"))
self.assertIn("511工程-智能座舱", html)
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('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=dual-seat-140"', html)
self.assertIn('href="/styles.css?v=dual-seat-23"', 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=local-first-70"', html)
self.assertIn('href="/console-v2.css?v=delivery-state-69"', html)
self.assertIn('id="seatTwinCanvas"', template)
self.assertIn('data-vehicle-model="byd-tang-2024-reference"', template)
self.assertIn("整车孪生界面", template)
self.assertIn("<strong>车内灯光</strong>", template)
self.assertIn('id="interiorLightPower"', template)
self.assertIn('id="ambientBrightness"', template)
self.assertIn('id="ambientColorIndex"', template)
self.assertIn('data-function="massage"', template)
self.assertIn("CMD 0x1DE · FB 0x404 · CH1", template)
self.assertIn("CMD 0x431 · FB 0x41A · CH1", template)
self.assertIn("CMD 0x431 · FB 0x388 · CH1", template)
self.assertNotIn("透明车体 · 实体设备", template)
self.assertIn('src="/app.js?v=dual-seat-141"', html)
self.assertIn('import "../styles.css";', vite_entry)
self.assertNotIn('class="vehicle-system-hud"', template)
self.assertIn('src="/console-v2.js?v=local-first-76"', html)
self.assertIn('import "../console-v2.css";', vite_entry)
self.assertIn('src="/speech-input.js?v=sensevoice-13"', html)
self.assertIn('href="/speech-input.css?v=sensevoice-7"', html)
self.assertIn('id="speechDictationButton"', html)
self.assertIn('class="speech-waveform"', html)
self.assertIn('import "../speech-input.css";', vite_entry)
self.assertIn('id="speechDictationButton"', template)
self.assertIn('class="speech-waveform"', template)
self.assertIn('<body class="console-v2">', html)
self.assertIn('id="v2BootScreen"', html)
self.assertIn('正在加载车机控制与实时状态', html)
self.assertIn('id="v2BootScreen"', template)
self.assertIn('正在加载车机控制与实时状态', template)
self.assertIn('src="/self-extension.js?v=pet-settings-52"', html)
self.assertIn('href="/self-extension.css?v=pet-settings-52"', html)
self.assertNotIn("self-calibration", html)
self.assertNotIn("self-repair", html)
self.assertNotIn("self-evolution", html)
self.assertNotIn("滚动查看更多", html)
self.assertIn('aria-label="发送消息"', html)
self.assertIn('id="assistantTranscript"', html)
self.assertIn('href="/assistant-chat.css?v=codex-assistant-29"', html)
self.assertIn('src="/assistant-chat.js?v=codex-assistant-33"', html)
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
self.assertIn('import "../self-extension.css";', vite_entry)
self.assertNotIn("self-calibration", template)
self.assertNotIn("self-repair", template)
self.assertNotIn("self-evolution", template)
self.assertNotIn("滚动查看更多", template)
self.assertIn('aria-label="发送消息"', template)
self.assertIn('id="assistantTranscript"', template)
self.assertIn('import "../assistant-chat.css";', vite_entry)
self.assertIn('src="/assistant-chat.js?v=codex-assistant-34"', html)
self.assertIn('import "../energy-monitor.css";', vite_entry)
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
self.assertIn('href="/codepen-glass-theme.css?v=creative-cloud-2"', html)
self.assertIn('import "../codepen-glass-theme.css";', vite_entry)
self.assertEqual(extension_control_status["source_label"], "车端 MQTT")
self.assertIn("action_statuses", extension_control_status)
self.assertIn("--glass-shell:", glass_theme)
self.assertIn("backdrop-filter: blur(22px) saturate(1.25)", glass_theme)
self.assertNotIn('"admin"', console_script)
@@ -984,7 +1116,7 @@ operations:
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('class="mode-option active" id="hardwareMode"', template)
self.assertIn('grid-template-columns: clamp(245px, 18.5vw, 310px)', console_styles)
self.assertNotIn('.v2-control-switch {', console_styles)
self.assertIn('.v2-module-switcher {', console_styles)
@@ -1036,12 +1168,12 @@ operations:
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)
self.assertNotIn('id="requestHardwareAccess"', html)
self.assertNotIn('id="releaseHardwareAccess"', html)
self.assertNotIn('id="approvalField"', html)
self.assertNotIn('id="approvalToken"', html)
self.assertNotIn('id="hardwareAccessField"', template)
self.assertNotIn('id="hardwareClientLabel"', template)
self.assertNotIn('id="requestHardwareAccess"', template)
self.assertNotIn('id="releaseHardwareAccess"', template)
self.assertNotIn('id="approvalField"', template)
self.assertNotIn('id="approvalToken"', template)
self.assertNotIn("approval_token: approvalToken", app_script)
self.assertNotIn("授权并真实执行", app_script)
self.assertIn('const dynamicOrder = new Map([["rocking_horse", 0]', app_script)
@@ -1052,19 +1184,19 @@ operations:
'if (mode === "hardware" && !hardwareExecutionReady) return false',
app_script,
)
self.assertIn('data-control-transport="wired_can"', html)
self.assertIn('data-control-transport="wireless_mqtt"', html)
self.assertIn('<div class="mode-rail" aria-label="功能模式">', html)
self.assertIn('id="modeRailToggle"', html)
self.assertIn('id="modeRailContent"', html)
self.assertIn('role="region"', html)
self.assertIn('aria-label="功能模式列表"', html)
self.assertIn('<span>AI 新建</span>', html)
self.assertIn('class="mode-rail-identity"', html)
self.assertIn('aria-expanded="true"', html)
self.assertIn('模拟下发', html)
self.assertIn('真实下发', html)
self.assertNotIn('id="simulationMode"', html)
self.assertIn('data-control-transport="wired_can"', template)
self.assertIn('data-control-transport="wireless_mqtt"', template)
self.assertIn('<div class="mode-rail" aria-label="功能模式">', template)
self.assertIn('id="modeRailToggle"', template)
self.assertIn('id="modeRailContent"', template)
self.assertIn('role="region"', template)
self.assertIn('aria-label="功能模式列表"', template)
self.assertIn('<span>AI 新建</span>', template)
self.assertIn('class="mode-rail-identity"', template)
self.assertIn('aria-expanded="true"', template)
self.assertIn('模拟下发', template)
self.assertIn('真实下发', template)
self.assertNotIn('id="simulationMode"', template)
self.assertIn('control_transport: state.controlTransport', app_script)
self.assertIn('button.dataset.connectionState = connectionState', app_script)
self.assertIn('信号链路${connectionState === "online" ? "正常" : "断开"}', app_script)
@@ -1097,16 +1229,16 @@ operations:
)
self.assertNotIn(
'id="hardwareMode" data-execution-mode="hardware" disabled',
html,
template,
)
self.assertIn('const seat = twin.selected === "driver" ? "driver" : "passenger"', app_script)
self.assertNotIn('data-mode-command="seat driver vent low"', app_script)
self.assertIn("CH0 副驾座椅与 CH1 主驾座椅独立收发、独立反馈", app_script)
self.assertIn("CH1 主驾实车测试不启用无闭环的座椅位置手势", app_script)
self.assertNotIn("车端 CAN 未启用,其他领域保持虚拟执行", app_script)
self.assertNotIn('id="hardwareNote"', html)
self.assertNotIn('id="hardwareAliasNote"', html)
self.assertNotIn('id="vehicleTransportNote"', html)
self.assertNotIn('id="hardwareNote"', template)
self.assertNotIn('id="hardwareAliasNote"', template)
self.assertNotIn('id="vehicleTransportNote"', template)
self.assertIn("下发控制指令", console_script)
self.assertIn('new Set(["mode_editor_compile"])', console_script)
self.assertIn("function extractReferenceWindowPanes", app_script)
@@ -1198,14 +1330,17 @@ operations:
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v305"', service_worker)
self.assertIn(
'const CACHE_VERSION = "cognitive-agent-pad-v315-vue-vite"',
service_worker,
)
self.assertIn('/console-v2.css?v=delivery-state-69', service_worker)
self.assertIn('/console-v2.js?v=local-first-70', service_worker)
self.assertIn('/console-v2.js?v=local-first-76', service_worker)
self.assertIn('/assistant-chat.css?v=codex-assistant-29', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-33', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-34', service_worker)
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
self.assertIn('"/app.js?v=dual-seat-140"', service_worker)
self.assertIn('"/app.js?v=dual-seat-141"', service_worker)
self.assertIn('"/speech-input.js?v=sensevoice-13"', service_worker)
self.assertIn('"/speech-input.css?v=sensevoice-7"', service_worker)
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-5"', service_worker)
@@ -1264,39 +1399,42 @@ operations:
self.assertIn('error.code === "hardware_busy"', app_script)
self.assertIn("BENCH BUSY", app_script)
self.assertIn("FBXLoader.js", html)
self.assertIn('data-seat-layout="five-seat"', html)
self.assertIn('data-seat-layout="five-seat"', template)
self.assertIn('data-active-seat="front-right"', html)
self.assertNotIn('placeholder="请输入服务端配置令牌"', html)
self.assertNotIn('class="twin-callout-layer"', html)
self.assertNotIn('class="twin-callout', html)
self.assertNotIn('data-callout=', html)
self.assertIn('data-twin-function="ventilation"', html)
self.assertIn('data-twin-function="heating"', html)
self.assertIn('class="twin-level-meter"', html)
self.assertIn('id="presetIndicator"', html)
self.assertIn('id="vehicleSelect"', html)
self.assertIn('id="saveModeToggle"', html)
self.assertIn("AI 新建", html)
self.assertIn('aria-label="快捷功能模式,可横向滚动"', html)
self.assertIn('id="modeRailContent"', html)
self.assertIn('aria-label="功能模式列表"', html)
self.assertNotIn('id="modeRailPrev"', html)
self.assertNotIn('id="modeRailNext"', html)
self.assertGreater(html.index('id="saveModeToggle"'), html.index('id="modeRailContent"'))
self.assertIn('id="modeGalleryToggle"', html)
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.assertNotIn('id="saveModeSource"', html)
self.assertNotIn("主驾 24℃", html)
self.assertIn('data-active-seat="front-right"', template)
self.assertNotIn('placeholder="请输入服务端配置令牌"', template)
self.assertNotIn('class="twin-callout-layer"', template)
self.assertNotIn('class="twin-callout', template)
self.assertNotIn('data-callout=', template)
self.assertIn('data-twin-function="ventilation"', template)
self.assertIn('data-twin-function="heating"', template)
self.assertIn('class="twin-level-meter"', template)
self.assertIn('id="presetIndicator"', template)
self.assertIn('id="vehicleSelect"', template)
self.assertIn('id="saveModeToggle"', template)
self.assertIn("AI 新建", template)
self.assertIn('aria-label="快捷功能模式,可横向滚动"', template)
self.assertIn('id="modeRailContent"', template)
self.assertIn('aria-label="功能模式列表"', template)
self.assertNotIn('id="modeRailPrev"', template)
self.assertNotIn('id="modeRailNext"', template)
self.assertGreater(
template.index('id="saveModeToggle"'),
template.index('id="modeRailContent"'),
)
self.assertIn('id="modeGalleryToggle"', template)
self.assertIn('id="modeGallery"', template)
self.assertIn('id="modeGalleryGroups"', template)
self.assertIn("管理功能模式", template)
self.assertIn('id="modeEditorBackdrop"', template)
self.assertIn('id="modeEditorName"', template)
self.assertIn('id="modeEditorInstruction"', template)
self.assertIn('id="modeEditorExecute"', template)
self.assertIn('id="modeEditorComplete"', template)
self.assertIn('id="modeEditorFlowPreview"', template)
self.assertIn('class="mode-editor-flow-preview empty"', template)
self.assertNotIn('id="saveModeSource"', template)
self.assertNotIn("主驾 24℃", template)
populate_modes = app_script[
app_script.index("function populateModeChips()")
: app_script.index("function setModeExecutionState")
@@ -1345,21 +1483,21 @@ operations:
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)
self.assertIn('data-domain-panel="window"', html)
self.assertIn('data-domain-panel="front_light"', html)
self.assertIn('data-domain-panel="vehicle_status"', html)
self.assertIn('id="vehicleGear"', html)
self.assertIn('id="vehicleSpeed"', html)
self.assertNotIn('data-domain-panel="control_link"', html)
self.assertIn('id="cabinStateTitle">整车设备状态</h3>', html)
self.assertIn('data-domain-panel="climate"', template)
self.assertIn('data-domain-panel="roof"', template)
self.assertIn('data-domain-panel="ambient_light"', template)
self.assertIn('data-domain-panel="window"', template)
self.assertIn('data-domain-panel="front_light"', template)
self.assertIn('data-domain-panel="vehicle_status"', template)
self.assertIn('id="vehicleGear"', template)
self.assertIn('id="vehicleSpeed"', template)
self.assertNotIn('data-domain-panel="control_link"', template)
self.assertIn('id="cabinStateTitle">整车设备状态</h3>', template)
self.assertIn('id="cabinCarouselPrev"', app_script)
self.assertIn('id="cabinCarouselNext"', app_script)
self.assertIn('aria-label="左右浏览设备卡片"', app_script)
self.assertNotIn('class="cabin-category-tabs"', app_script)
self.assertNotIn('class="section-kicker"', html)
self.assertNotIn('class="section-kicker"', template)
self.assertIn('.joint-twin-shell > .twin-band {', console_styles)
self.assertIn('grid-template-rows: 62px minmax(0, 1fr)', console_styles)
self.assertIn('grid-template-columns: repeat(2, minmax(0, 1fr))', console_styles)
@@ -1394,18 +1532,18 @@ operations:
self.assertIn('name: "遮阳帘", measurable: true, components: []', app_script)
self.assertIn("仅支持设备总功率", app_script)
self.assertIn('.device-power-drawer {', console_styles)
self.assertNotIn('class="roof-visual"', html)
self.assertNotIn('class="ambient-visual"', html)
self.assertIn('id="driverAirDirection"', html)
self.assertIn('id="passengerAirDirection"', html)
self.assertNotIn('id="transmittanceValue"', html)
self.assertNotIn('id="roofMotorState"', html)
self.assertIn('id="ambientBrightness"', 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.assertNotIn('class="roof-visual"', template)
self.assertNotIn('class="ambient-visual"', template)
self.assertIn('id="driverAirDirection"', template)
self.assertIn('id="passengerAirDirection"', template)
self.assertNotIn('id="transmittanceValue"', template)
self.assertNotIn('id="roofMotorState"', template)
self.assertIn('id="ambientBrightness"', template)
self.assertNotIn('class="example-group"', template)
self.assertNotIn('id="safetyStrip"', template)
self.assertNotIn('class="pipeline"', template)
self.assertNotIn('class="topbar"', template)
self.assertNotIn('data-axis="height"', template)
self.assertEqual(runtime["domain"], "cabin")
self.assertEqual(
runtime["hardware_execution_allowed"],
@@ -1662,7 +1800,7 @@ operations:
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/app.js?v=dual-seat-140", timeout=3
f"{self.base_url}/app.js?v=dual-seat-141", timeout=3
) as response:
app_script = response.read().decode("utf-8")
@@ -1946,7 +2084,7 @@ operations:
finally:
SeatDemoHandler.extension = original_extension
def test_main_request_routes_live_nfc_peripheral_to_gscu(self) -> None:
def test_main_request_routes_durable_nfc_peripheral_after_card_removal(self) -> None:
original_extension = SeatDemoHandler.extension
original_nfc = SeatDemoHandler.nfc
SeatDemoHandler.extension = SelfExtensionService(
@@ -1959,7 +2097,7 @@ operations:
snapshot=lambda: {
"revision": 1,
"reader": {"online": True},
"card": {"action": "present", "uid": "1A53DD95"},
"card": None,
}
)
catalog_request = urllib.request.Request(
@@ -2021,7 +2159,7 @@ operations:
self.assertEqual(operation["domain"], "self_extension")
self.assertTrue(operation["telemetry"]["gateway_acknowledged"])
def test_mode_creation_cannot_be_claimed_by_live_nfc_peripheral(self) -> None:
def test_mode_creation_cannot_be_claimed_by_bound_nfc_peripheral(self) -> None:
original_extension = SeatDemoHandler.extension
original_nfc = SeatDemoHandler.nfc
with tempfile.TemporaryDirectory() as directory:
@@ -2050,7 +2188,7 @@ operations:
nfc_snapshot = {
"revision": 1,
"reader": {"online": True},
"card": {"action": "present", "uid": "2CC8B103"},
"card": None,
}
utterance = "创建一个打开闪灯玩具模式"
partition = partition_active_peripheral_intent(
@@ -2352,14 +2490,20 @@ operations:
]
self.assertTrue(validated_payloads)
self.assertTrue(
all(
any(
{"native_climate", "self_extension"}.issubset(
payload["function_configs"]
)
for payload in validated_payloads
)
)
first_validated = validated_payloads[0]
first_validated = next(
payload
for payload in validated_payloads
if {"native_climate", "self_extension"}.issubset(
payload["function_configs"]
)
)
self.assertTrue(
{"native_climate", "self_extension"}.issubset(
first_validated["command_plans"]
@@ -2605,11 +2749,11 @@ operations:
self.assertEqual(events[-1]["type"], "error")
self.assertEqual(events[-1]["error"], "can_transport_failed")
validated = next(
validated = [
item["payload"]
for item in events
if item["type"] == "validated_commands"
)
][-1]
self.assertEqual(validated["status"], "processing")
self.assertEqual(
set(validated["function_configs"]),
@@ -3620,16 +3764,21 @@ operations:
"provider": "local",
"conversation_id": "conv-e2e-hardware-locked",
"execution_mode": "hardware",
"hardware_client_id": "hwc-e2e-locked-client",
"approval_token": "must-not-be-persisted",
}
).encode("utf-8"),
)
with self.assertRaises(urllib.error.HTTPError) as caught:
with patch.object(
SeatDemoHandler,
"_hardware_access_bypass_allowed",
return_value=False,
), self.assertRaises(urllib.error.HTTPError) as caught:
self.opener.open(request, timeout=3)
self.assertEqual(caught.exception.code, 403)
payload = json.loads(caught.exception.read().decode("utf-8"))
self.assertEqual(payload["error"], "hardware_safety_gate")
self.assertEqual(payload["error"], "hardware_access_required")
artifact_text = "\n".join(
path.read_text(encoding="utf-8")
for path in Path(self.temporary_directory.name).rglob("*")