Files
Cognitive_Agent/tests/e2e/test_seat_web_api.py
T

1364 lines
65 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import json
import os
import tempfile
import threading
import unittest
import urllib.error
import urllib.request
from http.cookiejar import CookieJar
from http.server import ThreadingHTTPServer
from pathlib import Path
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,
)
from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentError
from cognitive_agent.infrastructure.can.access import HardwareAccessController
from cognitive_agent.server import SeatDemoHandler
class SeatWebApiTests(unittest.TestCase):
username = "test-user"
password = "public-demo-test-password"
@classmethod
def setUpClass(cls) -> None:
cls.temporary_directory = tempfile.TemporaryDirectory()
# Web regression tests must never inherit a developer's live bench
# enablement, especially when local debug mode intentionally skips the
# per-request approval token.
with patch.dict(
os.environ,
{
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
},
clear=False,
):
service = SeatDemoService(REPOSITORY_ROOT)
service.artifact_root = Path(cls.temporary_directory.name)
cls.service = service
cls.original_service = SeatDemoHandler.service
SeatDemoHandler.service = service
cls.original_device_token = os.environ.get("COGNITIVE_AGENT_DEVICE_API_TOKEN")
os.environ["COGNITIVE_AGENT_DEVICE_API_TOKEN"] = "test-device-api-token"
cls.original_auth = SeatDemoHandler.auth
SeatDemoHandler.auth = AuthRuntime(
AuthConfig(
enabled=True,
username=cls.username,
password_hash=hash_password(
cls.password,
salt=b"0123456789abcdef",
iterations=100_000,
),
session_secret=b"t" * 64,
session_ttl_seconds=3600,
login_attempts=5,
login_window_seconds=300,
requests_per_minute=120,
trust_proxy_headers=False,
)
)
cls.server = ThreadingHTTPServer(("127.0.0.1", 0), SeatDemoHandler)
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
cls.thread.start()
cls.base_url = f"http://127.0.0.1:{cls.server.server_port}"
@classmethod
def tearDownClass(cls) -> None:
cls.server.shutdown()
cls.server.server_close()
cls.thread.join(timeout=2)
SeatDemoHandler.service = cls.original_service
SeatDemoHandler.auth = cls.original_auth
if cls.original_device_token is None:
os.environ.pop("COGNITIVE_AGENT_DEVICE_API_TOKEN", None)
else:
os.environ["COGNITIVE_AGENT_DEVICE_API_TOKEN"] = cls.original_device_token
cls.temporary_directory.cleanup()
def setUp(self) -> None:
self.original_hardware_access = self.service.hardware_access
self.service.hardware_access = HardwareAccessController()
self.cookie_jar = CookieJar()
self.opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(self.cookie_jar)
)
request = urllib.request.Request(
f"{self.base_url}/api/auth/login",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{"username": self.username, "password": self.password}
).encode("utf-8"),
)
with self.opener.open(request, timeout=3) as response:
payload = json.loads(response.read().decode("utf-8"))
self.assertTrue(payload["authenticated"])
def tearDown(self) -> None:
self.service.hardware_access = self.original_hardware_access
def test_authentication_protects_console_and_api(self) -> None:
unauthenticated = urllib.request.build_opener()
with unauthenticated.open(f"{self.base_url}/", timeout=3) as response:
login_html = response.read().decode("utf-8")
final_url = response.geturl()
self.assertIn("/login", final_url)
self.assertIn("体验账号登录", login_html)
with unauthenticated.open(f"{self.base_url}/network.js", timeout=3) as response:
network_script = response.read().decode("utf-8")
self.assertIn("CognitiveAgentNetwork", network_script)
with self.assertRaises(urllib.error.HTTPError) as caught:
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=dual-seat-69", 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-7", timeout=3
) as response:
gesture_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.js?v=desktop-refactor-30", timeout=3
) as response:
console_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.css?v=desktop-refactor-89", timeout=3
) as response:
console_styles = response.read().decode("utf-8")
with self.opener.open(
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-12", timeout=3
) as response:
extension_styles = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/api/extension/health", timeout=3
) as response:
extension_health = 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"))
self.assertIn("Cognitive Agent", 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-69"', 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-30"', html)
self.assertIn('href="/console-v2.css?v=desktop-refactor-89"', 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)
self.assertIn('id="v2SidebarToggle"', console_script)
self.assertIn('id = "v2ModuleSwitcher"', console_script)
self.assertIn('label: "自编排", available: true', console_script)
self.assertIn('id: "self-calibration"', console_script)
self.assertIn('label: "自标定"', console_script)
self.assertIn("adminOnly: true", console_script)
self.assertIn(
"$('.v2-module-tab[data-module=\"self-calibration\"]')",
console_script,
)
self.assertIn('label: "自扩展", available: true', console_script)
self.assertIn('aria-label="自扩展面向所有登录用户"', console_script)
self.assertIn('<span>Hi,</span><strong>user</strong>', console_script)
self.assertIn('id: "self-repair"', console_script)
self.assertIn('label: "自修复"', console_script)
self.assertIn('roleSelect.disabled = ui.activeModule === "self-repair"', 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.assertIn('actionRail.append(cameraToggle)', console_script)
self.assertIn("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('userTargetText.textContent = "真实台架尚未就绪"', console_script)
self.assertIn('grid-template-columns: clamp(245px, 18.5vw, 310px)', console_styles)
self.assertIn('.v2-control-switch {', console_styles)
self.assertIn('.v2-module-switcher {', console_styles)
self.assertIn('grid-template-columns: repeat(4, minmax(0, 1fr))', console_styles)
self.assertIn('margin-inline: calc(0px - var(--v2-main-pad))', console_styles)
self.assertIn('background: color-mix(in srgb, var(--v2-accent) 30%, var(--v2-accent-soft))', console_styles)
self.assertIn('width: clamp(48px, 45%, 72px)', console_styles)
self.assertIn('font-weight: 800', console_styles)
self.assertIn('width: clamp(300px, 28vw, 360px)', console_styles)
self.assertIn('white-space: nowrap', console_styles)
self.assertIn('.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)
self.assertIn('background: var(--v2-accent);', console_styles)
self.assertIn('[data-demo-role="user"] .v2-inline-gesture #gestureArmButton svg', console_styles)
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.assertIn('.v2-edge-fade-left', console_styles)
self.assertIn('.v2-edge-fade-right', console_styles)
self.assertIn('.mode-group-divider', console_styles)
self.assertIn('.joint-twin-shell > .cabin-band', console_styles)
self.assertIn('[data-demo-role="user"] .v2-page-index', console_styles)
self.assertIn('.joint-twin-shell .twin-readout', console_styles)
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.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("approval_token: approvalToken", app_script)
self.assertNotIn("授权并真实执行", app_script)
self.assertIn('const dynamicModeOrder = ["rocking_horse", "dynamic_cradle"]', app_script)
self.assertIn('divider.className = "mode-group-divider"', app_script)
self.assertIn(
'mode === "hardware" && !state.runtime?.hardware_execution_allowed',
app_script,
)
self.assertIn("hardwareButton.disabled = !hardwareExecutionReady", 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)
self.assertIn("function collectReferenceFrontLampMeshes", app_script)
self.assertIn("function emphasizeReferenceWindowPanes", app_script)
self.assertIn("function configureActiveSeatDepthLayers", app_script)
self.assertIn("function addTwinGlassDepthMask", app_script)
self.assertIn("function applyTwinVehicleTheme", app_script)
self.assertIn("leadingMaxZ", app_script)
self.assertIn("function updateReferenceWindowOutlineLayers", app_script)
self.assertIn("vehicle.traverse((object) => object.layers.set(1))", app_script)
self.assertIn('const categoryPriority = ["body", "glass", "wheel"', app_script)
self.assertIn("function styleReferencePassiveGlass", app_script)
self.assertIn("function styleReferencePassiveRearExterior", app_script)
self.assertIn('twinRole: "reference_passive_rear_bumper"', app_script)
self.assertIn('"cognitive-agent:demo-role-change"', console_script)
self.assertIn('resultStack.id = "v2GestureResultStack"', console_script)
self.assertIn('executionSlot.id = "v2GestureExecutionSlot"', console_script)
self.assertIn('statusTarget.append(requestStatus)', console_script)
self.assertIn('root.id = "v2SelfExtension"', extension_script)
self.assertIn('data-extension-mode="study"', extension_script)
self.assertIn('data-extension-mode="control"', extension_script)
self.assertIn('外设玩具能力一览与学习', extension_script)
self.assertIn('说一句,联动外设玩具与车端设备', extension_script)
self.assertIn('id="extensionExecuteButton"', extension_script)
self.assertIn('data-lucide="play"', extension_script)
self.assertNotIn('id="extensionControlHealth"', extension_script)
self.assertIn('data-extension-connected="${Boolean(selected)}"', extension_script)
self.assertIn('<small>红外学习器</small>', extension_script)
self.assertIn('id="extensionStudyHealthState"', extension_script)
self.assertIn('id="extensionVehicleState">CHECK</span>', extension_script)
self.assertIn('id="extensionResetButton"', extension_script)
self.assertNotIn('id="extensionConnectionButton"', extension_script)
self.assertNotIn('id="extensionStatusDialog"', extension_script)
self.assertIn('/api/extension/${path}', extension_script)
self.assertIn("HEALTH_POLL_INTERVAL_MS", extension_script)
self.assertIn("HEALTH_REQUEST_TIMEOUT_MS", extension_script)
self.assertIn("healthState.listening", extension_script)
self.assertIn("healthState.peer", extension_script)
self.assertIn('healthState.message || "GSCU1B 尚未连接"', extension_script)
self.assertIn('window.addEventListener("pageshow"', extension_script)
self.assertIn('[data-module="self-extension"]', extension_styles)
self.assertIn('[data-module="self-extension"] .v2-role-select', extension_styles)
self.assertIn('.v2-extension-composer .primary-action {', extension_styles)
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-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)
self.assertTrue(extension_health["success"])
self.assertIn(extension_health["transport"], {"tcp", "mqtt", "simulation"})
self.assertIn("function emphasizeReferenceFrontLampMeshes", app_script)
self.assertIn("function createReferenceFrontLampHighlights", app_script)
self.assertIn("referenceFrontLampMaterials", app_script)
self.assertIn("targets.windowPositions", app_script)
self.assertIn("hardware_access_required", app_script)
self.assertIn('button.setAttribute("aria-label", "关闭摄像头")', gesture_script)
self.assertIn("function refreshCameraAvailability", gesture_script)
self.assertIn("navigator.mediaDevices.enumerateDevices()", gesture_script)
self.assertIn('addEventListener?.("devicechange", handleCameraDeviceChange)', gesture_script)
self.assertIn('case "NotFoundError":', gesture_script)
self.assertIn('case "NotAllowedError":', gesture_script)
self.assertIn('case "NotReadableError":', gesture_script)
self.assertIn('case "OverconstrainedError":', gesture_script)
self.assertIn("function sendSimulatedGesture()", gesture_script)
self.assertIn('document.body.dataset.demoRole !== "admin"', gesture_script)
self.assertIn("function syncGestureDebugRole", gesture_script)
self.assertIn("HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000", app_script)
self.assertIn(
"requestTimeoutForExecutionMode(state.executionMode)", app_script
)
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-active-seat="front-right"', html)
self.assertNotIn('placeholder="请输入服务端配置令牌"', html)
self.assertIn('data-callout="backrest_recline"', html)
self.assertIn('data-callout="legrest_extension"', html)
self.assertIn('data-callout="backrest_included_angle"', html)
self.assertIn('data-callout="legrest_included_angle"', 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("根据指令创建", 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)
self.assertNotIn('id="roofMotorState"', html)
self.assertIn('id="ambientBrightness"', html)
self.assertIn("坐垫脚托夹角133度", html)
self.assertNotIn('data-axis="height"', html)
self.assertEqual(runtime["domain"], "cabin")
self.assertEqual(
runtime["hardware_execution_allowed"],
(
runtime["hardware"]["configured"]
or runtime["vehicle_can"]["configured"]
),
)
self.assertEqual(runtime["hardware"]["supported_targets"], ["front_right"])
self.assertEqual(
runtime["hardware"]["concurrency_policy"],
"single_flight_fail_fast",
)
self.assertEqual(
runtime["hardware"]["access_policy"],
"local_approval_idle_lease",
)
self.assertEqual(
runtime["hardware"]["access_inactivity_timeout_seconds"], 180
)
self.assertFalse(runtime["hardware"]["busy"])
self.assertEqual(
runtime["hardware"]["supported_seat_instances"], ["passenger"]
)
self.assertGreaterEqual(runtime["matrix"]["messages"], 25)
self.assertGreaterEqual(runtime["matrix"]["signals"], 128)
self.assertEqual(runtime["motion_position_range_pct"], [0, 100])
self.assertEqual(runtime["default_vehicle_id"], "denza_n7")
self.assertEqual(runtime["vehicle"]["name"], "腾势 N7")
self.assertIn(
"denza_n7",
{vehicle["id"] for vehicle in runtime["vehicles"]},
)
self.assertIn(
"zero_gravity",
{mode["id"] for mode in runtime["modes"]},
)
self.assertEqual(
runtime["vehicle"]["seat"]["motors"]["longitudinal"]["range"],
215,
)
self.assertEqual(
runtime["vehicle"]["seat"]["motors"]["longitudinal"]["reference_zero"],
"forwardmost",
)
self.assertEqual(
runtime["vehicle"]["seat"]["motors"]["cushion_tilt"]["reference_zero"],
"highest",
)
capability_names = {item["name"] for item in runtime["capabilities"]}
self.assertIn("seat.save_posture_preset", capability_names)
self.assertIn("seat.apply_posture_preset", capability_names)
self.assertIn("climate.set_temperature", capability_names)
self.assertIn("roof.set_transmittance", capability_names)
self.assertIn("ambient_light.set_illumination", capability_names)
passenger_seat_capabilities = [
item
for item in runtime["capabilities"]
if item["name"].startswith("seat.")
and not item["name"].startswith("seat.set_driver_")
]
driver_seat_capabilities = [
item
for item in runtime["capabilities"]
if item["name"].startswith("seat.set_driver_")
]
self.assertTrue(passenger_seat_capabilities)
self.assertTrue(
all(
item["target_scopes"] == ["passenger"]
for item in passenger_seat_capabilities
)
)
self.assertTrue(driver_seat_capabilities)
self.assertTrue(
all(
item["target_scopes"] == ["driver", "front_left"]
for item in driver_seat_capabilities
)
)
def test_remote_mode_api_executes_repository_preset_and_publishes_events(self) -> None:
modes_url = f"{self.base_url}/api/v1/vehicles/denza_n7/modes"
with self.opener.open(modes_url, timeout=3) as response:
catalog = json.loads(response.read().decode("utf-8"))
self.assertEqual(catalog["vehicle_id"], "denza_n7")
self.assertIn("zero_gravity", {mode["id"] for mode in catalog["modes"]})
create = urllib.request.Request(
f"{self.base_url}/api/v1/vehicles/denza_n7/mode-executions",
method="POST",
headers={
"Content-Type": "application/json",
"Idempotency-Key": "web-api-test-zero-gravity",
},
data=json.dumps(
{
"mode_id": "zero_gravity",
"execution_target": "dry_run",
"source": "android",
"source_device_id": "n7-cockpit-test",
}
).encode("utf-8"),
)
with self.opener.open(create, timeout=3) as response:
accepted = json.loads(response.read().decode("utf-8"))
execution_id = accepted["execution_id"]
self.assertEqual(accepted["mode_id"], "zero_gravity")
terminal = None
for _ in range(100):
with self.opener.open(
f"{self.base_url}/api/v1/mode-executions/{execution_id}", timeout=3
) as response:
terminal = json.loads(response.read().decode("utf-8"))
if terminal["status"] in {
"succeeded", "partially_succeeded", "failed", "cancelled", "timed_out"
}:
break
threading.Event().wait(0.02)
self.assertIsNotNone(terminal)
self.assertEqual(terminal["status"], "succeeded")
self.assertEqual(terminal["result"]["matched_modes"][0]["id"], "zero_gravity")
with self.opener.open(
f"{self.base_url}/api/v1/vehicles/denza_n7/events?after=0", timeout=3
) as response:
events = json.loads(response.read().decode("utf-8"))
matching = [
event for event in events["events"]
if event["execution"]["execution_id"] == execution_id
]
self.assertGreaterEqual(len(matching), 2)
self.assertEqual(matching[-1]["execution"]["status"], "succeeded")
nap_create = urllib.request.Request(
f"{self.base_url}/api/v1/vehicles/denza_n7/mode-executions",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"mode_id": "nap",
"execution_target": "dry_run",
"source": "android",
"source_device_id": "n7-cockpit-test",
}
).encode("utf-8"),
)
with self.opener.open(nap_create, timeout=3) as response:
nap_accepted = json.loads(response.read().decode("utf-8"))
nap_terminal = None
for _ in range(100):
with self.opener.open(
f"{self.base_url}/api/v1/mode-executions/"
f"{nap_accepted['execution_id']}",
timeout=3,
) as response:
nap_terminal = json.loads(response.read().decode("utf-8"))
if nap_terminal["status"] in {
"succeeded", "partially_succeeded", "failed", "cancelled", "timed_out"
}:
break
threading.Event().wait(0.02)
self.assertEqual(nap_terminal["status"], "succeeded")
self.assertEqual(
[mode["id"] for mode in nap_terminal["result"]["matched_modes"]],
["nap"],
)
self.assertEqual(
nap_terminal["vehicle_state"]["actuator_state"]["passenger"]
["position"]["backrest_recline"]["position_pct"],
81,
)
def test_remote_mode_api_rejects_unknown_mode(self) -> None:
request = urllib.request.Request(
f"{self.base_url}/api/v1/vehicles/denza_n7/mode-executions",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"mode_id": "not_a_real_mode",
"execution_target": "dry_run",
"source": "android",
"source_device_id": "n7-cockpit-test",
}
).encode("utf-8"),
)
with self.assertRaises(urllib.error.HTTPError) as caught:
self.opener.open(request, timeout=3)
self.assertEqual(caught.exception.code, 404)
def test_device_bearer_token_is_scoped_to_remote_mode_api(self) -> None:
device = urllib.request.build_opener()
modes = urllib.request.Request(
f"{self.base_url}/api/v1/vehicles/denza_n7/modes",
headers={"Authorization": "Bearer test-device-api-token"},
)
with device.open(modes, timeout=3) as response:
catalog = json.loads(response.read().decode("utf-8"))
self.assertEqual(catalog["vehicle_id"], "denza_n7")
runtime = urllib.request.Request(
f"{self.base_url}/api/runtime",
headers={"Authorization": "Bearer test-device-api-token"},
)
with self.assertRaises(urllib.error.HTTPError) as caught:
device.open(runtime, timeout=3)
self.assertEqual(caught.exception.code, 401)
bad_token = urllib.request.Request(
f"{self.base_url}/api/v1/vehicles/denza_n7/modes",
headers={
"Authorization": "Bearer wrong-device-token",
"X-Forwarded-For": "192.0.2.10",
},
)
with self.assertRaises(urllib.error.HTTPError) as caught:
device.open(bad_token, timeout=3)
self.assertEqual(caught.exception.code, 401)
def test_self_extension_vehicle_success_contract_is_strict(self) -> None:
with self.opener.open(
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=dual-seat-69", 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)
self.assertIn('command: "seat driver heat off"', extension_script)
self.assertIn('command: "ac off"', extension_script)
self.assertIn('command: "light indoor off"', extension_script)
self.assertIn('command: "light reading all off"', extension_script)
self.assertIn('command: "light ambient off"', extension_script)
self.assertIn('command: `window ${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)
self.assertNotIn('command: "door rear unlock"', extension_script)
self.assertNotIn('code: "OUTLET · 4 ZONES"', extension_script)
self.assertNotIn('command: "打开全部车窗"', extension_script)
self.assertNotIn('command: "打开副驾座椅通风"', extension_script)
self.assertNotIn('command: "把副驾座椅向后调一点"', extension_script)
self.assertIn('result?.status === "succeeded"', extension_script)
self.assertIn("everyOperationSucceeded", extension_script)
self.assertIn("everyHardwareOperationConfirmed", extension_script)
self.assertIn("result?.physical_feedback_confirmed === true", extension_script)
self.assertIn("cognitive-agent:extension-vehicle-result", extension_script)
self.assertIn("MASSAGE", extension_script)
self.assertIn("INTERIOR", extension_script)
self.assertIn("AMBIENT", extension_script)
self.assertIn(
"body: JSON.stringify({ command, conversation_id: sharedConversationId() })",
extension_script,
)
self.assertIn('window.addEventListener("cognitive-agent:result"', extension_script)
self.assertIn('id="extensionVehicleStateSummary"', extension_script)
self.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"',
app_script,
)
self.assertIn("applyAuthoritativeVehicleState", app_script)
self.assertIn('payload.conversation_id !== state.conversationId', app_script)
self.assertIn(
'target_policy === "ch1_body_can_driver_only"',
app_script,
)
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(
self,
) -> None:
service_result = {
"status": "partially_succeeded",
"provider_used": "local_rules",
"request_id": "req-extension-partial",
"conversation_id": "conv-extension-shared-e2e",
"conversation_turn": 4,
"decision_summary": ["空调完成,遮阳帘反馈超时。"],
"validation": [],
"execution_report": {
"spec": {
"status": "failed",
"physical_state": {
"source": "physical_can_ch1",
"complete": False,
},
"results": [
{
"ref": "climate-1",
"domain": "climate",
"status": "succeeded",
"telemetry": {
"simulated": False,
"feedback_observed": True,
"feedback_match": True,
"feedback_frame": {"frame_id": "0x404"},
},
},
{
"ref": "roof-1",
"domain": "roof",
"status": "failed",
"message": "反馈超时",
"telemetry": {"simulated": False},
},
],
}
},
"domain_state": {"climate": {"power": "on"}},
"actuator_state": {"driver": {"heating": {"state": "off"}}},
"deferred_requests": [],
}
request = urllib.request.Request(
f"{self.base_url}/api/extension/vehicle/execute",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"command": "打开空调并关闭遮阳帘",
"conversation_id": "conv-extension-shared-e2e",
}
).encode("utf-8"),
)
with patch.object(self.service, "process", return_value=service_result):
with self.opener.open(request, timeout=3) as response:
result = json.loads(response.read().decode("utf-8"))
self.assertFalse(result["success"])
self.assertEqual(result["status"], "partially_succeeded")
self.assertEqual(result["conversation_id"], "conv-extension-shared-e2e")
self.assertEqual(result["conversation_turn"], 4)
self.assertEqual(result["execution_report"], service_result["execution_report"])
self.assertEqual(result["domain_state"], service_result["domain_state"])
self.assertEqual(result["actuator_state"], service_result["actuator_state"])
self.assertEqual(result["physical_state"]["source"], "physical_can_ch1")
self.assertEqual(len(result["physical_feedback"]), 2)
self.assertFalse(result["physical_feedback_confirmed"])
def test_self_extension_gateway_uses_unified_authenticated_service(self) -> None:
original_extension = SeatDemoHandler.extension
SeatDemoHandler.extension = SelfExtensionService(SimulationGateway())
try:
with self.opener.open(
f"{self.base_url}/api/extension/health", timeout=3
) as response:
health = json.loads(response.read().decode("utf-8"))
request = urllib.request.Request(
f"{self.base_url}/api/extension/gscu/emit",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps({"slot": 9}).encode("utf-8"),
)
with self.opener.open(request, timeout=3) as response:
result = json.loads(response.read().decode("utf-8"))
finally:
SeatDemoHandler.extension = original_extension
self.assertTrue(health["success"])
self.assertEqual(health["transport"], "simulation")
self.assertTrue(health["connected"])
self.assertTrue(result["success"])
self.assertEqual(result["transport"], "simulation")
self.assertFalse(result["physical_state_confirmed"])
def test_post_request_returns_complete_dry_run(self) -> None:
request = urllib.request.Request(
f"{self.base_url}/api/requests",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"utterance": "副驾有点热,有点闷",
"provider": "local",
"conversation_id": "conv-e2e-browser",
"vehicle_id": "denza_n7",
}
).encode("utf-8"),
)
with self.opener.open(request, timeout=3) as response:
result = json.loads(response.read().decode("utf-8"))
self.assertEqual(result["status"], "succeeded")
self.assertEqual(result["function_config"]["kind"], "FunctionConfig")
self.assertEqual(result["command_plan"]["kind"], "CommandPlan")
self.assertEqual(result["execution_report"]["spec"]["mode"], "dry_run")
self.assertEqual(result["conversation_id"], "conv-e2e-browser")
self.assertEqual(result["vehicle"]["id"], "denza_n7")
self.assertEqual(result["actuator_state"]["passenger"]["ventilation"]["level"], 1)
def test_relative_backrest_phrase_uses_current_conversation_position(self) -> None:
conversation_id = "conv-e2e-relative-backrest"
for utterance in ("副驾靠背角度调到130度", "副驾靠背向前移动10度"):
request = urllib.request.Request(
f"{self.base_url}/api/requests",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"utterance": utterance,
"provider": "local",
"conversation_id": conversation_id,
"vehicle_id": "denza_n7",
"execution_mode": "dry_run",
},
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(request, timeout=3) as response:
result = json.loads(response.read().decode("utf-8"))
self.assertEqual(result["status"], "succeeded")
operation = result["function_config"]["spec"]["operations"][0]
self.assertEqual(operation["capability"], "seat.set_backrest_recline")
self.assertEqual(operation["parameters"]["position_pct"], 36)
self.assertEqual(operation["parameters"]["angle_deg"], 120)
self.assertEqual(operation["motion"]["direction"], "decrease")
self.assertEqual(operation["motion"]["delta_angle_deg"], 10.0)
def test_timed_and_oversized_backrest_requests_use_endpoint_fallbacks(self) -> None:
conversation_id = "conv-e2e-timed-endpoint-fallbacks"
def submit(utterance: str) -> dict:
request = urllib.request.Request(
f"{self.base_url}/api/requests",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"utterance": utterance,
"provider": "local",
"conversation_id": conversation_id,
"vehicle_id": "denza_n7",
"execution_mode": "dry_run",
},
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(request, timeout=3) as response:
return json.loads(response.read().decode("utf-8"))
submit("副驾靠背归零")
timed = submit("副驾靠背向后移动70秒")
self.assertEqual(timed["status"], "succeeded")
timed_operation = timed["function_config"]["spec"]["operations"][0]
self.assertEqual(timed_operation["parameters"]["position_pct"], 100)
self.assertEqual(timed_operation["motion"]["mode"], "timed")
self.assertEqual(timed_operation["motion"]["requested_duration_ms"], 70_000)
self.assertEqual(timed_operation["motion"]["applied_duration_ms"], 30_000)
self.assertTrue(timed_operation["motion"]["duration_clamped"])
self.assertIn("最大允许时长为30秒", "\n".join(timed["decision_summary"]))
submit("副驾靠背角度调到130度")
overflow = submit("副驾靠背向后移动70度")
self.assertEqual(overflow["status"], "succeeded")
overflow_operation = overflow["function_config"]["spec"]["operations"][0]
self.assertEqual(overflow_operation["parameters"]["position_pct"], 100)
self.assertEqual(overflow_operation["parameters"]["angle_deg"], 163)
self.assertTrue(overflow_operation["motion"]["range_clamped"])
self.assertEqual(
overflow_operation["motion"]["requested_delta_angle_deg"], 70
)
self.assertIn("备用目标", "\n".join(overflow["decision_summary"]))
def test_open_zero_gravity_phrase_matches_complete_mode(self) -> None:
request = urllib.request.Request(
f"{self.base_url}/api/requests",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"utterance": "打开零重力模式",
"provider": "local",
"conversation_id": "conv-e2e-open-zero-gravity",
"vehicle_id": "denza_n7",
"execution_mode": "dry_run",
},
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(request, timeout=3) as response:
result = json.loads(response.read().decode("utf-8"))
self.assertEqual(result["status"], "succeeded")
self.assertIn("zero_gravity", {item["id"] for item in result["matched_modes"]})
operations = result["function_config"]["spec"]["operations"]
self.assertEqual(len(operations), 4)
self.assertEqual(
{
operation["capability"]: operation["parameters"]["position_pct"]
for operation in operations
},
{
"seat.set_longitudinal_position": 100,
"seat.set_cushion_tilt": 100,
"seat.set_backrest_recline": 81,
"seat.set_legrest_height": 94,
},
)
self.assertEqual(result["execution_report"]["spec"]["mode"], "dry_run")
def test_hardware_state_endpoint_returns_physical_passenger_snapshot(self) -> None:
synchronized = {
"status": "succeeded",
"conversation_id": "conv-e2e-physical-state",
"source": "physical_can",
"complete": True,
"missing": [],
"actuator_state": {
"passenger": {
"position": {
"longitudinal": {"position_pct": 0},
"cushion_tilt": {"position_pct": 0},
"backrest_recline": {"position_pct": 0},
"legrest_height": {"position_pct": 0},
}
}
},
}
with patch.object(
self.service,
"synchronize_hardware_state",
return_value=synchronized,
) as synchronize:
with self.opener.open(
f"{self.base_url}/api/hardware/seat-state?"
"conversation_id=conv-e2e-physical-state&vehicle_id=denza_n7",
timeout=3,
) as response:
payload = json.loads(response.read().decode("utf-8"))
self.assertEqual(payload["source"], "physical_can")
self.assertEqual(
payload["actuator_state"]["passenger"]["position"]["longitudinal"]["position_pct"],
0,
)
synchronize.assert_called_once_with(
"conv-e2e-physical-state",
"denza_n7",
hardware_client_id=None,
hardware_access_bypass=True,
)
def test_gpt_http_failure_falls_back_without_returning_502(self) -> None:
original_api_key = self.service.openai.api_key
self.service.openai.api_key = "test-key"
request = urllib.request.Request(
f"{self.base_url}/api/requests",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"utterance": "打开副驾座椅通风",
"provider": "openai",
"conversation_id": "conv-e2e-gpt-fallback",
"execution_mode": "dry_run",
}
).encode("utf-8"),
)
try:
with patch.object(
self.service.openai,
"interpret",
side_effect=OpenAIIntentError("invalid_api_key"),
):
with self.opener.open(request, timeout=3) as response:
result = json.loads(response.read().decode("utf-8"))
self.assertEqual(response.status, 201)
finally:
self.service.openai.api_key = original_api_key
self.assertEqual(result["status"], "succeeded")
self.assertEqual(result["provider_used"], "local_rules_fallback")
self.assertEqual(result["execution_report"]["spec"]["mode"], "dry_run")
def test_frontend_suite_covers_every_physical_seat_capability(self) -> None:
suite_path = (
REPOSITORY_ROOT
/ "tests"
/ "fixtures"
/ "seat_frontend_capabilities.json"
)
suite = json.loads(suite_path.read_text(encoding="utf-8"))
required = set(suite["required_hardware_capabilities"])
covered: set[str] = set()
original_api_key = self.service.openai.api_key
self.service.openai.api_key = "frontend-suite-test-key"
try:
for case in suite["cases"]:
with self.subTest(case=case["id"]):
requests = []
for expected in case["operations"]:
covered.add(expected["capability"])
requests.append(
{
"domain": "seat",
"capability": expected["capability"],
"target_scope": expected["target"],
"operation": "set",
"parameters": [
{"name": name, "value": value}
for name, value in expected["parameters"].items()
],
"reason": f"frontend test case {case['id']}",
}
)
model_plan = {
"detected_domains": ["seat"],
"requests": requests,
"ambiguities": [],
"decision_summary": [f"执行前端测试用例 {case['id']}。"],
"confidence": 0.99,
}
request = urllib.request.Request(
f"{self.base_url}/api/requests",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"utterance": case["utterance"],
"provider": "openai",
"conversation_id": f"conv-frontend-suite-{case['id'].lower()}",
"execution_mode": "dry_run",
"vehicle_id": "denza_n7",
},
ensure_ascii=False,
).encode("utf-8"),
)
with patch.object(
self.service.openai,
"interpret",
return_value=model_plan,
):
try:
with self.opener.open(request, timeout=3) as response:
result = json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
self.fail(f"frontend case {case['id']} returned {exc.code}: {detail}")
self.assertEqual(result["status"], "succeeded")
operations = result["function_config"]["spec"]["operations"]
self.assertEqual(len(operations), len(case["operations"]))
self.assertEqual(
result["execution_report"]["spec"]["mode"], "dry_run"
)
self.assertEqual(
len(result["execution_report"]["spec"]["results"]),
len(case["operations"]),
)
for expected, actual in zip(case["operations"], operations):
self.assertEqual(actual["capability"], expected["capability"])
self.assertEqual(
actual["target"]["instance"], expected["target"]
)
for name, value in expected["parameters"].items():
self.assertEqual(actual["parameters"][name], value)
finally:
self.service.openai.api_key = original_api_key
self.assertEqual(covered, required)
safety_ids = {item["id"] for item in suite["safety_cases"]}
self.assertEqual(
safety_ids,
{"SAFE-01", "SAFE-02", "SAFE-03", "RECOVERY-01"},
)
def test_hardware_request_is_rejected_when_server_gate_is_locked(self) -> None:
request = urllib.request.Request(
f"{self.base_url}/api/requests",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"utterance": "副驾座椅加热开到一档",
"provider": "local",
"conversation_id": "conv-e2e-hardware-locked",
"execution_mode": "hardware",
"approval_token": "must-not-be-persisted",
}
).encode("utf-8"),
)
with 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")
artifact_text = "\n".join(
path.read_text(encoding="utf-8")
for path in Path(self.temporary_directory.name).rglob("*")
if path.is_file()
)
self.assertNotIn("must-not-be-persisted", artifact_text)
def test_local_decision_point_selects_one_public_terminal_and_can_stop_it(self) -> None:
client_a = "hwc-e2e-client-a1"
client_b = "hwc-e2e-client-b2"
def post(path: str, payload: dict, *, public: bool = False):
headers = {"Content-Type": "application/json"}
if public:
headers["X-Forwarded-For"] = "203.0.113.25"
request = urllib.request.Request(
f"{self.base_url}{path}",
method="POST",
headers=headers,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
)
return self.opener.open(request, timeout=3)
with post(
"/api/hardware/access/request",
{"client_id": client_a, "label": "用户电脑 A"},
public=True,
) as response:
first = json.loads(response.read().decode("utf-8"))
self.assertEqual(response.status, 202)
with post(
"/api/hardware/access/request",
{"client_id": client_b, "label": "用户电脑 B"},
public=True,
) as response:
second = json.loads(response.read().decode("utf-8"))
self.assertEqual(first["queue_position"], 1)
self.assertEqual(second["queue_position"], 2)
with self.opener.open(
f"{self.base_url}/api/local/hardware-access", timeout=3
) as response:
local_snapshot = json.loads(response.read().decode("utf-8"))
self.assertEqual(len(local_snapshot["pending"]), 2)
with post(
"/api/local/hardware-access/approve", {"client_id": client_b}
) as response:
approved = json.loads(response.read().decode("utf-8"))
self.assertEqual(approved["active"]["client_id"], client_b)
with self.assertRaises(urllib.error.HTTPError) as remote_admin:
request = urllib.request.Request(
f"{self.base_url}/api/local/hardware-access",
headers={"X-Forwarded-For": "203.0.113.25"},
)
self.opener.open(request, timeout=3)
self.assertEqual(remote_admin.exception.code, 403)
def hardware_request(client_id: str, conversation: str):
return post(
"/api/requests",
{
"utterance": "关闭副驾座椅加热",
"provider": "local",
"conversation_id": conversation,
"execution_mode": "hardware",
"hardware_client_id": client_id,
},
public=True,
)
fake_result = {"status": "succeeded", "request_id": "req-approved-client"}
with patch.object(
self.service, "_process_once", return_value=fake_result
) as process_once:
with hardware_request(client_b, "conv-approved-client") as response:
self.assertEqual(response.status, 201)
with self.assertRaises(urllib.error.HTTPError) as unapproved:
hardware_request(client_a, "conv-unapproved-client")
denied_payload = json.loads(unapproved.exception.read().decode("utf-8"))
self.assertEqual(unapproved.exception.code, 403)
self.assertEqual(denied_payload["error"], "hardware_access_required")
self.assertEqual(denied_payload["access_state"], "pending")
self.assertEqual(process_once.call_count, 1)
with post(
"/api/hardware/access/release",
{"client_id": client_b},
public=True,
) as response:
released = json.loads(response.read().decode("utf-8"))
self.assertEqual(released["state"], "none")
self.assertIsNone(self.service.hardware_access.admin_snapshot()["active"])
def test_local_hardware_decision_page_is_not_exposed_through_proxy(self) -> None:
with self.opener.open(
f"{self.base_url}/hardware-control", timeout=3
) as response:
html = response.read().decode("utf-8")
self.assertIn("真实台架本地决策台", html)
proxied = urllib.request.Request(
f"{self.base_url}/hardware-control",
headers={"X-Forwarded-For": "203.0.113.25"},
)
with self.assertRaises(urllib.error.HTTPError) as caught:
self.opener.open(proxied, timeout=3)
self.assertEqual(caught.exception.code, 403)
def test_overlapping_hardware_request_returns_conflict_without_entering_can(self) -> None:
entered = threading.Event()
release = threading.Event()
first_result: dict[str, object] = {}
cookie_header = "; ".join(
f"{cookie.name}={cookie.value}" for cookie in self.cookie_jar
)
def request(conversation_id: str) -> urllib.request.Request:
return urllib.request.Request(
f"{self.base_url}/api/requests",
method="POST",
headers={
"Content-Type": "application/json",
"Cookie": cookie_header,
},
data=json.dumps(
{
"utterance": "关闭副驾座椅加热",
"provider": "local",
"conversation_id": conversation_id,
"execution_mode": "hardware",
},
ensure_ascii=False,
).encode("utf-8"),
)
def occupy_bench(*_args: object, **_kwargs: object) -> dict[str, object]:
entered.set()
self.assertTrue(release.wait(timeout=3))
return {"status": "succeeded", "request_id": "req-exclusive-owner"}
def submit_owner() -> None:
with urllib.request.urlopen(
request("conv-exclusive-owner"), timeout=5
) as response:
first_result["http_status"] = response.status
first_result["payload"] = json.loads(
response.read().decode("utf-8")
)
with patch.object(
self.service,
"_process_once",
side_effect=occupy_bench,
) as process_once:
owner = threading.Thread(target=submit_owner)
owner.start()
self.assertTrue(entered.wait(timeout=2))
with self.assertRaises(urllib.error.HTTPError) as caught:
urllib.request.urlopen(
request("conv-exclusive-contender"), timeout=3
)
payload = json.loads(caught.exception.read().decode("utf-8"))
self.assertEqual(caught.exception.code, 409)
self.assertEqual(caught.exception.headers["Retry-After"], "5")
self.assertEqual(payload["error"], "hardware_busy")
self.assertIn("未进入CAN链路", payload["message"])
self.assertEqual(payload["retry_after_seconds"], 5)
self.assertEqual(process_once.call_count, 1)
state_request = urllib.request.Request(
f"{self.base_url}/api/hardware/seat-state?"
"conversation_id=conv-exclusive-state-reader&vehicle_id=denza_n7",
headers={"Cookie": cookie_header},
)
with self.assertRaises(urllib.error.HTTPError) as state_caught:
urllib.request.urlopen(state_request, timeout=3)
state_payload = json.loads(
state_caught.exception.read().decode("utf-8")
)
self.assertEqual(state_caught.exception.code, 409)
self.assertEqual(state_payload["error"], "hardware_busy")
release.set()
owner.join(timeout=5)
self.assertFalse(owner.is_alive())
self.assertEqual(first_result["http_status"], 201)
self.assertFalse(self.service.hardware_execution.public_info()["busy"])
if __name__ == "__main__":
unittest.main()