feat: complete dual-seat whole-vehicle orchestration

This commit is contained in:
vee
2026-08-12 15:12:40 +08:00
parent 32379a1573
commit 9ce64b3a01
62 changed files with 9741 additions and 3950 deletions
+130 -25
View File
@@ -14,6 +14,7 @@ from unittest.mock import patch
from cognitive_agent.auth import AuthConfig, AuthRuntime, hash_password
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.self_extension import (
SelfExtensionService,
SimulationGateway,
@@ -115,13 +116,71 @@ class SeatWebApiTests(unittest.TestCase):
unauthenticated.open(f"{self.base_url}/api/runtime", timeout=3)
self.assertEqual(caught.exception.code, 401)
def test_user_mode_delete_endpoint_removes_only_user_profiles(self) -> None:
original_registry = self.service.mode_profiles
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
preset_path = root / "common" / "preset" / "office.yaml"
preset_path.parent.mkdir(parents=True)
preset_path.write_text(
"""
id: office
name: 办公模式
operations:
- domain: climate
capability: climate.set_temperature
target_scope: vehicle
parameters:
temperature_c: 24
""".strip()
+ "\n",
encoding="utf-8",
)
registry = ModeProfileRegistry(root)
user_mode = registry.save_mode(
"K歌模式",
[
{
"domain": "ambient_light",
"capability": "ambient_light.set_power",
"target_scope": "vehicle",
"parameters": {"state": "on"},
}
],
vehicle_id="denza_n7",
)
self.service.mode_profiles = registry
try:
request = urllib.request.Request(
f"{self.base_url}/api/mode-profiles/{user_mode.id}?vehicle_id=denza_n7",
method="DELETE",
)
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["deleted_mode_profile"]["name"], "K歌模式")
self.assertFalse(Path(user_mode.source).exists())
protected = urllib.request.Request(
f"{self.base_url}/api/mode-profiles/office?vehicle_id=denza_n7",
method="DELETE",
)
with self.assertRaises(urllib.error.HTTPError) as caught:
self.opener.open(protected, timeout=3)
self.assertEqual(caught.exception.code, 403)
error = json.loads(caught.exception.read().decode("utf-8"))
self.assertEqual(error["error"], "preset_mode_protected")
self.assertTrue(preset_path.is_file())
finally:
self.service.mode_profiles = original_registry
def test_static_console_and_runtime_endpoint(self) -> None:
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8")
with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-51", timeout=3) as response:
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-67", timeout=3) as response:
app_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/gesture-control.js?v=android-pad-6", timeout=3
f"{self.base_url}/gesture-control.js?v=android-pad-7", timeout=3
) as response:
gesture_script = response.read().decode("utf-8")
with self.opener.open(
@@ -129,15 +188,15 @@ class SeatWebApiTests(unittest.TestCase):
) as response:
console_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.css?v=desktop-refactor-68", timeout=3
f"{self.base_url}/console-v2.css?v=desktop-refactor-87", timeout=3
) as response:
console_styles = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/self-extension.js?v=extension-15", timeout=3
f"{self.base_url}/self-extension.js?v=extension-18", timeout=3
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/self-extension.css?v=extension-9", timeout=3
f"{self.base_url}/self-extension.css?v=extension-12", timeout=3
) as response:
extension_styles = response.read().decode("utf-8")
with self.opener.open(
@@ -160,13 +219,14 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
self.assertNotIn("透明车体 · 实体设备", html)
self.assertIn('src="/app.js?v=seat-hil-51"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-6"', html)
self.assertIn('src="/app.js?v=dual-seat-67"', html)
self.assertIn('href="/styles.css?v=dual-seat-9"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-7"', html)
self.assertNotIn('class="vehicle-system-hud"', html)
self.assertIn('src="/console-v2.js?v=desktop-refactor-28"', html)
self.assertIn('href="/console-v2.css?v=desktop-refactor-68"', html)
self.assertIn('src="/self-extension.js?v=extension-15"', html)
self.assertIn('href="/self-extension.css?v=extension-9"', html)
self.assertIn('href="/console-v2.css?v=desktop-refactor-87"', html)
self.assertIn('src="/self-extension.js?v=extension-18"', html)
self.assertIn('href="/self-extension.css?v=extension-12"', html)
self.assertNotIn("滚动查看更多", html)
self.assertIn('aria-label="执行车舱控制指令"', html)
self.assertIn('sessionStorage.getItem("cognitive-agent.demo-role")', console_script)
@@ -248,9 +308,9 @@ class SeatWebApiTests(unittest.TestCase):
app_script,
)
self.assertIn("hardwareButton.disabled = !hardwareExecutionReady", app_script)
self.assertIn('const seat = driverOnly ? "driver" : "passenger"', app_script)
self.assertIn('data-mode-command="seat driver vent low"', app_script)
self.assertIn("CH1 主驾座椅与车身设备物理反馈联合展示", app_script)
self.assertIn('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.assertIn("车端 CAN 未启用,其他领域保持虚拟执行", app_script)
self.assertIn("function extractReferenceWindowPanes", app_script)
@@ -298,7 +358,9 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn('--v2-accent: #0e7773', extension_styles)
self.assertIn('.v2-sidebar-collapsed[data-module="self-extension"] .v2-extension-sidebar', extension_styles)
self.assertIn('.v2-extension-capability span {', extension_styles)
self.assertIn('.v2-extension-vehicle-card > div button {', extension_styles)
self.assertIn('.v2-extension-parameter-list > span {', extension_styles)
self.assertIn('.v2-extension-vehicle-card > footer {', extension_styles)
self.assertIn('.v2-extension-twin-host > .twin-band {', extension_styles)
self.assertIn('.v2-extension-type-card[data-extension-connected="true"]', extension_styles)
self.assertIn('.v2-extension-type-card[data-extension-connected="false"]', extension_styles)
self.assertIn('grid-template-columns: repeat(3, minmax(0, 1fr))', extension_styles)
@@ -340,11 +402,50 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn('id="presetIndicator"', html)
self.assertIn('id="vehicleSelect"', html)
self.assertIn('id="saveModeToggle"', html)
self.assertIn("根据指令创建", html)
self.assertIn('aria-label="快捷功能模式"', html)
self.assertIn('id="modeRailPrev"', html)
self.assertIn('id="modeRailNext"', html)
self.assertIn('id="modeGalleryToggle"', html)
self.assertIn('id="modeGallery"', html)
self.assertIn('id="modeGalleryGroups"', html)
self.assertIn("全部功能模式", html)
self.assertIn('id="saveModeSource"', html)
self.assertNotIn("主驾 24℃", html)
populate_modes = app_script[
app_script.index("function populateModeChips()")
: app_script.index("function setModeExecutionState")
]
self.assertNotIn("ch1_body_can_driver_only", populate_modes)
self.assertIn("function populateModeGallery", app_script)
self.assertIn("function updateModeRailNavigation", app_script)
self.assertIn("function scrollModeRail", app_script)
self.assertIn('className = "mode-gallery-card"', app_script)
self.assertIn("#modeGallery button[data-mode-profile-id]", app_script)
self.assertIn("function setModeExecutionState", app_script)
self.assertIn("function requestCommandAndWait", app_script)
self.assertIn("已创建并添加到功能模式栏", app_script)
self.assertLess(
app_script.index('id: "personal"'),
app_script.index('id: "frequent"'),
)
self.assertIn('button.dataset.modeOrigin === "user"', app_script)
self.assertIn('userDivider.dataset.modeDivider = "user"', app_script)
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('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.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)
@@ -435,15 +536,18 @@ class SeatWebApiTests(unittest.TestCase):
def test_self_extension_vehicle_success_contract_is_strict(self) -> None:
with self.opener.open(
f"{self.base_url}/self-extension.js?v=extension-15", timeout=3
f"{self.base_url}/self-extension.js?v=extension-18", timeout=3
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/app.js?v=seat-hil-51", timeout=3
f"{self.base_url}/app.js?v=dual-seat-67", timeout=3
) as response:
app_script = response.read().decode("utf-8")
self.assertIn('code: "SEAT · CH1 BODY CAN"', extension_script)
self.assertIn('code: "SEAT · CH0 T2G CAN"', extension_script)
self.assertIn('id: "passenger-seat"', extension_script)
self.assertIn('parameters: ["通风 0–3 档", "加热 0–2 档"', extension_script)
self.assertIn('command: "seat driver vent low"', extension_script)
self.assertIn('command: "seat driver vent high"', extension_script)
self.assertIn('command: "seat driver vent off"', extension_script)
@@ -452,8 +556,7 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn('command: "light indoor off"', extension_script)
self.assertIn('command: "light reading all off"', extension_script)
self.assertIn('command: "light ambient off"', extension_script)
self.assertIn('command: "window front-left position 50"', extension_script)
self.assertIn('command: "window front-left position 0"', extension_script)
self.assertIn('command: `window ${positionName} position ${position}`', extension_script)
self.assertNotIn('command: "shade stop"', extension_script)
self.assertNotIn('command: "door vehicle lock"', extension_script)
self.assertNotIn('command: "door vehicle unlock"', extension_script)
@@ -476,6 +579,10 @@ class SeatWebApiTests(unittest.TestCase):
)
self.assertIn('window.addEventListener("cognitive-agent:result"', extension_script)
self.assertIn('id="extensionVehicleStateSummary"', extension_script)
self.assertIn('id="extensionTwinHost"', extension_script)
self.assertIn('data-extension-current-action', extension_script)
self.assertIn('function syncSharedTwinMount()', extension_script)
self.assertIn('function updateVehicleCardExecution(', extension_script)
self.assertNotIn("/api/hardware/seat-state", extension_script)
self.assertIn(
'window.addEventListener("cognitive-agent:extension-vehicle-result"',
@@ -487,13 +594,11 @@ class SeatWebApiTests(unittest.TestCase):
'target_policy === "ch1_body_can_driver_only"',
app_script,
)
seat_sync_body = app_script.split(
"async function synchronizePhysicalSeatState()", 1
)[1].split("async function loadAuthStatus()", 1)[0]
self.assertLess(
seat_sync_body.index('target_policy === "ch1_body_can_driver_only"'),
seat_sync_body.index("/api/hardware/seat-state"),
)
self.assertIn("async function synchronizePassengerSeatState()", app_script)
self.assertIn("/api/hardware/seat-state", app_script)
self.assertIn("async function synchronizeDriverSeatState()", app_script)
self.assertIn("/api/hardware/vehicle-state", app_script)
self.assertNotIn("passenger: mergedActuatorState.driver", app_script)
self.assertNotIn("approvalText", app_script)
def test_self_extension_vehicle_endpoint_preserves_unsuccessful_feedback(