feat: complete dual-seat whole-vehicle orchestration
This commit is contained in:
+130
-25
@@ -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(
|
||||
|
||||
@@ -126,12 +126,12 @@ class TestSelfCalibrationApi:
|
||||
with urllib.request.urlopen(f"{self.base_url}/", timeout=3) as response:
|
||||
html = response.read().decode("utf-8")
|
||||
with urllib.request.urlopen(
|
||||
f"{self.base_url}/self-calibration.js?v=calibration-2",
|
||||
f"{self.base_url}/self-calibration.js?v=calibration-3",
|
||||
timeout=3,
|
||||
) as response:
|
||||
script = response.read().decode("utf-8")
|
||||
with urllib.request.urlopen(
|
||||
f"{self.base_url}/self-calibration.css?v=calibration-4",
|
||||
f"{self.base_url}/self-calibration.css?v=calibration-6",
|
||||
timeout=3,
|
||||
) as response:
|
||||
styles = response.read().decode("utf-8")
|
||||
@@ -148,8 +148,17 @@ class TestSelfCalibrationApi:
|
||||
content_type = response.headers["Content-Type"]
|
||||
content_security_policy = response.headers["Content-Security-Policy"]
|
||||
|
||||
assert 'href="/self-calibration.css?v=calibration-4"' in html
|
||||
assert 'src="/self-calibration.js?v=calibration-2"' in html
|
||||
assert 'href="/self-calibration.css?v=calibration-6"' in html
|
||||
assert 'src="/self-calibration.js?v=calibration-3"' in html
|
||||
assert 'data-calibration-page="live"' in script
|
||||
assert 'data-calibration-view="live"' in script
|
||||
assert 'data-calibration-page="hall"' not in script
|
||||
assert 'data-calibration-page="stereo"' not in script
|
||||
assert "座椅—双目联合标定" in script
|
||||
assert "实时座椅姿态" in script
|
||||
assert "四轴实时控制" in script
|
||||
assert "ZED 实时画面" in script
|
||||
assert "双目位姿数据" in script
|
||||
assert "标定仿真台架" in script
|
||||
assert "X-Cognitive-Agent-Role" in script
|
||||
assert "--v2-sidebar: #dae3f5" in styles
|
||||
|
||||
@@ -193,3 +193,5 @@ def test_self_repair_bridge_uses_the_backend_runtime_directory() -> None:
|
||||
assert "A silent or disconnected CAN bus must not kill" in source
|
||||
assert 'self.message("GUI STOP_ALL发送失败' in source
|
||||
assert 'self.message("GUI thermal off发送失败' in source
|
||||
assert "if opcode == 0x01 and self.latest_system is not None" in source
|
||||
assert "A bounded\n # timed command is deliberately allowed" in source
|
||||
|
||||
@@ -510,6 +510,113 @@ class MultiDomainServiceTests(unittest.TestCase):
|
||||
[],
|
||||
)
|
||||
|
||||
def test_build_named_mode_executes_and_persists_in_one_turn(self) -> None:
|
||||
mode_root = Path(self.temporary_directory.name) / "modes"
|
||||
self.service.mode_profiles = ModeProfileRegistry(mode_root)
|
||||
model_output = {
|
||||
"detected_domains": ["ambient_light", "climate"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "ambient_light",
|
||||
"capability": "ambient_light.set_illumination",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": [
|
||||
{"name": "color_hex", "value": "#B040FF"},
|
||||
{"name": "brightness_pct", "value": 60},
|
||||
],
|
||||
"reason": "营造K歌氛围。",
|
||||
},
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": [{"name": "temperature_c", "value": 24}],
|
||||
"reason": "保持K歌时的座舱舒适度。",
|
||||
},
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["已构建K歌场景。"],
|
||||
"confidence": 0.94,
|
||||
}
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
||||
built = self.service.process(
|
||||
"构建一个K歌模式",
|
||||
"openai",
|
||||
"conv-build-karaoke-mode",
|
||||
vehicle_id="denza_n7",
|
||||
)
|
||||
|
||||
self.assertEqual(built["status"], "succeeded")
|
||||
self.assertEqual(built["mode_profile"]["name"], "K歌模式")
|
||||
self.assertEqual(built["mode_creation"]["source_status"], "succeeded")
|
||||
self.assertEqual(built["mode_creation"]["operation_count"], 2)
|
||||
self.assertIn(
|
||||
"K歌模式",
|
||||
{
|
||||
mode["name"]
|
||||
for mode in self.service.runtime_info()["modes"]
|
||||
},
|
||||
)
|
||||
matched = self.service.mode_profiles.match_text(
|
||||
"进入K歌模式", vehicle_id="denza_n7"
|
||||
)
|
||||
self.assertEqual([profile.name for profile in matched], ["K歌模式"])
|
||||
self.assertTrue(Path(built["mode_profile"]["source"]).is_file())
|
||||
|
||||
def test_build_named_mode_is_always_a_dry_run_configuration_workflow(self) -> None:
|
||||
mode_root = Path(self.temporary_directory.name) / "hardware-modes"
|
||||
self.service.mode_profiles = ModeProfileRegistry(mode_root)
|
||||
model_output = {
|
||||
"detected_domains": ["ambient_light"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "ambient_light",
|
||||
"capability": "ambient_light.set_illumination",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": [
|
||||
{"name": "color_hex", "value": "#B040FF"},
|
||||
{"name": "brightness_pct", "value": 60},
|
||||
],
|
||||
"reason": "构建K歌氛围。",
|
||||
}
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["构建K歌灯光。"],
|
||||
"confidence": 0.95,
|
||||
}
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
||||
built = self.service.process(
|
||||
"构建一个K歌模式",
|
||||
"openai",
|
||||
"conv-build-karaoke-hardware-mode",
|
||||
execution_mode="hardware",
|
||||
vehicle_id="denza_n7",
|
||||
hardware_access_bypass=False,
|
||||
)
|
||||
|
||||
self.assertEqual(built["status"], "succeeded")
|
||||
self.assertEqual(built["execution_mode"], "dry_run")
|
||||
self.assertFalse(built["hardware_execution_allowed"])
|
||||
self.assertEqual(built["mode_profile"]["name"], "K歌模式")
|
||||
|
||||
def test_create_mode_language_does_not_match_ordinary_mode_execution(self) -> None:
|
||||
self.assertEqual(
|
||||
self.service._parse_mode_profile_create_name("创建一个K歌模式"),
|
||||
"K歌模式",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.service._parse_mode_profile_create_name("新增冥想空间模式"),
|
||||
"冥想空间模式",
|
||||
)
|
||||
self.assertIsNone(
|
||||
self.service._parse_mode_profile_create_name("进入K歌模式")
|
||||
)
|
||||
|
||||
def test_update_and_restore_long_term_mode_profile_are_control_plane_only(self) -> None:
|
||||
self.service.mode_profiles = ModeProfileRegistry(
|
||||
self.service.artifact_root / "modes"
|
||||
|
||||
@@ -166,18 +166,35 @@ class FrontendVehicleStateSyncContractTests(unittest.TestCase):
|
||||
{"climate": state["climate"]},
|
||||
)
|
||||
|
||||
def test_ch1_mode_never_polls_the_legacy_ch0_seat_endpoint(self) -> None:
|
||||
sync_source = function_source(
|
||||
def test_dual_channel_runtime_polls_each_seat_without_aliasing(self) -> None:
|
||||
passenger_source = function_source(
|
||||
self.app_source,
|
||||
"async function synchronizePassengerSeatState() {",
|
||||
"\nasync function synchronizeDriverSeatState()",
|
||||
)
|
||||
driver_source = function_source(
|
||||
self.app_source,
|
||||
"async function synchronizeDriverSeatState() {",
|
||||
"\nasync function synchronizePhysicalSeatState()",
|
||||
)
|
||||
combined_source = function_source(
|
||||
self.app_source,
|
||||
"async function synchronizePhysicalSeatState() {",
|
||||
"\nasync function loadAuthStatus()",
|
||||
)
|
||||
guard = sync_source.index(
|
||||
'target_policy === "ch1_body_can_driver_only"'
|
||||
self.assertIn("state.runtime?.hardware?.configured", passenger_source)
|
||||
self.assertIn("/api/hardware/seat-state", passenger_source)
|
||||
self.assertIn("/api/hardware/vehicle-state", driver_source)
|
||||
self.assertLess(
|
||||
combined_source.index("synchronizePassengerSeatState()"),
|
||||
combined_source.index("synchronizeDriverSeatState()"),
|
||||
)
|
||||
legacy_request = sync_source.index("/api/hardware/seat-state")
|
||||
self.assertLess(guard, legacy_request)
|
||||
self.assertIn("return false", sync_source[guard:legacy_request])
|
||||
render_source = function_source(
|
||||
self.app_source,
|
||||
"function renderSeatState(actuatorState) {",
|
||||
"\nfunction renderDomainState",
|
||||
)
|
||||
self.assertNotIn("passenger: mergedActuatorState.driver", render_source)
|
||||
|
||||
def test_vehicle_selection_cannot_replay_unfiltered_state_in_ch1_mode(self) -> None:
|
||||
select_source = function_source(
|
||||
|
||||
@@ -356,6 +356,59 @@ operations:
|
||||
[{"name": "transmittance_pct", "value": 20}],
|
||||
)
|
||||
|
||||
def test_delete_user_mode_removes_yaml_and_protects_presets(self) -> None:
|
||||
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_profile = registry.save_mode(
|
||||
"K歌模式",
|
||||
[
|
||||
{
|
||||
"domain": "ambient_light",
|
||||
"capability": "ambient_light.set_power",
|
||||
"target_scope": "vehicle",
|
||||
"parameters": {"state": "on"},
|
||||
}
|
||||
],
|
||||
vehicle_id="denza_n7",
|
||||
)
|
||||
|
||||
public_modes = {
|
||||
mode["id"]: mode
|
||||
for mode in registry.public_modes(vehicle_id="denza_n7")
|
||||
}
|
||||
self.assertEqual(public_modes[user_profile.id]["origin"], "user")
|
||||
self.assertTrue(public_modes[user_profile.id]["deletable"])
|
||||
self.assertEqual(public_modes["office"]["origin"], "preset")
|
||||
self.assertFalse(public_modes["office"]["deletable"])
|
||||
|
||||
deleted = registry.delete_user_mode(user_profile.id, "denza_n7")
|
||||
|
||||
self.assertEqual(deleted.id, user_profile.id)
|
||||
self.assertFalse(Path(user_profile.source).exists())
|
||||
self.assertNotIn(
|
||||
user_profile.id,
|
||||
{mode["id"] for mode in registry.public_modes("denza_n7")},
|
||||
)
|
||||
with self.assertRaises(PermissionError):
|
||||
registry.delete_user_mode("office", "denza_n7")
|
||||
|
||||
def test_update_mode_profile_backs_up_initial_yaml_and_restores(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
|
||||
@@ -11,7 +11,7 @@ from cognitive_agent.application.native_can_runtime import (
|
||||
NativeCanRuntime,
|
||||
)
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
from cognitive_agent.infrastructure.can.gateway import CanFrame
|
||||
from cognitive_agent.infrastructure.can.gateway import CanFrame, ZlgChannelConfig
|
||||
from cognitive_agent.infrastructure.can.hardware import VehicleCanSettings
|
||||
|
||||
|
||||
@@ -253,6 +253,27 @@ class NativeCanIntegrationTests(unittest.TestCase):
|
||||
|
||||
self.assertFalse(settings.enabled)
|
||||
|
||||
def test_dual_channel_formats_are_explicit_and_independent(self) -> None:
|
||||
seat = ZlgChannelConfig(
|
||||
channel=0,
|
||||
frame_format="classic_can",
|
||||
arbitration_bitrate=500_000,
|
||||
data_bitrate=2_000_000,
|
||||
termination_enabled=True,
|
||||
)
|
||||
vehicle = ZlgChannelConfig(
|
||||
channel=1,
|
||||
frame_format="can_fd",
|
||||
arbitration_bitrate=500_000,
|
||||
data_bitrate=2_000_000,
|
||||
termination_enabled=False,
|
||||
)
|
||||
|
||||
self.assertEqual(seat.public()["frame_format"], "classic_can")
|
||||
self.assertEqual(vehicle.public()["frame_format"], "can_fd")
|
||||
self.assertNotEqual(seat.channel, vehicle.channel)
|
||||
self.assertNotEqual(seat.termination_enabled, vehicle.termination_enabled)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -190,6 +190,88 @@ class NativeStateSyncTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(after, before)
|
||||
|
||||
def test_verified_read_only_ch1_feedback_updates_only_driver(self) -> None:
|
||||
state = self.store.synchronize_physical_ch1_feedback(
|
||||
"conv-native-sync",
|
||||
[{
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"frame_id": "0x4FA",
|
||||
"feedback_frame_id": "0x4FA",
|
||||
"valid": True,
|
||||
"verification": {
|
||||
"authoritative": True,
|
||||
"channel": 1,
|
||||
"simulated": False,
|
||||
"read_only": True,
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
},
|
||||
"actuator_state": {
|
||||
"driver": {"ventilation": {"state": "on", "level": 3}}
|
||||
},
|
||||
}],
|
||||
)
|
||||
self.assertEqual(state["devices"]["driver"]["ventilation"]["level"], 3)
|
||||
self.assertEqual(
|
||||
state["devices"]["driver"]["ventilation"]["feedback_source"],
|
||||
"physical_can_ch1",
|
||||
)
|
||||
self.assertEqual(state["devices"]["passenger"]["ventilation"]["state"], "off")
|
||||
|
||||
def test_verified_vehicle_safety_and_door_feedback_feed_whole_vehicle_state(self) -> None:
|
||||
def feedback(frame_id: str, **state_patch: dict) -> dict:
|
||||
return {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"frame_id": frame_id,
|
||||
"feedback_frame_id": frame_id,
|
||||
"valid": True,
|
||||
"verification": {
|
||||
"authoritative": True,
|
||||
"channel": 1,
|
||||
"simulated": False,
|
||||
"read_only": True,
|
||||
"frame_id": frame_id,
|
||||
"payload_size": 8,
|
||||
},
|
||||
**state_patch,
|
||||
}
|
||||
|
||||
state = self.store.synchronize_physical_ch1_feedback(
|
||||
"conv-native-sync",
|
||||
[
|
||||
feedback("0x342", vehicle_safety={"gear": "P"}),
|
||||
feedback("0x4EB", vehicle_safety={"speed_kmh": 0.0}),
|
||||
feedback(
|
||||
"0x407",
|
||||
domain_state={
|
||||
"door": {
|
||||
"locks": {
|
||||
"front_left": {"state": "lock"},
|
||||
"front_right": {"state": "unlock"},
|
||||
"rear_left": {"state": "lock"},
|
||||
"rear_right": {"state": "lock"},
|
||||
},
|
||||
"vehicle_lock": {"state": "unknown"},
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
vehicle_status = state["domain_devices"]["vehicle_status"]
|
||||
self.assertEqual(vehicle_status["gear"], "P")
|
||||
self.assertEqual(vehicle_status["speed_kmh"], 0.0)
|
||||
self.assertTrue(vehicle_status["stationary"])
|
||||
self.assertEqual(vehicle_status["feedback_source"], "physical_can_ch1")
|
||||
door = state["domain_devices"]["door"]
|
||||
self.assertEqual(door["locks"]["front_right"]["state"], "unlock")
|
||||
self.assertEqual(
|
||||
door["locks"]["front_right"]["feedback_source"],
|
||||
"physical_can_ch1",
|
||||
)
|
||||
|
||||
def test_unverified_or_empty_feedback_never_updates_state(self) -> None:
|
||||
before = self.store.snapshot("conv-native-sync")
|
||||
for feedback in (
|
||||
|
||||
@@ -9,6 +9,7 @@ from unittest.mock import patch
|
||||
from cognitive_agent.infrastructure.llm.openai_responses import (
|
||||
SYSTEM_PROMPT,
|
||||
OpenAIIntentInterpreter,
|
||||
build_intent_schema,
|
||||
)
|
||||
|
||||
|
||||
@@ -20,6 +21,7 @@ INTENT = {
|
||||
"capability": "seat.set_ventilation",
|
||||
"target_scope": "passenger",
|
||||
"operation": "activate",
|
||||
"duration_ms": None,
|
||||
"parameters": [
|
||||
{"name": "state", "value": "on"},
|
||||
{"name": "level", "value": 1},
|
||||
@@ -76,6 +78,16 @@ class OpenAIResponsesTests(unittest.TestCase):
|
||||
self.assertIn("longitudinal 20%", normalized)
|
||||
self.assertIn("do not invent catalog_only capabilities", normalized)
|
||||
|
||||
def test_explicit_seat_motion_duration_is_part_of_the_model_contract(self) -> None:
|
||||
normalized = " ".join(SYSTEM_PROMPT.split())
|
||||
self.assertIn("preserve that duration exactly in request.duration_ms", normalized)
|
||||
request_schema = build_intent_schema({})["properties"]["requests"]["items"]
|
||||
self.assertIn("duration_ms", request_schema["required"])
|
||||
self.assertEqual(
|
||||
request_schema["properties"]["duration_ms"]["type"],
|
||||
["integer", "null"],
|
||||
)
|
||||
|
||||
def test_non_streaming_structured_output(self) -> None:
|
||||
response_body = json.dumps({"output_text": json.dumps(INTENT)})
|
||||
environment = {
|
||||
|
||||
@@ -19,6 +19,7 @@ from cognitive_agent.application.self_extension import (
|
||||
build_gateway_command,
|
||||
validate_slot,
|
||||
)
|
||||
from cognitive_agent.application.zcan_seat_executor import ZcanBridgeSeatExecutor
|
||||
|
||||
|
||||
class SelfExtensionTests(unittest.TestCase):
|
||||
@@ -138,7 +139,7 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
"_request_gpt_plan",
|
||||
return_value=json.dumps(proposed),
|
||||
):
|
||||
with self.assertRaisesRegex(ExtensionIntegrationError, "CH1"):
|
||||
with self.assertRaisesRegex(ExtensionIntegrationError, "双通道"):
|
||||
service.plan(
|
||||
{
|
||||
"utterance": "打开座椅通风",
|
||||
@@ -225,7 +226,59 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
command_schema = request_payload["text"]["format"]["schema"]["properties"][
|
||||
"steps"
|
||||
]["items"]["properties"]["command"]
|
||||
self.assertEqual(command_schema["enum"], ["", "ac off"])
|
||||
self.assertEqual(command_schema["type"], "string")
|
||||
self.assertEqual(command_schema["maxLength"], 300)
|
||||
|
||||
def test_plan_accepts_bounded_passenger_position_for_declared_ch0_executor(
|
||||
self,
|
||||
) -> None:
|
||||
service = SelfExtensionService(SimulationGateway())
|
||||
proposed = {
|
||||
"summary": "调整副驾",
|
||||
"needs_confirmation": False,
|
||||
"question": "",
|
||||
"steps": [
|
||||
{
|
||||
"kind": "vehicle",
|
||||
"capability_id": "",
|
||||
"command": "副驾水平调到63%",
|
||||
"label": "副驾水平位置 63%",
|
||||
"delay_ms": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
payload = {
|
||||
"utterance": "副驾水平位置调到63%",
|
||||
"peripherals": [],
|
||||
"vehicle_devices": [
|
||||
{
|
||||
"id": "passenger-seat",
|
||||
"name": "副驾座椅",
|
||||
"channel": "CH0",
|
||||
"actions": [
|
||||
{"name": "副驾水平 50%", "command": "副驾水平调到50%"}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
|
||||
with patch.object(
|
||||
service,
|
||||
"_request_gpt_plan",
|
||||
return_value=json.dumps(proposed, ensure_ascii=False),
|
||||
):
|
||||
result = service.plan(payload)
|
||||
self.assertEqual(result["steps"][0]["command"], "副驾水平调到63%")
|
||||
|
||||
proposed["steps"][0]["command"] = "副驾水平调到101%"
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
|
||||
with patch.object(
|
||||
service,
|
||||
"_request_gpt_plan",
|
||||
return_value=json.dumps(proposed, ensure_ascii=False),
|
||||
):
|
||||
with self.assertRaisesRegex(ExtensionIntegrationError, "双通道"):
|
||||
service.plan(payload)
|
||||
|
||||
def test_vehicle_execution_reuses_the_same_orchestration_service(self) -> None:
|
||||
with patch.dict(
|
||||
@@ -515,6 +568,93 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
self.assertFalse(rejected["success"])
|
||||
self.assertFalse(rejected["physical_feedback_confirmed"])
|
||||
|
||||
def test_ch0_passenger_feedback_requires_shared_runtime_physical_snapshot(
|
||||
self,
|
||||
) -> None:
|
||||
operation = {
|
||||
"ref": "passenger-longitudinal-63",
|
||||
"domain": "seat",
|
||||
"status": "succeeded",
|
||||
"telemetry": {
|
||||
"transport": "shared_can_runtime",
|
||||
"channel": 0,
|
||||
"simulated": False,
|
||||
"hardware_feedback": True,
|
||||
"feedback_required": True,
|
||||
"feedback_observed": True,
|
||||
"feedback_match": True,
|
||||
"physical_state": {
|
||||
"source": "physical_can",
|
||||
"channel": 0,
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger",
|
||||
},
|
||||
"position": {
|
||||
"longitudinal": {
|
||||
"position_pct": 63,
|
||||
"position_valid": True,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
verified = SelfExtensionService._authoritative_vehicle_feedback_state(
|
||||
operation
|
||||
)
|
||||
self.assertIsNotNone(verified)
|
||||
self.assertEqual(verified["verification"]["channel"], 0)
|
||||
|
||||
operation["telemetry"]["transport"] = "untrusted_bridge"
|
||||
self.assertIsNone(
|
||||
SelfExtensionService._authoritative_vehicle_feedback_state(operation)
|
||||
)
|
||||
|
||||
def test_ch0_bridge_report_exposes_verified_passenger_snapshot(self) -> None:
|
||||
function_config = {
|
||||
"metadata": {"request_id": "req-ch0-extension"},
|
||||
"spec": {
|
||||
"operations": [
|
||||
{
|
||||
"id": "passenger-longitudinal-63",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
bridge_result = {
|
||||
"result": "PASS",
|
||||
"details": {
|
||||
"summary": "物理反馈达到目标",
|
||||
"motors": [
|
||||
{
|
||||
"position": 6300 if index == 0 else 5000,
|
||||
"position_valid": True,
|
||||
"active": False,
|
||||
}
|
||||
for index in range(4)
|
||||
],
|
||||
"thermal": [0x01],
|
||||
},
|
||||
}
|
||||
report = ZcanBridgeSeatExecutor._report(
|
||||
function_config,
|
||||
"2026-08-11T00:00:00Z",
|
||||
bridge_result,
|
||||
None,
|
||||
)
|
||||
physical_state = report["spec"]["physical_state"]
|
||||
self.assertEqual(physical_state["source"], "physical_can")
|
||||
self.assertEqual(physical_state["channel"], 0)
|
||||
self.assertEqual(
|
||||
physical_state["position"]["longitudinal"]["position_pct"], 63
|
||||
)
|
||||
verified = SelfExtensionService._authoritative_vehicle_feedback_state(
|
||||
report["spec"]["results"][0]
|
||||
)
|
||||
self.assertIsNotNone(verified)
|
||||
|
||||
def test_vehicle_execution_ignores_browser_routing_overrides(self) -> None:
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
|
||||
@@ -13,8 +13,12 @@ from cognitive_agent.application.self_repair.healing_model import (
|
||||
public_scenarios,
|
||||
)
|
||||
from cognitive_agent.application.self_repair.service import SelfRepairService
|
||||
from cognitive_agent.application.self_repair.can_runtime import (
|
||||
StandaloneCanRuntimeManager,
|
||||
)
|
||||
from cognitive_agent.application.self_repair.signal_diagnostic import SignalDiagnosticModel
|
||||
from cognitive_agent.application.self_repair.zcan_bridge import ZcanBridgeManager
|
||||
from cognitive_agent.infrastructure.can.gateway import ZlgChannelConfig
|
||||
|
||||
|
||||
def test_six_production_cases_complete_the_shadow_repair_loop() -> None:
|
||||
@@ -104,3 +108,179 @@ def test_bridge_manager_does_not_restart_a_live_busy_request(tmp_path: Path) ->
|
||||
|
||||
assert manager._heartbeat_online() is False
|
||||
assert manager._bridge_busy_within_grace() is True
|
||||
|
||||
|
||||
def test_standalone_runtime_opens_one_owner_for_seat_and_vehicle_channels(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
required_version = "runtime-test-v1"
|
||||
bridge_script = tmp_path / "seat_bridge.py"
|
||||
bridge_script.write_text(
|
||||
"BRIDGE_VERSION = 'runtime-test-v1'\n"
|
||||
"def run_bridge(transport=None, stop_event=None):\n"
|
||||
" stop_event.wait(2.0)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
python_executable = tmp_path / "python.exe"
|
||||
zlg_bridge_script = tmp_path / "zlg_bridge.py"
|
||||
dll_path = tmp_path / "zlgcan.dll"
|
||||
for path in (python_executable, zlg_bridge_script, dll_path):
|
||||
path.write_bytes(b"test")
|
||||
|
||||
owners = []
|
||||
|
||||
class FakeBus:
|
||||
def __init__(self, channel, config):
|
||||
self.channel = channel
|
||||
self.config = config
|
||||
|
||||
def send(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
def receive(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
class FakeOwner:
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
self.closed = False
|
||||
self.configs = {item.channel: item for item in kwargs["channels"]}
|
||||
owners.append(self)
|
||||
|
||||
def channel_bus(self, channel):
|
||||
return FakeBus(channel, self.configs[channel])
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
manager = StandaloneCanRuntimeManager(
|
||||
tmp_path / "status.json",
|
||||
bridge_script,
|
||||
required_version=required_version,
|
||||
python_executable=python_executable,
|
||||
zlg_bridge_script=zlg_bridge_script,
|
||||
dll_path=dll_path,
|
||||
device_type=41,
|
||||
device_index=0,
|
||||
channels=[
|
||||
ZlgChannelConfig(channel=0, frame_format="classic_can"),
|
||||
ZlgChannelConfig(channel=1, frame_format="can_fd"),
|
||||
],
|
||||
seat_channel=0,
|
||||
poll_s=0.01,
|
||||
process_factory=FakeOwner,
|
||||
)
|
||||
try:
|
||||
assert manager.configured is True
|
||||
assert manager.channel_bus(1, timeout_s=1.0).channel == 1
|
||||
manager.ensure_async()
|
||||
assert len(owners) == 1
|
||||
assert [
|
||||
item.frame_format for item in owners[0].kwargs["channels"]
|
||||
] == ["classic_can", "can_fd"]
|
||||
assert manager.snapshot()["backend"] == "standalone_zlg"
|
||||
finally:
|
||||
manager.stop()
|
||||
|
||||
assert owners[0].closed is True
|
||||
|
||||
|
||||
def _qualified_runtime_status(*, build_id: int = 16, frame_age_s: float = 0.0):
|
||||
now = time.time()
|
||||
frame_epoch = now - frame_age_s
|
||||
return {
|
||||
"bridge_version": "runtime-test-v1",
|
||||
"heartbeat_epoch": now,
|
||||
"current_request_id": None,
|
||||
"system": {"firmware_revision": 15},
|
||||
"firmware_identity": {
|
||||
"firmware_build_id": build_id,
|
||||
"qualified": build_id == 16,
|
||||
},
|
||||
"frame_epochs": {
|
||||
frame_id: frame_epoch
|
||||
for frame_id in (
|
||||
"0x301", "0x302", "0x303", "0x304", "0x305", "0x306", "0x307"
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_standalone_runtime_rejects_stale_protocol_and_old_firmware_build(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
manager = StandaloneCanRuntimeManager(
|
||||
tmp_path / "status.json",
|
||||
tmp_path / "seat_bridge.py",
|
||||
required_version="runtime-test-v1",
|
||||
python_executable=tmp_path / "python.exe",
|
||||
zlg_bridge_script=tmp_path / "zlg_bridge.py",
|
||||
dll_path=tmp_path / "zlgcan.dll",
|
||||
device_type=41,
|
||||
device_index=0,
|
||||
channels=[ZlgChannelConfig(channel=0, frame_format="classic_can")],
|
||||
seat_channel=0,
|
||||
expected_firmware_revision=15,
|
||||
expected_firmware_build_id=16,
|
||||
)
|
||||
|
||||
manager.status_path.write_text(
|
||||
json.dumps(_qualified_runtime_status(frame_age_s=5.0)), encoding="utf-8"
|
||||
)
|
||||
ready, reason, _ = manager._protocol_status()
|
||||
assert ready is False
|
||||
assert "过期" in reason
|
||||
|
||||
manager.status_path.write_text(
|
||||
json.dumps(_qualified_runtime_status(build_id=15)), encoding="utf-8"
|
||||
)
|
||||
ready, reason, _ = manager._protocol_status()
|
||||
assert ready is False
|
||||
assert "build" in reason
|
||||
|
||||
|
||||
def test_standalone_runtime_restores_repository_firmware_before_request(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
class FakeProgrammer:
|
||||
def __init__(self):
|
||||
self.calls = 0
|
||||
|
||||
def program(self):
|
||||
self.calls += 1
|
||||
return {"programmed": True, "image_sha256": "f" * 64}
|
||||
|
||||
programmer = FakeProgrammer()
|
||||
manager = StandaloneCanRuntimeManager(
|
||||
tmp_path / "status.json",
|
||||
tmp_path / "seat_bridge.py",
|
||||
required_version="runtime-test-v1",
|
||||
python_executable=tmp_path / "python.exe",
|
||||
zlg_bridge_script=tmp_path / "zlg_bridge.py",
|
||||
dll_path=tmp_path / "zlgcan.dll",
|
||||
device_type=41,
|
||||
device_index=0,
|
||||
channels=[ZlgChannelConfig(channel=0, frame_format="classic_can")],
|
||||
seat_channel=0,
|
||||
firmware_programmer=programmer,
|
||||
firmware_auto_flash=True,
|
||||
expected_firmware_revision=15,
|
||||
expected_firmware_build_id=16,
|
||||
firmware_boot_wait_s=0,
|
||||
protocol_probe_timeout_s=0.01,
|
||||
startup_timeout_s=0.2,
|
||||
)
|
||||
manager.status_path.write_text(
|
||||
json.dumps(_qualified_runtime_status(build_id=15)), encoding="utf-8"
|
||||
)
|
||||
manager.ensure_async = lambda: None
|
||||
manager._stop_runtime_owner = lambda: None
|
||||
manager._attempt_start = lambda: manager.status_path.write_text(
|
||||
json.dumps(_qualified_runtime_status()), encoding="utf-8"
|
||||
)
|
||||
|
||||
status = manager.ensure_seat_protocol()
|
||||
|
||||
assert status["firmware_identity"]["firmware_build_id"] == 16
|
||||
assert programmer.calls == 1
|
||||
assert manager.snapshot()["firmware_recovery_count"] == 1
|
||||
|
||||
@@ -117,10 +117,12 @@ class VehicleSafetyPreflightTests(unittest.TestCase):
|
||||
bus = _ReceiveOnlyBus([_gear(4), _speed(0)])
|
||||
bridge = _Bridge(bus)
|
||||
|
||||
with patch.object(
|
||||
type(service.vehicle_can_settings),
|
||||
"bridge",
|
||||
return_value=bridge,
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{"COGNITIVE_AGENT_SHARED_CAN_RUNTIME_ENABLED": "false"},
|
||||
clear=False,
|
||||
), patch.object(
|
||||
type(service.vehicle_can_settings), "bridge", return_value=bridge
|
||||
):
|
||||
with self.assertRaisesRegex(HardwareSafetyError, "not confirmed in P"):
|
||||
service._execute_orchestrated_reports(
|
||||
|
||||
Reference in New Issue
Block a user