feat(n7web): enhance control workflows and status feedback
This commit is contained in:
@@ -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
@@ -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("*")
|
||||
|
||||
Reference in New Issue
Block a user