merge: integrate remote MQTT routing with seat-scoped dynamic modes

This commit is contained in:
vee
2026-09-12 15:10:16 +08:00
43 changed files with 5726 additions and 492 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ async function main() {
});
async function handleApiRoute(route) {
const pathname = new URL(route.request().url()).pathname;
if (pathname === '/api/hardware/seat-state') {
if (['/api/hardware/seat-state', '/api/hardware/vehicle-state'].includes(pathname)) {
// This legacy GET may attempt automatic firmware recovery. Never touch
// the physical gateway while verifying browser interactions.
await route.fulfill({json:{status:'unavailable',hardware_execution_allowed:false}});
+6 -9
View File
@@ -1101,7 +1101,7 @@ class TestCodexAssistantWebApi:
assert "not-for-mcp" not in serialized_events
assert "模拟编排" in events[-1]["answer"]
def test_structured_plan_crosses_the_internal_bridge_without_replacing_original_text(
def test_explicit_roof_control_uses_local_preview_without_codex(
self,
) -> None:
events = self.post_assistant(
@@ -1112,17 +1112,14 @@ class TestCodexAssistantWebApi:
}
)
assert FAKE_CODEX.bridge_results[-1][0] == 200
call = AssistantApiHandler.service.calls[-1]
assert call["utterance"] == "天幕透光率调到30%"
assert call["provider"] == "local"
forwarded_plan = call["assistant_plan"]
assert forwarded_plan["requests"][0]["capability"] == "roof.set_transmittance"
assert forwarded_plan["requests"][0]["parameters"] == [
{"name": "transmittance_pct", "value": 30}
]
assert forwarded_plan["requests"][0]["reason"] == "原始用户请求的结构化候选动作。"
assert "instruction" not in call
assert "assistant_plan" not in call
assert not FAKE_CODEX.turn_calls
assert not FAKE_CODEX.bridge_results
assert events[0]["route"] == "vehicle_control"
assert events[0]["engine"] == "n7web-control-router"
assert events[-1]["type"] == "answer.completed"
def test_assistant_returns_named_mode_draft_without_saving_it(self) -> None:
+8 -1
View File
@@ -10,6 +10,7 @@ from http import HTTPStatus
from http.server import ThreadingHTTPServer
from pathlib import Path
from unittest.mock import patch
from urllib.parse import urlparse
import pytest
@@ -52,7 +53,13 @@ def test_mode_rail_real_browser_interactions(tmp_path: Path) -> None:
class BrowserHandler(SeatDemoHandler):
auth = AuthRuntime(AuthConfig(False, "", "", b"", 3600, 5, 300, 120, False))
def do_POST(self) -> None: # noqa: N802
def do_GET(self) -> None:
if urlparse(self.path).path in {"/api/hardware/seat-state", "/api/hardware/vehicle-state"}:
self._json({"status": "unavailable", "hardware_execution_allowed": False})
return
super().do_GET()
def do_POST(self) -> None:
# Only GET reaches this test server. Browser requests to the
# assistant are intercepted; unexpected writes fail closed.
self._json({"error": "browser_test_write_forbidden"}, HTTPStatus.FORBIDDEN)
+248 -20
View File
@@ -13,6 +13,7 @@ from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from cognitive_agent.application.codex_assistant import CodexAssistantService
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.application.self_extension import (
@@ -48,6 +49,7 @@ class SeatWebApiTests(unittest.TestCase):
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED": "false",
},
clear=False,
):
@@ -134,6 +136,66 @@ class SeatWebApiTests(unittest.TestCase):
unauthenticated.open(f"{self.base_url}/api/runtime", timeout=3)
self.assertEqual(caught.exception.code, 401)
def test_n7_mode_scene_catalog_is_public_and_read_only(self) -> None:
unauthenticated = urllib.request.build_opener()
with unauthenticated.open(f"{self.base_url}/n7/scenes", timeout=3) as response:
catalog = json.loads(response.read().decode("utf-8"))
self.assertEqual(response.status, 200)
self.assertEqual(response.headers["Cache-Control"], "no-store")
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
self.assertEqual(catalog["service"], "n7-scenes")
self.assertEqual(catalog["schema_version"], 1)
self.assertEqual(catalog["vehicle_id"], "denza_n7")
self.assertGreaterEqual(catalog["scene_count"], 13)
self.assertEqual(catalog["scene_count"], len(catalog["scenes"]))
office = next(scene for scene in catalog["scenes"] if scene["id"] == "office")
self.assertIn(office["origin"], {"preset", "customized"})
self.assertTrue(office["actions"])
self.assertIn("capability", office["actions"][0])
self.assertNotIn("command_flow", json.dumps(catalog, ensure_ascii=False))
preflight = urllib.request.Request(
f"{self.base_url}/n7/scenes",
method="OPTIONS",
)
with unauthenticated.open(preflight, timeout=3) as response:
self.assertEqual(response.status, 204)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
self.assertIn("GET", response.headers["Access-Control-Allow-Methods"])
def test_schedule_status_stays_authenticated_and_sanitized_beside_public_catalog(self) -> None:
endpoint = f"{self.base_url}/api/modes/schedule/status?vehicle_id=denza_n7"
unauthenticated = urllib.request.build_opener()
with self.assertRaises(urllib.error.HTTPError) as caught:
unauthenticated.open(endpoint, timeout=3)
self.assertEqual(caught.exception.code, 401)
state = {
"status": "running", "vehicle_id": "denza_n7", "session_id": "conv-mode-schedule-api",
"run_id": "schedule-private-safe-id", "phase_index": 0, "repeat_index": 0,
"phase_count": 2, "repeat_count": 2, "executed_phases": 0,
"elapsed_ms": 250, "total_duration_ms": 6000, "completed": False,
"phase_results": [{"raw_can": "private-command-bytes"}],
"hardware_client_id": "private-bench-client",
"error": "private transport stack or secret",
}
with (
patch.object(self.service.mode_schedule_runner, "status", return_value=state) as status,
self.opener.open(endpoint, timeout=3) as response,
):
payload = json.loads(response.read().decode("utf-8"))
status.assert_called_once_with("denza_n7")
self.assertEqual(payload["status"], "running")
self.assertEqual(payload["phase_count"], 2)
self.assertEqual(payload["total_duration_ms"], 6000)
self.assertEqual(payload["completed"], False)
self.assertIn("后续阶段已停止", payload["error"])
self.assertNotIn("phase_results", payload)
self.assertNotIn("hardware_client_id", payload)
self.assertNotIn("private-command-bytes", json.dumps(payload))
self.assertNotIn("private transport", json.dumps(payload))
def test_user_mode_delete_endpoint_removes_only_user_profiles(self) -> None:
original_registry = self.service.mode_profiles
with tempfile.TemporaryDirectory() as directory:
@@ -409,6 +471,12 @@ operations:
}
)
try:
with self.opener.open(
f"{self.base_url}/api/vehicle-display-sync", timeout=3
) as response:
display_before_compile = json.loads(
response.read().decode("utf-8")
)
generate_request = urllib.request.Request(
f"{self.base_url}/api/requests/preview",
method="POST",
@@ -428,6 +496,16 @@ operations:
)
with self.opener.open(generate_request, timeout=3) as response:
generated = json.loads(response.read().decode("utf-8"))
with self.opener.open(
f"{self.base_url}/api/vehicle-display-sync", timeout=3
) as response:
display_after_compile = json.loads(
response.read().decode("utf-8")
)
self.assertEqual(
display_after_compile["task"],
display_before_compile["task"],
)
save_request = urllib.request.Request(
f"{self.base_url}/api/mode-profiles/office",
@@ -621,18 +699,18 @@ operations:
def test_static_console_and_runtime_endpoint(self) -> None:
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8")
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-132", timeout=3) as response:
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-133", timeout=3) as response:
app_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
) as response:
gesture_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.js?v=delivery-state-62", timeout=3
f"{self.base_url}/console-v2.js?v=local-first-66", timeout=3
) as response:
console_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.css?v=delivery-state-66", timeout=3
f"{self.base_url}/console-v2.css?v=delivery-state-68", timeout=3
) as response:
console_styles = response.read().decode("utf-8")
with self.opener.open(
@@ -656,7 +734,7 @@ operations:
) as response:
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
with self.opener.open(
f"{self.base_url}/assistant-chat.js?v=codex-assistant-28", timeout=3
f"{self.base_url}/assistant-chat.js?v=codex-assistant-29", timeout=3
) as response:
assistant_script = response.read().decode("utf-8")
with self.opener.open(
@@ -697,12 +775,12 @@ operations:
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-132"', html)
self.assertIn('src="/app.js?v=dual-seat-133"', html)
self.assertIn('href="/styles.css?v=dual-seat-23"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
self.assertNotIn('class="vehicle-system-hud"', html)
self.assertIn('src="/console-v2.js?v=delivery-state-62"', html)
self.assertIn('href="/console-v2.css?v=delivery-state-66"', html)
self.assertIn('src="/console-v2.js?v=local-first-66"', html)
self.assertIn('href="/console-v2.css?v=delivery-state-68"', html)
self.assertIn('src="/speech-input.js?v=sensevoice-13"', html)
self.assertIn('href="/speech-input.css?v=sensevoice-7"', html)
self.assertIn('id="speechDictationButton"', html)
@@ -718,8 +796,8 @@ operations:
self.assertNotIn("滚动查看更多", html)
self.assertIn('aria-label="发送消息"', html)
self.assertIn('id="assistantTranscript"', html)
self.assertIn('href="/assistant-chat.css?v=codex-assistant-28"', html)
self.assertIn('src="/assistant-chat.js?v=codex-assistant-28"', html)
self.assertIn('href="/assistant-chat.css?v=codex-assistant-29"', html)
self.assertIn('src="/assistant-chat.js?v=codex-assistant-29"', html)
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
self.assertNotIn('"admin"', console_script)
@@ -902,7 +980,12 @@ operations:
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.assertNotIn("车端 CAN 未启用,其他领域保持虚拟执行", app_script)
self.assertNotIn('id="hardwareNote"', html)
self.assertNotIn('id="hardwareAliasNote"', html)
self.assertNotIn('id="vehicleTransportNote"', html)
self.assertIn("下发控制指令", console_script)
self.assertIn('new Set(["mode_editor_compile"])', console_script)
self.assertIn("function extractReferenceWindowPanes", app_script)
self.assertIn("function collectReferenceFrontLampMeshes", app_script)
self.assertIn("function emphasizeReferenceWindowPanes", app_script)
@@ -992,14 +1075,14 @@ operations:
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v279"', service_worker)
self.assertIn('/console-v2.css?v=delivery-state-66', service_worker)
self.assertIn('/console-v2.js?v=delivery-state-62', service_worker)
self.assertIn('/assistant-chat.css?v=codex-assistant-28', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-28', service_worker)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v283"', service_worker)
self.assertIn('/console-v2.css?v=delivery-state-68', service_worker)
self.assertIn('/console-v2.js?v=local-first-66', service_worker)
self.assertIn('/assistant-chat.css?v=codex-assistant-29', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-29', service_worker)
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
self.assertIn('"/app.js?v=dual-seat-132"', service_worker)
self.assertIn('"/app.js?v=dual-seat-133"', service_worker)
self.assertIn('"/speech-input.js?v=sensevoice-13"', service_worker)
self.assertIn('"/speech-input.css?v=sensevoice-7"', service_worker)
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-5"', service_worker)
@@ -1456,7 +1539,7 @@ operations:
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/app.js?v=dual-seat-132", timeout=3
f"{self.base_url}/app.js?v=dual-seat-133", timeout=3
) as response:
app_script = response.read().decode("utf-8")
@@ -2105,7 +2188,7 @@ operations:
},
data=json.dumps(
{
"utterance": "打开闪烁灯,颜色切换到红色,打开空调",
"utterance": "空调温度设置到23度,打开闪烁灯",
"provider": "local",
"conversation_id": "conv-mixed-nfc-can",
"execution_mode": "dry_run",
@@ -2131,23 +2214,138 @@ operations:
self.assertEqual(synced["learned_count"], 2)
self.assertEqual(events[-1]["type"], "result")
self.assertTrue(any(item["type"] == "validated_commands" for item in events))
self.assertLess(
next(
index
for index, item in enumerate(events)
if item["type"] == "validated_commands"
),
len(events) - 1,
)
validated_payloads = [
item["payload"]
for item in events
if item["type"] == "validated_commands"
]
self.assertTrue(validated_payloads)
# The local-first stage publishes known IR controls before CAN parsing;
# subsequent evidence must include the complete combined command set.
self.assertEqual(set(validated_payloads[0]["function_configs"]), {"self_extension"})
self.assertTrue(
all(
"self_extension" in payload["function_configs"]
for payload in validated_payloads
)
)
first_validated = next(
payload for payload in validated_payloads
if {"native_climate", "self_extension"}.issubset(payload["function_configs"])
)
self.assertTrue(
{"native_climate", "self_extension"}.issubset(
first_validated["command_plans"]
)
)
validated_operations = [
operation
for configuration in first_validated["function_configs"].values()
for operation in configuration["spec"].get("operations", [])
]
self.assertEqual(
{operation["capability"] for operation in validated_operations},
{"climate.set_temperature", "infrared.emit"},
)
self.assertTrue(
all(
operation.get("parse_source") == "local_rule"
for operation in validated_operations
)
)
validated_commands = [
command
for plan in first_validated["command_plans"].values()
for command in plan["spec"].get("commands", [])
]
climate_command = next(
command
for command in validated_commands
if command.get("signals", {}).get("debug_command")
== "ac temp driver 23"
)
self.assertEqual(climate_command["signals"]["frame_id"], "0x1DE")
self.assertEqual(
climate_command["signals"]["data_hex"],
"00 00 00 00 17 00 00 00",
)
infrared_command = next(
command
for command in validated_commands
if command.get("message") == "infrared.emit"
)
self.assertEqual(infrared_command["signals"]["slot"], 1)
assistant_projection = CodexAssistantService._validated_control_payload(
first_validated
)
self.assertIsNotNone(assistant_projection)
projected_commands = assistant_projection["validated_control_commands"]
projected_by_capability = {
item["operation"]["capability"]: item for item in projected_commands
}
self.assertEqual(
projected_by_capability["climate.set_temperature"]["actual_command"],
"ac temp driver 23",
)
self.assertEqual(
projected_by_capability["climate.set_temperature"]["frames"][0][
"value"
],
"0x1DE · 00 00 00 00 17 00 00 00",
)
self.assertEqual(
projected_by_capability["infrared.emit"]["actual_command"],
"infrared emit 1",
)
serialized_projection = json.dumps(
assistant_projection, ensure_ascii=False
)
self.assertNotIn("command_plans", serialized_projection)
self.assertNotIn("data_hex", serialized_projection)
compile_trace = next(
event
for event in first_validated["trace_events"]
if event["stage"] == "compile"
)
validated_traces = first_validated["trace_events"][
: first_validated["trace_events"].index(compile_trace) + 1
]
required_validation_stages = {
"intent_parse",
"context_resolve",
"domain_route",
"function_generate",
"schema_validate",
"compile",
}
actual_validation_stages = {
event["stage"] for event in validated_traces
}
self.assertTrue(
required_validation_stages.issubset(actual_validation_stages),
sorted(actual_validation_stages),
)
self.assertTrue(
all(
isinstance(event.get("duration_ms"), int)
and event["duration_ms"] >= 0
for event in validated_traces
)
)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(result["orchestration_route"], "unified_can_infrared")
self.assertEqual(
[item["slot"] for item in result["infrared_actions"]], [1, 3]
result["provider_used"], "local_rules+infrared_local_rules"
)
self.assertEqual([item["slot"] for item in result["infrared_actions"]], [1])
self.assertEqual(
result["infrared_actions"][0]["parse_source"], "local_rule"
)
@@ -2288,10 +2486,15 @@ operations:
self.assertEqual(events[-1]["type"], "error")
self.assertEqual(events[-1]["error"], "can_transport_failed")
validated = next(
validated_payloads = [
item["payload"]
for item in events
if item["type"] == "validated_commands"
]
self.assertEqual(set(validated_payloads[0]["function_configs"]), {"self_extension"})
validated = next(
payload for payload in validated_payloads
if {"native_climate", "self_extension"}.issubset(payload["function_configs"])
)
self.assertEqual(validated["status"], "processing")
self.assertEqual(
@@ -2665,6 +2868,31 @@ operations:
planner.assert_called_once()
self.assertEqual(planner.call_args.args[0]["utterance"], utterance)
def test_peripheral_timing_aliases_preserve_sequence_before_local_action_execution(self) -> None:
handler = object.__new__(SeatDemoHandler)
handler.service = SimpleNamespace(is_mode_profile_configuration_request=lambda _text: False)
capabilities = [
{"id": identity, "status": "learned", "device_id": "third-party-light",
"device_name": "第三方氛围灯", "card_uid": "1A53DD95", "action": action, "slot": slot}
for identity, action, slot in (("on", "开启灯光", 1), ("red", "红色灯光", 2), ("off", "关闭灯光", 3))
]
handler.extension = SimpleNamespace(list_peripheral_capabilities=lambda: capabilities)
handler.nfc = SimpleNamespace(snapshot=lambda: {
"reader": {"online": True}, "card": {"action": "present", "uid": "1A53DD95"},
})
for duration in ("5s", "5S", "500ms", "500MS", "1.5s", "5秒", "半分钟"):
with (
self.subTest(duration=duration),
patch.object(handler, "_execute_active_peripheral_request", return_value={"status": "succeeded"}) as planner,
patch.object(handler, "_execute_local_peripheral_actions", side_effect=AssertionError("sequence flattened")),
):
result = handler._execute_request_with_peripherals("/api/requests", {
"utterance": f"打开第三方氛围灯,红灯闪烁{duration}后,最后关闭第三方氛围灯",
"provider": "local", "execution_mode": "dry_run",
})
self.assertEqual(result["status"], "succeeded")
planner.assert_called_once()
def test_post_request_returns_complete_dry_run(self) -> None:
request = urllib.request.Request(
f"{self.base_url}/api/requests",