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",
@@ -287,6 +287,59 @@ test("a completed NDJSON answer remains the authoritative visible response", asy
assert.equal(harness.completed[0].error, null);
});
test("assistant bridges request start and validated local controls to the console", async () => {
const payload = {
request_id: "req-local-mixed",
utterance: "空调温度设置到23度,打开闪烁灯",
execution_mode: "dry_run",
provider_used: "local_rules+infrared_local_rules",
validated_control_commands: [
{
operation: {
id: "climate-temperature-1",
capability: "climate.set_temperature",
target: { zone: "driver" },
parameters: { temperature_c: 23 },
parse_source: "local_rule",
},
channel: "CAN1",
actual_command: "ac temp driver 23",
frames: [{
label: "CAN1 帧",
value: "0x1DE · 00 00 00 00 17 00 00 00",
}],
},
],
trace_events: [{ stage: "compile", duration_ms: 2 }],
};
const harness = createHarness([
{ type: "validated_control_commands", payload },
{ type: "answer.completed", route: "vehicle_control", answer: "模拟编排已完成。" },
]);
const starts = [];
const validated = [];
harness.sandbox.addEventListener("cognitive-agent:control-request-start", (event) => {
starts.push(event.detail);
});
harness.sandbox.addEventListener("cognitive-agent:validated-can-commands", (event) => {
validated.push(event.detail);
});
harness.input.value = payload.utterance;
harness.input.dataset.speechUtteranceId = "speech-local-mixed";
harness.input.dataset.speechValue = payload.utterance;
await harness.sandbox.CognitiveAgentAssistant.submit();
assert.deepEqual(JSON.parse(JSON.stringify(starts)), [{
utterance: payload.utterance,
speech_utterance_id: "speech-local-mixed",
speech_value: payload.utterance,
}]);
assert.deepEqual(JSON.parse(JSON.stringify(validated)), [payload]);
assert.equal(harness.requestCalls.length, 1);
assert.equal(JSON.parse(harness.requestCalls[0].options.body).execution_mode, "dry_run");
});
test("unsupported-control suggestions render once and click through as a new command", async () => {
const suggestions = [
{ id: "climate-on", label: "打开空调", command: "打开空调", domain: "climate" },
+10 -5
View File
@@ -13,15 +13,15 @@ def test_console_contains_ai_assistant_surface():
assert 'aria-label="AI助手对话"' in html
assert 'placeholder="向 AI助手提问或交代工作"' in html
assert 'aria-label="发送消息"' in html
assert 'href="/assistant-chat.css?v=codex-assistant-28"' in html
assert 'src="/assistant-chat.js?v=codex-assistant-28"' in html
assert 'href="/assistant-chat.css?v=codex-assistant-29"' in html
assert 'src="/assistant-chat.js?v=codex-assistant-29"' in html
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
assert 'const CACHE_VERSION = "cognitive-agent-pad-v279"' in service_worker
assert '"/assistant-chat.css?v=codex-assistant-28"' in service_worker
assert '"/assistant-chat.js?v=codex-assistant-28"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v283"' in service_worker
assert '"/assistant-chat.css?v=codex-assistant-29"' in service_worker
assert '"/assistant-chat.js?v=codex-assistant-29"' in service_worker
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
@@ -33,6 +33,11 @@ def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
assert 'event.type === "answer.delta"' in script
assert '"work.started", "work.progress"' in script
assert 'event.type === "work.completed"' in script
assert 'event.type === "validated_control_commands"' in script
assert '"cognitive-agent:validated-can-commands"' in script
assert '"cognitive-agent:control-request-start"' in script
assert "speech_utterance_id:" in script
assert "speech_value:" in script
assert 'Object.prototype.hasOwnProperty.call(event, "answer")' in script
assert 'pendingController.abort("user_stop")' in script
assert 'signal.addEventListener("abort", cancelReader, { once: true })' in script
@@ -0,0 +1,410 @@
from __future__ import annotations
import json
import threading
import unittest
from copy import deepcopy
from cognitive_agent.application.codex_assistant import (
CodexAssistantService,
_VehicleAuthorization,
_VehicleTurnContext,
)
UTTERANCE = "空调温度设置到23度,打开闪烁灯"
def compiled_mixed_result() -> dict[str, object]:
return {
"request_id": "req-assistant-local-mixed",
"utterance": UTTERANCE,
"status": "processing",
"phase": "commands_validated",
"provider_used": "local_rules+infrared_local_rules",
"execution_mode": "dry_run",
"control_transport": "wired_can",
"function_configs": {
"native_climate": {
"spec": {
"domain": "climate",
"operations": [
{
"id": "climate-temperature-1",
"capability": "climate.set_temperature",
"target": {"zone": "driver"},
"parameters": {"temperature_c": 23},
"parse_source": "local_rule",
}
],
}
},
"self_extension": {
"spec": {
"domain": "self_extension",
"operations": [
{
"id": "infrared-1",
"capability": "infrared.emit",
"target": {"device": "toy-001", "name": "闪灯玩具"},
"parameters": {"action": "开启", "slot": 1},
"parse_source": "local_rule",
}
],
}
},
},
"command_plans": {
"self_extension": {
"spec": {
"domain": "self_extension",
"commands": [
{
"operation_ref": "unrelated-operation",
"message": "Native_0xBAD",
"signals": {
"debug_command": "must-not-cross-tool-boundary",
"frame_id": "0xBAD",
"data_hex": "DE AD BE EF",
},
},
{
"operation_ref": "infrared-1",
"bus": "TCP",
"message": "infrared.emit",
"signals": {"slot": 1},
},
],
}
},
"native_climate": {
"spec": {
"domain": "climate",
"commands": [
{
"operation_ref": "climate-temperature-1",
"bus": "body_can",
"message": "Native_0x1DE",
"signals": {
"debug_command": "ac temp driver 23",
"frame_id": "0x1DE",
"data_hex": "00 00 00 00 17 00 00 00",
},
}
],
}
},
},
"trace_events": [
{"stage": "intent_parse", "duration_ms": 2},
{"stage": "context_resolve", "duration_ms": 1},
{"stage": "domain_route", "duration_ms": 1},
{"stage": "function_generate", "duration_ms": 2},
{"stage": "schema_validate", "duration_ms": 1},
{"stage": "compile", "duration_ms": 2},
{"stage": "execute", "duration_ms": 999},
],
"raw_can": "must-not-cross-tool-boundary",
}
class ValidatedControlProjectionTest(unittest.TestCase):
def test_projection_exposes_only_exact_display_commands(self) -> None:
projection = CodexAssistantService._validated_control_payload(
compiled_mixed_result()
)
self.assertIsNotNone(projection)
assert projection is not None
commands = projection["validated_control_commands"]
assert isinstance(commands, list)
self.assertEqual(len(commands), 2)
by_capability = {
command["operation"]["capability"]: command for command in commands
}
climate = by_capability["climate.set_temperature"]
infrared = by_capability["infrared.emit"]
self.assertEqual(climate["actual_command"], "ac temp driver 23")
self.assertEqual(
climate["frames"],
[
{
"label": "CAN1 帧",
"value": "0x1DE · 00 00 00 00 17 00 00 00",
}
],
)
self.assertEqual(infrared["actual_command"], "infrared emit 1")
self.assertEqual(
infrared["frames"],
[
{
"label": "GSCU1B TCP",
"value": '{"action":"emit","data":{"no":1},"type":"infrared"}',
}
],
)
self.assertEqual(
[event["stage"] for event in projection["trace_events"]],
[
"intent_parse",
"context_resolve",
"domain_route",
"function_generate",
"schema_validate",
"compile",
],
)
serialized = json.dumps(projection, ensure_ascii=False)
self.assertNotIn("command_plans", serialized)
self.assertNotIn("data_hex", serialized)
self.assertNotIn("raw_can", serialized)
self.assertNotIn("must-not-cross-tool-boundary", serialized)
def test_projection_drops_secrets_and_sensitive_nested_keys(self) -> None:
result = compiled_mixed_result()
secret = "projection-secret-token"
climate_config = result["function_configs"]["native_climate"]
climate_config["spec"]["domain"] = f"climate-{secret}"
operation = climate_config["spec"]["operations"][0]
operation["target"].update(
{
"api_key": "API-LEAK",
"cookie": "COOKIE-LEAK",
}
)
operation["parameters"].update(
{
"credential": "CRED-LEAK",
"access_key": "ACCESS-LEAK",
}
)
operation["parse_source"] = f"local-{secret}"
result["command_plans"]["native_climate"]["spec"]["commands"][0][
"signals"
]["debug_command"] = f"ac temp driver 23 {secret}"
projection = CodexAssistantService._validated_control_payload(
result,
secret=secret,
)
self.assertIsNotNone(projection)
assert projection is not None
serialized = json.dumps(projection, ensure_ascii=False)
for forbidden in (
secret,
"API-LEAK",
"COOKIE-LEAK",
"CRED-LEAK",
"ACCESS-LEAK",
"api_key",
"credential",
"access_key",
):
self.assertNotIn(forbidden, serialized)
climate = next(
item
for item in projection["validated_control_commands"]
if item["operation"]["capability"] == "climate.set_temperature"
)
self.assertEqual(climate["domain"], "")
self.assertNotIn("parse_source", climate["operation"])
self.assertEqual(climate["operation"]["target"], {"zone": "driver"})
self.assertEqual(
climate["operation"]["parameters"],
{"temperature_c": 23},
)
self.assertEqual(climate["actual_command"], "")
def test_projection_drops_operation_when_identity_contains_secret(self) -> None:
result = compiled_mixed_result()
secret = "projection-secret-token"
operation = result["function_configs"]["native_climate"]["spec"][
"operations"
][0]
operation["id"] = f"climate-{secret}"
result["command_plans"]["native_climate"]["spec"]["commands"][0][
"operation_ref"
] = operation["id"]
projection = CodexAssistantService._validated_control_payload(
result,
secret=secret,
)
self.assertIsNotNone(projection)
assert projection is not None
serialized = json.dumps(projection, ensure_ascii=False)
self.assertNotIn(secret, serialized)
self.assertEqual(
[
item["operation"]["capability"]
for item in projection["validated_control_commands"]
],
["infrared.emit"],
)
def test_duplicate_operation_refs_never_cross_configuration_boundaries(self) -> None:
result = {
"function_configs": {
"native_climate": {
"spec": {
"domain": "climate",
"operations": [
{
"id": "op-1",
"capability": "climate.set_temperature",
"target": {"zone": "driver"},
"parameters": {"temperature_c": 23},
}
],
}
},
"native_seat": {
"spec": {
"domain": "seat",
"operations": [
{
"id": "op-1",
"capability": "seat.set_heating",
"target": {"zone": "passenger"},
"parameters": {"level": 2},
}
],
}
},
},
"command_plans": {
"native_seat": {
"spec": {
"domain": "seat",
"commands": [
{
"operation_ref": "op-1",
"bus": "seat_can0",
"signals": {
"debug_command": "seat passenger heat 2"
},
}
],
}
}
},
}
projection = CodexAssistantService._validated_control_payload(result)
self.assertIsNotNone(projection)
assert projection is not None
commands = projection["validated_control_commands"]
self.assertEqual(len(commands), 1)
self.assertEqual(
commands[0]["operation"]["capability"],
"seat.set_heating",
)
self.assertEqual(commands[0]["actual_command"], "seat passenger heat 2")
def test_compiled_semantic_operation_is_kept_without_debug_command(self) -> None:
result = deepcopy(compiled_mixed_result())
climate_command = result["command_plans"]["native_climate"]["spec"][
"commands"
][0]
climate_command["signals"].pop("debug_command")
projection = CodexAssistantService._validated_control_payload(result)
self.assertIsNotNone(projection)
assert projection is not None
climate = next(
item
for item in projection["validated_control_commands"]
if item["operation"]["capability"] == "climate.set_temperature"
)
self.assertEqual(climate["actual_command"], "")
self.assertEqual(
climate["frames"][0]["value"],
"0x1DE · 00 00 00 00 17 00 00 00",
)
def test_direct_stream_emits_projection_before_progress_and_completion(self) -> None:
client = type("UnavailableClient", (), {"available": False})()
assistant = CodexAssistantService(client)
events: list[dict[str, object]] = []
def execute_control(instruction, progress):
self.assertEqual(instruction, UTTERANCE)
result = compiled_mixed_result()
progress(result)
return {**result, "status": "succeeded"}
result = assistant.stream(
UTTERANCE,
conversation_id="conv-validated-projection",
principal="local-test",
history=[],
emit=events.append,
execute_control=execute_control,
control_terms=("空调", "闪烁灯"),
quick_control_commands=[UTTERANCE],
)
event_types = [event["type"] for event in events]
validated_index = event_types.index("validated_control_commands")
self.assertLess(validated_index, event_types.index("action.progress"))
self.assertLess(validated_index, event_types.index("action.completed"))
self.assertLess(validated_index, event_types.index("answer.completed"))
self.assertEqual(result["route"], "vehicle_control")
self.assertEqual(result["success"], True)
safe_result = CodexAssistantService._safe_vehicle_result(
compiled_mixed_result()
)
self.assertNotIn("command_plans", safe_result)
self.assertNotIn("validated_control_commands", safe_result)
def test_agent_tool_path_queues_projection_for_the_browser_thread(self) -> None:
client = type("UnavailableClient", (), {"available": False})()
assistant = CodexAssistantService(client)
request_token = "projection-secret-token"
def execute_control(instruction, progress):
self.assertEqual(instruction, UTTERANCE)
result = compiled_mixed_result()
progress(result)
return {**result, "status": "succeeded"}
context = _VehicleTurnContext(
original_message=UTTERANCE,
execute_control=execute_control,
authorization=_VehicleAuthorization(
instruction=UTTERANCE,
reason="explicit_registered_control",
),
capability_snapshot={},
query_capabilities=lambda _snapshot, _query: {},
lock=threading.Lock(),
)
assistant._tool_contexts[request_token] = context
safe_result = assistant.execute_vehicle_control(request_token, UTTERANCE)
queued_events = []
while not context.events.empty():
queued_events.append(context.events.get_nowait())
event_types = [event["type"] for event in queued_events]
self.assertEqual(event_types[0], "action.started")
self.assertLess(
event_types.index("validated_control_commands"),
event_types.index("action.progress"),
)
self.assertLess(
event_types.index("validated_control_commands"),
event_types.index("action.completed"),
)
self.assertNotIn("command_plans", safe_result)
self.assertNotIn(request_token, json.dumps(queued_events, ensure_ascii=False))
if __name__ == "__main__":
unittest.main()
+285 -23
View File
@@ -395,6 +395,8 @@ def _stream(
history: list[dict[str, str]] | None = None,
emit: Callable[[dict[str, object]], bool | None] | None = None,
execute_control: Callable[..., dict[str, object]] | None = None,
preview_control: Callable[..., dict[str, object]] | None = None,
supplement_control: Callable[..., dict[str, object]] | None = None,
capability_snapshot: dict[str, object] | None = None,
capability_context: dict[str, object] | None = None,
mode_profile_draft_context: dict[str, object] | None = None,
@@ -409,6 +411,8 @@ def _stream(
emit=emit or (lambda event: events.append(event)),
execute_control=execute_control
or (lambda _instruction, _progress: pytest.fail("vehicle control was unexpected")),
preview_control=preview_control,
supplement_control=supplement_control,
control_terms=CONTROL_TERMS,
capability_snapshot=capability_snapshot,
capability_context=capability_context,
@@ -453,6 +457,15 @@ def test_general_question_streams_from_codex_without_vehicle_execution() -> None
assert "Codex AI 助手" not in client.thread_instructions[0]
assert client.thread_ephemeral == [True]
assert client.turn_calls[0]["text"] == "解释一下 CAN FD"
assert client.turn_calls[0]["application_context"][
"natural_language_routing"
] == {
"original_utterance": "解释一下 CAN FD",
"parsed_clauses": [],
"resolved_clauses": [],
"unresolved_clauses": ["解释一下 CAN FD"],
"cloud_input_utterance": "解释一下 CAN FD",
}
assert "不可信的浏览器系统指令" not in client.turn_calls[0]["text"]
assert client.finished[0].turn_id == "turn-1"
@@ -991,34 +1004,52 @@ def test_hot_cabin_calls_one_explicit_cooling_instruction_and_streams_verified_r
assistant.execute_vehicle_control(token, COOLING_INSTRUCTION)
def test_explicit_vehicle_request_without_tool_call_fails_closed() -> None:
client = FakeCodexClient()
def test_explicit_vehicle_request_uses_local_route_without_codex() -> None:
client = FakeCodexClient(available=False)
assistant = CodexAssistantService(client)
control_calls: list[str] = []
def on_start_turn(
_text: str,
_request_token: str,
turn: ActiveCodexTurn,
) -> None:
_put_answer(turn, "已经为你打开遮阳帘。", deltas=("已经为你", "打开遮阳帘。"))
def execute_control(
instruction: str,
progress: Callable[[dict[str, object]], None],
) -> dict[str, object]:
control_calls.append(instruction)
progress({"status": "processing", "request_id": "req-local-shade"})
return {
"request_id": "req-local-shade",
"status": "succeeded",
"execution_mode": "dry_run",
"utterance": instruction,
}
client.on_start_turn = on_start_turn
result, events = _stream(assistant, "打开遮阳帘")
result, events = _stream(
assistant,
"打开遮阳帘",
execute_control=execute_control,
preview_control=execute_control,
supplement_control=lambda *_args: pytest.fail(
"fully local control must not invoke cloud supplement"
),
)
assert result["answer"].startswith("车辆控制未完成")
assert "未向车辆下发任何操作" in result["answer"]
assert "已经为你" not in str(events)
assert control_calls == ["打开遮阳帘"]
assert result["route"] == "vehicle_control"
assert result["success"] is True
assert "打开遮阳帘" in result["answer"]
assert [event["type"] for event in events] == [
"message.accepted",
"work.started",
"action.started",
"action.progress",
"action.completed",
"answer.delta",
"work.completed",
"answer.completed",
]
assert events[2]["success"] is False
assert events[4]["success"] is False
assert events[-1]["answer"] == result["answer"]
assert events[0]["engine"] == "n7web-control-router"
assert events[1]["display_text"] == "正在本地解析并下发明确控制指令……"
assert not client.thread_instructions
assert not client.turn_calls
def test_exact_server_owned_quick_control_executes_without_codex() -> None:
@@ -1057,22 +1088,253 @@ def test_exact_server_owned_quick_control_executes_without_codex() -> None:
assert not client.turn_calls
def test_quick_control_requires_exact_normalized_command_match() -> None:
client = FakeCodexClient()
def test_explicit_control_does_not_require_exact_quick_card_match() -> None:
client = FakeCodexClient(available=False)
assistant = CodexAssistantService(client)
control_calls: list[str] = []
def execute_control(
instruction: str,
_progress: Callable[[dict[str, object]], None],
) -> dict[str, object]:
control_calls.append(instruction)
return {
"request_id": "req-explicit-local",
"status": "succeeded",
"execution_mode": "dry_run",
"utterance": instruction,
}
result, events = _stream(
assistant,
"请关闭所有车窗",
execute_control=lambda instruction, _progress: control_calls.append(instruction),
execute_control=execute_control,
preview_control=execute_control,
supplement_control=lambda *_args: pytest.fail(
"fully local control must not invoke cloud supplement"
),
quick_control_commands=["关闭所有车窗"],
)
assert events[0]["route"] == "agent"
assert result["answer"].startswith("车辆控制未完成")
assert not control_calls
assert client.turn_calls
assert events[0]["route"] == "vehicle_control"
assert result["success"] is True
assert control_calls == ["请关闭所有车窗"]
assert not client.turn_calls
def test_mixed_control_streams_local_result_before_unresolved_codex_turn() -> None:
client = FakeCodexClient()
assistant = CodexAssistantService(client)
order: list[str] = []
original = "空调温度设置为23度并把遮阳帘调整到适合观影"
local_clause = "空调温度设置为23度"
unresolved_clause = "把遮阳帘调整到适合观影"
preview_result = {
"request_id": "req-local-preview",
"status": "partially_succeeded",
"execution_mode": "dry_run",
"utterance": original,
"provider_used": "local_rules",
"local_control_budget_ms": 1000,
"local_control_elapsed_ms": 18.0,
"local_control_within_budget": True,
"natural_language_routing": {
"original_utterance": original,
"parsed_clauses": [local_clause],
"resolved_clauses": [local_clause],
"unresolved_clauses": [unresolved_clause],
},
"function_configs": {
"climate": {
"spec": {
"domain": "climate",
"operations": [
{
"id": "local-temperature",
"capability": "climate.set_temperature",
"target": {"zone": "driver"},
"parameters": {"temperature_c": 23},
"parse_source": "local_rule",
}
],
}
}
},
"command_plans": {
"climate": {
"spec": {
"domain": "climate",
"commands": [
{
"operation_ref": "local-temperature",
"bus": "body-can1",
"message": "climate.set_temperature",
"signals": {"debug_command": "ac temp driver 23"},
}
],
}
}
},
}
def preview_control(
instruction: str,
progress: Callable[[dict[str, object]], None],
) -> dict[str, object]:
assert instruction == original
order.append("local_preview")
progress(preview_result)
return preview_result
def supplement_control(
instruction: str,
progress: Callable[[dict[str, object]], None],
plan: dict[str, object],
) -> dict[str, object]:
assert instruction == unresolved_clause
assert plan["requests"][0]["capability"] == "roof.set_shade_position"
order.append("cloud_supplement")
result = {
"request_id": "req-cloud-supplement",
"status": "succeeded",
"execution_mode": "dry_run",
"utterance": original,
}
progress(result)
return result
cloud_plan = {
"detected_domains": ["roof"],
"requests": [
{
"domain": "roof",
"capability": "roof.set_shade_position",
"target_scope": "vehicle",
"operation": "set",
"duration_ms": None,
"parameters": [{"name": "position_pct", "value": 100}],
"reason": "补足未解析的观影遮光需求。",
}
],
"ambiguities": [],
"decision_summary": ["只处理未解析的遮阳帘子句。"],
"confidence": 0.9,
}
def on_start_turn(
text: str,
request_token: str,
turn: ActiveCodexTurn,
) -> None:
order.append("cloud_turn")
assert text == unresolved_clause
assistant.execute_cockpit_request(request_token, cloud_plan)
_put_answer(turn, "本地温度已先行处理,遮阳帘补充控制已完成。")
client.on_start_turn = on_start_turn
result, events = _stream(
assistant,
original,
preview_control=preview_control,
supplement_control=supplement_control,
)
assert order == ["local_preview", "cloud_turn", "cloud_supplement"]
assert client.turn_calls[0]["text"] == unresolved_clause
assert client.turn_calls[0]["application_context"][
"natural_language_routing"
] == {
"original_utterance": original,
"parsed_clauses": [local_clause],
"resolved_clauses": [local_clause],
"unresolved_clauses": [unresolved_clause],
"cloud_input_utterance": unresolved_clause,
}
validated_index = next(
index
for index, event in enumerate(events)
if event["type"] == "validated_control_commands"
)
cloud_progress_index = next(
index
for index, event in enumerate(events)
if event.get("display_text")
== "本地明确子句已处理,正在补充未解析子句……"
)
assert validated_index < cloud_progress_index
assert result["route"] == "agent"
def test_fully_unresolved_control_starts_bounded_cloud_turn_and_fails_closed() -> None:
client = FakeCodexClient()
assistant = CodexAssistantService(client)
instruction = "把遮阳帘调整到适合观影"
preview_result = {
"request_id": "req-unresolved-preview",
"preview_request_id": "req-unresolved-preview",
"status": "no_action",
"execution_mode": "dry_run",
"utterance": instruction,
"provider_used": "local_rules",
"function_configs": {},
"command_plans": {},
"local_control_budget_ms": 1000,
"local_control_elapsed_ms": 4.0,
"local_control_within_budget": True,
"supplement_required": True,
"natural_language_routing": {
"original_utterance": instruction,
"parsed_clauses": [],
"resolved_clauses": [],
"unresolved_clauses": [instruction],
},
}
def preview_control(
text: str,
_progress: Callable[[dict[str, object]], None],
) -> dict[str, object]:
assert text == instruction
return preview_result
def on_start_turn(
text: str,
_request_token: str,
turn: ActiveCodexTurn,
) -> None:
assert text == instruction
# The browser must never receive this unsupported execution claim
# because no guarded vehicle tool call follows it.
_put_answer(turn, "遮阳帘已经调整完成。")
client.on_start_turn = on_start_turn
result, events = _stream(
assistant,
instruction,
preview_control=preview_control,
supplement_control=lambda *_args: pytest.fail(
"a model answer without a tool call must not execute control"
),
)
assert client.turn_calls[0]["text"] == instruction
assert client.turn_calls[0]["application_context"][
"natural_language_routing"
] == {
"original_utterance": instruction,
"parsed_clauses": [],
"resolved_clauses": [],
"unresolved_clauses": [instruction],
"cloud_input_utterance": instruction,
}
assert not any(
event["type"] == "validated_control_commands" for event in events
)
assert result == {
"route": "agent",
"answer": "车辆控制未完成:AI 助手没有调用受控执行链,未向车辆下发任何操作。",
}
assert events[-1]["answer"] == result["answer"]
@pytest.mark.parametrize(
@@ -35,10 +35,10 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
):
assert asset in html
assert f'"{asset}"' in service_worker
assert html.index("/assistant-chat.js?v=codex-assistant-28") < html.index(
assert html.index("/assistant-chat.js?v=codex-assistant-29") < html.index(
"/energy-monitor.js?v=energy-monitor-4"
)
assert 'const CACHE_VERSION = "cognitive-agent-pad-v279"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v283"' in service_worker
def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None:
@@ -160,7 +160,7 @@ def test_energy_monitor_tracks_ai_and_direct_control_activity() -> None:
assert "completeControlActivity" in script
assert 'event.type === "action.progress" && event.payload' in assistant
assert 'new CustomEvent("cognitive-agent:vehicle-control-activity"' in assistant
assert 'new CustomEvent("cognitive-agent:validated-can-commands"' not in assistant
assert '"cognitive-agent:validated-can-commands"' in assistant
def test_energy_monitor_reads_persistent_seat_state_for_both_seats() -> None:
+2 -2
View File
@@ -41,13 +41,13 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
for asset in (
"/styles.css?v=dual-seat-23",
"/console-v2.css?v=delivery-state-66",
"/console-v2.css?v=delivery-state-68",
"/self-extension.css?v=gscu-tcp-43",
"/speech-input.css?v=sensevoice-7",
):
assert asset in html
assert f'"{asset}"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v279"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v283"' in service_worker
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None:
+243
View File
@@ -0,0 +1,243 @@
from __future__ import annotations
import json
from pathlib import Path
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.mode_scene_catalog import (
ModeSceneCatalogPublisher,
build_mode_scene_catalog,
)
from cognitive_agent.application.mode_schedule import ModeSchedule
from cognitive_agent.application.seat_service import REPOSITORY_ROOT
def _write_office_preset(path: Path) -> None:
path.parent.mkdir(parents=True)
path.write_text(
"""
id: office
name: 办公模式
description: N7 办公场景。
icon: briefcase
scope:
vehicle_ids: [denza_n7]
operations:
- domain: climate
capability: climate.set_temperature
target_scope: passenger
operation: set
parameters:
temperature_c: 24
""".strip()
+ "\n",
encoding="utf-8",
)
def test_publisher_tracks_create_override_and_delete(tmp_path: Path) -> None:
preset_root = tmp_path / "presets"
user_root = tmp_path / "user"
_write_office_preset(
preset_root / "vehicles" / "denza_n7" / "preset" / "office.yaml"
)
registry = ModeProfileRegistry(preset_root, user_root=user_root)
catalog_path = tmp_path / "scenses" / "demo-scenes.json"
publisher = ModeSceneCatalogPublisher(
catalog_path,
"denza_n7",
server_id="test-n7",
)
registry.add_change_listener(publisher.publish)
initial = json.loads(catalog_path.read_text(encoding="utf-8"))
assert initial["service"] == "n7-scenes"
assert initial["schema_version"] == 1
assert initial["server_id"] == "test-n7"
assert initial["scene_count"] == 1
assert initial["scenes"][0]["origin"] == "preset"
assert initial["scenes"][0]["actions"] == [
{
"type": "climate_temperature",
"at_ms": 0,
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "passenger",
"operation": "set",
"parameters": {"temperature_c": 24},
"zone": "passenger",
"celsius": 24,
}
]
created = registry.save_mode(
"阅读模式",
[
{
"domain": "ambient_light",
"capability": "ambient_light.set_illumination",
"target_scope": "vehicle",
"parameters": {"color_hex": "#E8F3FF", "brightness_pct": 40},
}
],
vehicle_id="denza_n7",
)
after_create = json.loads(catalog_path.read_text(encoding="utf-8"))
created_scene = next(
scene for scene in after_create["scenes"] if scene["id"] == created.id
)
assert after_create["scene_count"] == 2
assert created_scene["origin"] == "user"
assert created_scene["deletable"] is True
assert created_scene["actions"][0]["parameters"]["brightness_pct"] == 40
fan_mode = registry.save_mode(
"微风模式",
[
{
"domain": "climate",
"capability": "climate.set_fan_level",
"target_scope": "vehicle",
"parameters": {"level": 2},
}
],
vehicle_id="denza_n7",
)
after_fan_create = json.loads(catalog_path.read_text(encoding="utf-8"))
fan_scene = next(
scene for scene in after_fan_create["scenes"] if scene["id"] == fan_mode.id
)
assert fan_scene["actions"][0]["type"] == "climate_fan"
assert fan_scene["actions"][0]["level"] == 2
registry.save_mode(
"专注办公模式",
[
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "passenger",
"parameters": {"temperature_c": 26},
}
],
mode_id="office",
vehicle_id="denza_n7",
)
after_override = json.loads(catalog_path.read_text(encoding="utf-8"))
office = next(scene for scene in after_override["scenes"] if scene["id"] == "office")
assert office["label"] == "专注办公模式"
assert office["origin"] == "customized"
assert office["preset_override"] is True
assert office["actions"][0]["celsius"] == 26
registry.delete_user_mode(created.id, "denza_n7")
registry.delete_user_mode(fan_mode.id, "denza_n7")
registry.delete_user_mode("office", "denza_n7")
after_delete = json.loads(catalog_path.read_text(encoding="utf-8"))
assert [scene["id"] for scene in after_delete["scenes"]] == ["office"]
assert after_delete["scenes"][0]["label"] == "办公模式"
assert after_delete["scenes"][0]["origin"] == "preset"
def test_repository_presets_are_exported_without_internal_command_flows() -> None:
registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes")
catalog = build_mode_scene_catalog(
registry,
"denza_n7",
generated_at="2026-09-09T00:00:00Z",
)
assert catalog["scene_count"] >= 13
assert len({scene["id"] for scene in catalog["scenes"]}) == catalog["scene_count"]
by_id = {scene["id"]: scene for scene in catalog["scenes"]}
assert by_id["office"]["operation_count"] == len(registry.get_profile("office", "denza_n7").operations)
assert by_id["rocking_horse"]["behavior"] == "cyclic"
assert by_id["rocking_horse"]["timeline"]["repeat"] == "until_stopped_or_timeout"
encoded = json.dumps(catalog, ensure_ascii=False)
assert "command_flow" not in encoded
assert "source_request_id" not in encoded
def _schedule() -> dict:
return ModeSchedule.from_dict({
"schema": "cognitive-agent-mode-schedule/v1",
"repeat_count": 2,
"phases": [
{"id": "warm", "name": "暖舱", "duration_ms": 1000, "operations": [
{"domain": "climate", "capability": "climate.set_temperature", "target_scope": "vehicle",
"operation": "set", "parameters": {"temperature_c": 28}},
]},
{"id": "settle", "name": "舒适", "duration_ms": 2000, "operations": [
{"domain": "climate", "capability": "climate.set_temperature", "target_scope": "vehicle",
"operation": "set", "parameters": {"temperature_c": 24}},
{"domain": "seat", "capability": "seat.set_backrest_recline", "target_scope": "passenger",
"operation": "set", "parameters": {"position_pct": 45}},
]},
],
}).to_dict()
def test_schedule_catalog_preserves_phases_but_never_exports_flat_executable_actions(tmp_path: Path) -> None:
registry = ModeProfileRegistry(tmp_path / "presets", user_root=tmp_path / "user")
schedule = _schedule()
profile = registry.save_mode(
"分阶段舒适模式", schedule["phases"][0]["operations"], vehicle_id="denza_n7",
schedule=ModeSchedule.from_dict(schedule),
command_flow={"source_request_id": "req-private", "raw_can": "private-transport-bytes"},
)
catalog = build_mode_scene_catalog(registry, "denza_n7")
scene = next(item for item in catalog["scenes"] if item["id"] == profile.id)
assert scene["schedule"] == schedule
assert scene["behavior"] == "cyclic"
assert scene["operation_count"] == 3
assert scene["domains"] == ["climate", "seat"]
assert scene["actions"] == [], "legacy clients must not execute all timeline stages at once"
assert scene["execution"] == {
"kind": "server_schedule", "mode_id": profile.id,
"client_actions_supported": False, "requires_authentication": True,
}
assert "服务端时间轴" in scene["execution_notice"]
assert scene["preview_actions_scope"] == "single_cycle"
preview = scene["preview_actions"]
assert [action["phase_id"] for action in preview] == ["warm", "settle", "settle"]
assert [action["at_ms"] for action in preview] == [0, 1000, 1000]
assert [action["phase_duration_ms"] for action in preview] == [1000, 2000, 2000]
assert [action["celsius"] for action in preview if action["type"] == "climate_temperature"] == [28, 24]
assert preview[-1]["target_scope"] == "passenger"
assert preview[-1]["parameters"] == {"position_pct": 45}
assert ModeSchedule.from_dict(scene["schedule"]).total_duration_ms == 6000
encoded = json.dumps(catalog)
assert "source_request_id" not in encoded
assert "private-transport-bytes" not in encoded
assert "command_flow" not in encoded
def test_schedule_publisher_updates_after_save_reload_duration_change_and_delete(tmp_path: Path) -> None:
registry = ModeProfileRegistry(tmp_path / "presets", user_root=tmp_path / "user")
publisher = ModeSceneCatalogPublisher(tmp_path / "catalog.json", "denza_n7")
registry.add_change_listener(publisher.publish)
assert publisher.snapshot()["scene_count"] == 0
schedule = _schedule()
created = registry.save_mode(
"动态释压模式", schedule["phases"][0]["operations"], vehicle_id="denza_n7",
schedule=ModeSchedule.from_dict(schedule),
)
first = publisher.snapshot()
assert first["scenes"][0]["schedule"] == schedule
registry.reload()
assert publisher.snapshot() == first
schedule["phases"][1]["duration_ms"] = 5000
registry.save_mode(
created.name, schedule["phases"][0]["operations"], vehicle_id="denza_n7",
mode_id=created.id, schedule=ModeSchedule.from_dict(schedule),
)
changed = publisher.snapshot()
assert changed["catalog_revision"] != first["catalog_revision"]
assert changed["scenes"][0]["profile_revision"] != first["scenes"][0]["profile_revision"]
assert changed["scenes"][0]["schedule"]["phases"][1]["duration_ms"] == 5000
assert changed["scenes"][0]["actions"] == []
assert json.loads(publisher.path.read_text(encoding="utf-8")) == changed
registry.delete_user_mode(created.id, "denza_n7")
assert publisher.snapshot()["scene_count"] == 0
assert publisher.snapshot()["scenes"] == []
+59
View File
@@ -42,10 +42,22 @@ class _FakeMqttClient:
self.on_message = None
self.subscriptions: list[tuple[str, int]] = []
self.published: list[tuple[str, str, int, bool]] = []
self.websocket_options: list[dict[str, object]] = []
self.credentials: tuple[str, str | None] | None = None
self.tls_configured = False
def reconnect_delay_set(self, **_kwargs) -> None:
return None
def ws_set_options(self, **kwargs: object) -> None:
self.websocket_options.append(kwargs)
def username_pw_set(self, username: str, password: str | None) -> None:
self.credentials = (username, password)
def tls_set(self) -> None:
self.tls_configured = True
def connect_async(self, host: str, port: int, keepalive: int) -> None:
self.connection = (host, port, keepalive)
@@ -89,6 +101,8 @@ def _settings():
"password": None,
"tls_enabled": False,
"feedback_timeout_ms": 100,
"transport": "tcp",
"websocket_path": "/mqtt",
}
values["public_info"] = lambda: {
"enabled": True,
@@ -109,9 +123,31 @@ class MqttCanPayloadTests(unittest.TestCase):
self.assertEqual(settings.condition_topic, "n7/n7-headunit/can/state")
self.assertEqual(settings.control_topic, "n7/n7-headunit/can/command")
self.assertFalse(settings.approval_required)
self.assertEqual(settings.transport, "tcp")
self.assertEqual(settings.websocket_path, "/mqtt")
self.assertEqual(info["state_contract"], "n7-can-signals/v1")
self.assertEqual(info["control_contract"], "n7-headunit-can-command/v1")
def test_websocket_transport_settings_are_loaded_from_environment(self) -> None:
with patch.dict(
"os.environ",
{
"COGNITIVE_AGENT_VEHICLE_MQTT_HOST": "mqtt.example.test",
"COGNITIVE_AGENT_VEHICLE_MQTT_PORT": "443",
"COGNITIVE_AGENT_VEHICLE_MQTT_TRANSPORT": "websocket",
"COGNITIVE_AGENT_VEHICLE_MQTT_WEBSOCKET_PATH": "/mqtt",
"COGNITIVE_AGENT_VEHICLE_MQTT_TLS": "true",
},
clear=True,
):
settings = VehicleMqttSettings.from_env()
self.assertTrue(settings.adapter_configured)
self.assertEqual(settings.transport, "websockets")
self.assertEqual(settings.websocket_path, "/mqtt")
self.assertTrue(settings.tls_enabled)
self.assertEqual(settings.public_info()["transport"], "websockets")
def test_explicit_shared_approval_policy_still_locks_mqtt_control(self) -> None:
with patch.dict(
"os.environ",
@@ -461,6 +497,29 @@ class MqttCanTransportTests(unittest.TestCase):
def tearDown(self) -> None:
self.transport.close()
def test_websocket_client_applies_transport_path_authentication_and_tls(self) -> None:
client = _FakeMqttClient()
settings = _settings()
settings.transport = "websockets"
settings.websocket_path = "/mqtt"
settings.username = "n7web"
settings.password = "secret"
settings.tls_enabled = True
transport = MqttCanTransport(settings)
with patch("paho.mqtt.client.Client", return_value=client) as constructor:
transport.start()
try:
self.assertEqual(
constructor.call_args.kwargs["transport"], "websockets"
)
self.assertEqual(client.websocket_options, [{"path": "/mqtt"}])
self.assertEqual(client.credentials, ("n7web", "secret"))
self.assertTrue(client.tls_configured)
self.assertEqual(client.connection, ("127.0.0.1", 1883, 30))
finally:
transport.close()
def test_condition_frames_use_existing_decoder_and_snapshot_contract(self) -> None:
self.client.emit(
"n7/n7-headunit/can/state",
+3 -2
View File
@@ -137,7 +137,8 @@ def test_main_parsed_control_panel_renders_validated_infrared_operations() -> No
assert 'capability === "infrared.emit"' in console
assert 'channel: "IR"' in console
assert 'frameLabel: "GSCU1B TCP"' in console
assert "已解析控制指令" in console
assert "下发控制指令" in console
assert "已解析控制指令" not in console
assert 'li[data-can-channel="IR"]' in styles
assert "console-v2.js?v=delivery-state-62" in html
assert "console-v2.js?v=local-first-66" in html
assert 'speech_semantic: "语音语义"' in console
@@ -195,9 +195,15 @@ def test_left_sidebar_exposes_collapsed_parsed_can_command_panel() -> None:
assert 'canCommandPanel.id = "v2CanCommandPanel"' in console
assert 'id="v2CanCommandCount">0</strong>' in console
assert 'id="v2CanCommandList"' in console
assert "canCommandPanel.open" not in console
assert 'id="v2CommandTiming"' in console
assert "panel.open = true;" in console
assert "if (ui.sidebarCollapsed) setSidebarCollapsed(false);" in console
assert "function parsedCanCommandForOperation(operation = {})" in console
assert "function parsedCanCommands(result = {})" in console
assert "function semanticControlInstruction(operation = {})" in console
assert "function compiledCommandsForOperation(result, configKey, operation = {})" in console
assert "function parsedCommandTimingRows(result = {}, timingState = {})" in console
assert "result.validated_control_commands" in console
assert "getParsedCanCommands: (result) => parsedCanCommands(result)" in console
assert "const configurations = result.function_configs || {};" in console
assert "if (!configurationEntries.length && result.function_config)" in console
@@ -206,8 +212,18 @@ def test_left_sidebar_exposes_collapsed_parsed_can_command_panel() -> None:
assert 'const command = (category, text, channel = "CAN1", frame = "")' in console
assert 'data-can-channel="${escapeHtml(item.channel)}"' in console
assert 'class="v2-can-command-channel"' in console
assert 'class="v2-can-command-layer v2-can-command-semantic"' in console
assert 'class="v2-can-command-layer v2-can-command-actual"' in console
assert console.index("语义控制指令") < console.index("实际控制指令")
assert 'recordTiming: true' in console
assert 'reveal: true' in console
assert 'window.addEventListener("cognitive-agent:speech-state"' in console
assert 'window.addEventListener("cognitive-agent:speech-result"' in console
assert 'window.addEventListener("cognitive-agent:control-request-start"' in console
assert 'window.addEventListener("cognitive-agent:result"' in console
assert '$("#requestForm")?.addEventListener("submit"' in console
assert "function commandResultMatchesTimedRequest(result = {})" in console
assert "matchesTimedRequest && !validatedCommandsVisible" in console
for command_contract in (
"door rear ${action}",
@@ -246,6 +262,9 @@ def test_left_sidebar_exposes_collapsed_parsed_can_command_panel() -> None:
assert ".v2-can-command-list li code" in styles
assert ".v2-can-command-channel" in styles
assert ".v2-can-command-delivery" in styles
assert ".v2-command-timing" in styles
assert ".v2-can-command-semantic" in styles
assert ".v2-can-command-actual" in styles
assert '.v2-can-command-delivery[data-delivery-state="submitted"]' in styles
assert 'li[data-can-channel="CAN0"]' in styles
assert ".v2-sidebar-collapsed .v2-can-command-panel" in styles
@@ -297,10 +316,12 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
"process.stdout.write(JSON.stringify(parsedCanCommands(sample)));"
)
completed = subprocess.run(
[node, "-e", script],
[node, "-"],
input=script,
check=True,
capture_output=True,
text=True,
encoding="utf-8",
)
assert json.loads(completed.stdout) == [
@@ -310,6 +331,8 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
"command": "seat passenger backrest position 36",
"frame": "0x300 · 01 12 <SEQ_LO> <SEQ_HI> 10 0E 30 75",
"parseSource": "local_rule",
"semantic": "seat.set_backrest_recline(target=front-right, angle_deg=120, position_pct=36)",
"operationRef": "",
"deliveryState": "not_submitted",
"deliveryLabel": "未下发",
"deliveryTransport": "有线 CAN",
@@ -319,6 +342,8 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
"category": "空调温度",
"command": "ac temp driver 24",
"parseSource": "cloud_supplement",
"semantic": "climate.set_temperature(target=driver, temperature_c=24)",
"operationRef": "",
"deliveryState": "not_submitted",
"deliveryLabel": "未下发",
"deliveryTransport": "有线 CAN",
@@ -326,6 +351,335 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
]
def test_local_mixed_command_uses_compiled_can_and_ir_details_with_stage_timings() -> None:
node = shutil.which("node")
if node is None:
return
console = source("console-v2.js")
start = console.index(" const normalizedCanTarget")
end = console.index("\n\n function renderParsedCanCommands", start)
helpers = console[start:end]
sample = {
"provider_used": "local_rules",
"function_configs": {
"native_climate": {
"spec": {
"domain": "climate",
"operations": [
{
"id": "climate-temperature-1",
"capability": "climate.set_temperature",
"target": {"zone": "driver"},
"parameters": {"temperature_c": 23},
"parse_source": "local_rule",
}
],
}
},
"self_extension": {
"spec": {
"domain": "self_extension",
"operations": [
{
"id": "infrared-1",
"capability": "infrared.emit",
"target": {"device": "toy-001", "name": "闪灯玩具"},
"parameters": {"slot": 1, "action": "开启"},
"parse_source": "local_rule",
}
],
}
},
},
# Deliberately reverse the plan order and include a distracting command;
# the renderer must join only on the exact operation_ref.
"command_plans": {
"self_extension": {
"spec": {
"domain": "self_extension",
"commands": [
{
"operation_ref": "unrelated-ir",
"message": "infrared.emit",
"signals": {"slot": 9},
},
{
"operation_ref": "infrared-1",
"bus": "TCP",
"message": "infrared.emit",
"signals": {"slot": 1},
},
],
}
},
"native_climate": {
"spec": {
"domain": "climate",
"commands": [
{
"operation_ref": "climate-temperature-1",
"bus": "body_can",
"message": "Native_0x1DE",
"signals": {
"debug_command": "ac temp driver 23",
"frame_id": "0x1DE",
"data_hex": "00 00 00 00 17 00 00 00",
},
}
],
}
},
},
"trace_events": [
{"stage": "local_peripheral_plan", "duration_ms": 1},
{"stage": "nfc_peripheral_sequence", "duration_ms": 0},
{"stage": "intent_parse", "duration_ms": 12},
{"stage": "context_resolve", "duration_ms": 1},
{"stage": "compile", "duration_ms": 2},
{"stage": "execute", "duration_ms": 999},
],
}
timing_state = {
"inputMode": "speech",
"speechStartedAt": 0,
"speechResultAt": 80,
"speechTimings": {
"audio_read_ms": 1,
"vad_ms": 2,
"asr_ms": 30,
"assembly_ms": 1,
"semantic_ms": 3,
"command_resolution_ms": 1,
"recovery_ms": 0,
"total_ms": 37,
},
"requestStartedAt": 100,
"validatedAt": 125,
"displayedAt": 126,
"renderDurationMs": 1,
}
browser_projection = {
"provider_used": "local_rules+infrared_local_rules",
"execution_mode": "dry_run",
"validated_control_commands": [
{
"operation": sample["function_configs"]["native_climate"]["spec"][
"operations"
][0],
"domain": "climate",
"channel": "CAN1",
"actual_command": "ac temp driver 23",
"frames": [
{
"label": "CAN1 帧",
"value": "0x1DE · 00 00 00 00 17 00 00 00",
}
],
},
{
"operation": sample["function_configs"]["self_extension"]["spec"][
"operations"
][0],
"domain": "self_extension",
"channel": "IR",
"actual_command": "infrared emit 1",
"frames": [
{
"label": "GSCU1B TCP",
"value": '{"action":"emit","data":{"no":1},"type":"infrared"}',
}
],
},
],
"trace_events": sample["trace_events"][:3],
}
script = (
f"{helpers}\n"
f"const sample = {json.dumps(sample, ensure_ascii=False)};\n"
f"const browserProjection = {json.dumps(browser_projection, ensure_ascii=False)};\n"
f"const timingState = {json.dumps(timing_state)};\n"
"process.stdout.write(JSON.stringify({"
"commands: parsedCanCommands(sample), "
"projectedCommands: parsedCanCommands(browserProjection), "
"timings: parsedCommandTimingRows(sample, timingState)"
"}));"
)
completed = subprocess.run(
[node, "-"],
input=script,
check=True,
capture_output=True,
text=True,
encoding="utf-8",
)
parsed = json.loads(completed.stdout)
climate, infrared = parsed["commands"]
projected_climate, projected_infrared = parsed["projectedCommands"]
assert climate["semantic"] == (
"climate.set_temperature(target=driver, temperature_c=23)"
)
assert climate["command"] == "ac temp driver 23"
assert climate["frame"] == "0x1DE · 00 00 00 00 17 00 00 00"
assert climate["frameLabel"] == "CAN1 帧"
assert climate["parseSource"] == "local_rule"
assert infrared["semantic"] == (
"infrared.emit(target=toy-001, action=开启, slot=1)"
)
assert infrared["command"] == "infrared emit 1"
assert infrared["frameLabel"] == "GSCU1B TCP"
assert json.loads(infrared["frame"])["data"]["no"] == 1
assert infrared["parseSource"] == "local_rule"
assert projected_climate["semantic"] == climate["semantic"]
assert projected_climate["command"] == climate["command"]
assert projected_climate["frames"] == climate["frames"]
assert projected_infrared["semantic"] == infrared["semantic"]
assert projected_infrared["command"] == infrared["command"]
assert projected_infrared["frames"] == infrared["frames"]
timing_by_key = {item["key"]: item for item in parsed["timings"]}
assert timing_by_key["local_peripheral_plan-0"]["label"] == "红外本地规则编译"
assert timing_by_key["nfc_peripheral_sequence-1"]["label"] == "红外指令序列生成"
assert timing_by_key["intent_parse-2"]["label"] == "本地规则意图解析"
assert timing_by_key["request_to_validated"]["durationMs"] == 25
assert timing_by_key["dom_render"]["durationMs"] == 1
assert timing_by_key["request_to_display"]["durationMs"] == 26
assert timing_by_key["speech_to_display"]["durationMs"] == 126
assert timing_by_key["total_ms"]["durationMs"] == 37
assert all(item["durationMs"] >= 0 for item in parsed["timings"])
assert not any(item["key"].startswith("execute") for item in parsed["timings"])
def test_duplicate_operation_refs_do_not_borrow_another_configs_plan() -> None:
node = shutil.which("node")
if node is None:
return
console = source("console-v2.js")
start = console.index(" const normalizedCanTarget")
end = console.index("\n\n function renderParsedCanCommands", start)
helpers = console[start:end]
sample = {
"function_configs": {
"native_climate": {
"spec": {
"domain": "climate",
"operations": [
{
"id": "op-1",
"capability": "climate.set_temperature",
"target": {"zone": "driver"},
"parameters": {"temperature_c": 23},
}
],
}
},
"native_seat": {
"spec": {
"domain": "seat",
"operations": [
{
"id": "op-1",
"capability": "seat.set_heating",
"target": {"zone": "passenger"},
"parameters": {"level": 2},
}
],
}
},
},
"command_plans": {
"native_seat": {
"spec": {
"domain": "seat",
"commands": [
{
"operation_ref": "op-1",
"bus": "seat_can0",
"signals": {
"debug_command": "seat passenger heat 2",
"frame_id": "0x300",
"data_hex": "01 02 03 04 05 06 07 08",
},
}
],
}
}
},
}
script = (
f"{helpers}\n"
f"const sample = {json.dumps(sample)};\n"
"process.stdout.write(JSON.stringify(parsedCanCommands(sample)));"
)
completed = subprocess.run(
[node, "-"],
input=script,
check=True,
capture_output=True,
text=True,
encoding="utf-8",
)
parsed = json.loads(completed.stdout)
by_semantic = {item["semantic"]: item for item in parsed}
climate = by_semantic[
"climate.set_temperature(target=driver, temperature_c=23)"
]
seat = by_semantic["seat.set_heating(target=passenger, level=2)"]
assert climate["command"] == "ac temp driver 23"
assert "frame" not in climate
assert climate["channel"] == "CAN1"
assert seat["command"] == "seat passenger heat 2"
assert seat["channel"] == "CAN0"
def test_command_timing_rejects_stale_request_events() -> None:
node = shutil.which("node")
if node is None:
return
console = source("console-v2.js")
correlation_start = console.index(" function commandCorrelationIds")
correlation_end = console.index(
"\n\n function rememberValidatedCommandContext",
correlation_start,
)
correlation_helper = console[correlation_start:correlation_end]
start = console.index(" const normalizedCanTarget")
end = console.index("\n\n function renderParsedCanCommands", start)
helpers = console[start:end]
script = (
"let commandTimingState = {requestStartedAt: 1, requestId: '', "
"utterance: '空调温度设置到23度,打开闪烁灯'};\n"
f"{correlation_helper}\n"
f"{helpers}\n"
"const first = commandResultMatchesTimedRequest({request_id: 'req-current', "
"utterance: '空调温度设置到23度,打开闪烁灯'});\n"
"const staleId = commandResultMatchesTimedRequest({request_id: 'req-old', "
"utterance: '空调温度设置到23度,打开闪烁灯'});\n"
"const staleText = commandResultMatchesTimedRequest({request_id: 'req-current', "
"utterance: '打开车窗'});\n"
"process.stdout.write(JSON.stringify({first, staleId, staleText, "
"boundRequestId: commandTimingState.requestId}));"
)
completed = subprocess.run(
[node, "-"],
input=script,
check=True,
capture_output=True,
text=True,
encoding="utf-8",
)
assert json.loads(completed.stdout) == {
"first": True,
"staleId": False,
"staleText": False,
"boundRequestId": "req-current",
}
def test_parsed_command_delivery_is_independent_of_transport_and_target() -> None:
node = shutil.which("node")
if node is None:
@@ -398,10 +752,12 @@ def test_parsed_command_delivery_is_independent_of_transport_and_target() -> Non
"parsedCanCommands(sample))));"
)
completed = subprocess.run(
[node, "-e", script],
[node, "-"],
input=script,
check=True,
capture_output=True,
text=True,
encoding="utf-8",
)
parsed = json.loads(completed.stdout)
@@ -423,6 +779,7 @@ def test_parsed_command_delivery_is_independent_of_transport_and_target() -> Non
def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences() -> None:
app = source("app.js")
console = source("console-v2.js")
styles = source("styles.css")
html = source("index.html")
@@ -442,6 +799,10 @@ def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences()
assert 'executionMode: "dry_run"' in app
assert 'clientType: "mode_editor_compile"' in app
assert "previewOnly: true" in app
assert 'const nonDispatchTaskSources = new Set(["mode_editor_compile"])' in console
assert "if (isDispatchTask(task))" in console
assert "下发控制指令" in console
assert "已解析控制指令" not in console
assert "function executeSavedModeProfile(mode)" in app
assert "/api/mode-profiles/${encodeURIComponent(mode.id)}/execute" in app
assert '$("#utterance").value = mode.name || mode.id;' in app
@@ -450,6 +811,9 @@ def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences()
assert "生成指令" in html
assert ">执行</span></button>" not in html
assert 'id="hardwareNote"' not in html
assert 'id="hardwareAliasNote"' not in html
assert 'id="vehicleTransportNote"' not in html
assert ".mode-editor-command-card" in styles
assert ".mode-editor-command-meta" in styles
@@ -544,10 +908,12 @@ def test_mode_editor_prepares_one_to_many_mqtt_message_sequences() -> None:
"process.stdout.write(JSON.stringify(modeEditorReadableCommands(flow)));"
)
completed = subprocess.run(
[node, "-e", script],
[node, "-"],
input=script,
check=True,
capture_output=True,
text=True,
encoding="utf-8",
)
commands = json.loads(completed.stdout)
@@ -619,10 +985,12 @@ def test_left_sidebar_extracts_speech_and_peripheral_control_commands() -> None:
"}));"
)
completed = subprocess.run(
[node, "-e", script],
[node, "-"],
input=script,
check=True,
capture_output=True,
text=True,
encoding="utf-8",
)
output = json.loads(completed.stdout)
@@ -924,7 +1292,8 @@ def test_assistant_mode_preview_groups_validated_actions_for_the_card() -> None:
"process.stdout.write(JSON.stringify(modeDraftRows(draft, actions)));"
)
completed = subprocess.run(
[node, "-e", script],
[node, "-"],
input=script,
check=True,
capture_output=True,
text=True,
@@ -19,6 +19,57 @@ from cognitive_agent.application.codex_assistant import (
CONTROL_TERMS = ("空调", "座椅", "车窗", "遮阳帘", "氛围灯", "阅读灯", "出风口")
@pytest.mark.parametrize(("message", "mode_name"), [
("构建一个观影模式", "观影模式"),
("为主驾构建一个观影模式", "主驾观影模式"),
])
def test_local_first_callbacks_do_not_intercept_scoped_mode_draft_creation(message, mode_name):
client = StructuredSceneClient()
assistant = CodexAssistantService(client)
events = []
control_calls = []
def execute_control(instruction, _progress, plan):
control_calls.append((instruction, plan))
return _preview_result(mode_name)
result = assistant.stream(
message, conversation_id="conv-merged-scene-router", principal="test",
history=[], emit=events.append, execute_control=execute_control,
preview_control=lambda *_args: pytest.fail("mode previews must not partially execute via local-first controls"),
supplement_control=lambda *_args: pytest.fail("mode previews must not become unresolved control supplements"),
control_terms=CONTROL_TERMS,
capability_snapshot=_catalog_with_templates(_scene_template()),
scene_capability_context=_scene_context(),
)
assert result["success"] is True
assert result["route"] == "scene_template"
assert [instruction for instruction, _ in control_calls] == [message]
assert not client.structured_calls and not client.legacy_calls
def test_local_first_callbacks_preserve_schedule_stop_route_and_answer():
client = StructuredSceneClient()
assistant = CodexAssistantService(client)
events = []
result = assistant.stream(
"停止动态模式", conversation_id="conv-merged-schedule-stop", principal="test",
history=[], emit=events.append,
execute_control=lambda instruction, _progress: {
"request_id": "req-stop-merged", "utterance": instruction,
"status": "succeeded", "execution_mode": "dry_run",
"orchestration_route": "mode_schedule_stop",
"schedule_execution": {"status": "idle"},
},
preview_control=lambda *_args: pytest.fail("schedule stop must retain its scheduler route"),
supplement_control=lambda *_args: pytest.fail("schedule stop is not an unresolved control"),
control_terms=CONTROL_TERMS,
)
assert result["success"] is True
assert "已模拟停止动态模式" in result["answer"]
assert not client.structured_calls and not client.legacy_calls
def _structured_plan(
*,
temperature: float = 24,
@@ -798,6 +849,13 @@ def test_unknown_mode_uses_one_structured_turn_then_local_dry_run() -> None:
"default_scope": "passenger",
"applies_to": "seat capabilities in every phase",
}
expected_context["natural_language_routing"] = {
"original_utterance": "构建一个星夜模式",
"parsed_clauses": [],
"resolved_clauses": [],
"unresolved_clauses": ["构建一个星夜模式"],
"cloud_input_utterance": "构建一个星夜模式",
}
assert call["application_context"] == expected_context
assert call["output_schema"]["type"] == "object"
assert call["output_schema"]["additionalProperties"] is False
+476 -2
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from cognitive_agent.application.seat_service import SeatDemoService
@@ -47,6 +48,19 @@ def test_preview_validates_local_controls_without_calling_cloud(tmp_path: Path)
assert result["status"] == "succeeded"
assert result["supplement_required"] is True
assert result["unresolved_clauses"] == ["播放音乐"]
assert result["parsed_clauses"] == [
"空调温度设置为24度",
"风量设置为2档",
]
assert result["natural_language_routing"] == {
"original_utterance": "空调温度设置为24度,风量设置为2档,播放音乐",
"parsed_clauses": ["空调温度设置为24度", "风量设置为2档"],
"resolved_clauses": ["空调温度设置为24度", "风量设置为2档"],
"unresolved_clauses": ["播放音乐"],
}
assert result["local_control_budget_ms"] == 1000
assert result["local_control_elapsed_ms"] < 1000
assert result["local_control_within_budget"] is True
assert [item["capability"] for item in _operations(result)] == [
"climate.set_temperature",
"climate.set_fan_level",
@@ -58,6 +72,8 @@ def test_preview_validates_local_controls_without_calling_cloud(tmp_path: Path)
assert validated_events[0]["phase"] == "deterministic_commands_validated"
assert validated_events[0]["status"] == "processing"
assert validated_events[0]["execution_report"] is None
assert validated_events[0]["local_control_elapsed_ms"] < 1000
assert validated_events[0]["local_control_within_budget"] is True
assert _operations(validated_events[0]) == _operations(result)
assert validated_events[0]["command_plans"]
@@ -119,12 +135,90 @@ def test_fully_local_cabin_request_streams_all_commands_without_supplement(
assert _operations(validated_events[0]) == operations
def test_qualitative_window_position_keeps_adjacent_reading_light_clause(
tmp_path: Path,
) -> None:
service = _service(tmp_path)
cases = (
("空调 29℃、打开阅读灯、副驾车窗开到20%", 20),
("空调 29℃、打开阅读灯、副驾车窗开到一半", 50),
)
for index, (utterance, expected_position) in enumerate(cases):
result = service.preview_native_can(
utterance,
provider="local",
conversation_id=f"conv-reading-window-{index:02d}",
execution_mode="dry_run",
)
operations = _operations(result)
by_capability = {item["capability"]: item for item in operations}
assert result["unresolved_clauses"] == []
assert result["local_control_elapsed_ms"] < 1000
assert result["local_control_within_budget"] is True
assert result["parsed_clauses"] == [
"空调 29℃",
"打开阅读灯",
utterance.rsplit("、", 1)[-1],
]
assert set(by_capability) == {
"climate.set_temperature",
"interior_light.set_reading_power",
"window.set_position",
}
assert (
by_capability["interior_light.set_reading_power"]["target"]["zone"]
== "all"
)
assert by_capability["interior_light.set_reading_power"]["parameters"] == {
"state": "on"
}
assert (
by_capability["window.set_position"]["target"]["zone"]
== "front_right"
)
assert by_capability["window.set_position"]["parameters"] == {
"position_pct": expected_position
}
def test_local_window_parser_accepts_front_and_rear_row_side_aliases(
tmp_path: Path,
) -> None:
service = _service(tmp_path)
cases = (
("左前排车窗开到20%", "front_left", 20),
("右前排窗开到一半", "front_right", 50),
("左后排车窗开到35%", "rear_left", 35),
("右后排窗开到一半", "rear_right", 50),
)
for index, (utterance, expected_target, expected_position) in enumerate(cases):
result = service.preview_native_can(
utterance,
provider="local",
conversation_id=f"conv-window-alias-{index:02d}",
execution_mode="dry_run",
)
operations = _operations(result)
assert result["status"] == "succeeded"
assert result["unresolved_clauses"] == []
assert len(operations) == 1
assert operations[0]["capability"] == "window.set_position"
assert operations[0]["target"]["zone"] == expected_target
assert operations[0]["parameters"] == {
"position_pct": expected_position
}
def test_supplement_receives_only_unresolved_text_and_deduplicates_local_control(
tmp_path: Path,
) -> None:
service = _service(tmp_path)
preview = service.preview_native_can(
"观影时空调温度设置为24度并播放音乐",
"空调温度设置为24度并播放音乐",
provider="openai",
conversation_id="conv-two-stage-deduplicate",
execution_mode="dry_run",
@@ -169,7 +263,15 @@ def test_supplement_receives_only_unresolved_text_and_deduplicates_local_control
provider="openai",
)
assert interpret.call_args.args[0] == "观影时空调温度设置为24度并播放音乐"
assert interpret.call_args.args[0] == "播放音乐"
routing = interpret.call_args.args[1]["natural_language_routing"]
assert routing == {
"original_utterance": "空调温度设置为24度并播放音乐",
"parsed_clauses": ["空调温度设置为24度"],
"resolved_clauses": ["空调温度设置为24度"],
"unresolved_clauses": ["播放音乐"],
"cloud_input_utterance": "播放音乐",
}
operations = _operations(result)
assert [item["capability"] for item in operations].count(
"climate.set_temperature"
@@ -217,6 +319,11 @@ def test_frontend_two_stage_contract_and_source_badges() -> None:
assert 'item.deliveryLabel' in console
assert 'data-delivery-state="${escapeHtml(item.deliveryState)}"' in console
assert '"cognitive-agent:validated-can-commands"' in console
assert "function commandCorrelationIds(result = {})" in console
assert "result.preview_request_id" in console
assert "result.supplement_of" in console
assert '"local_control_elapsed"' in console
assert '"本地规则解析与下发"' in console
assert '.v2-can-command-source[data-parse-source="local_rule"]' in styles
assert '.v2-can-command-delivery[data-delivery-state="submitted"]' in styles
assert '"/api/can/commands"' in server
@@ -287,6 +394,7 @@ def test_unified_result_preserves_cloud_infrared_attribution() -> None:
"command_plans": {},
"operation_results": [],
"decision_summary": [],
"trace_events": [{"stage": "compile", "duration_ms": 3}],
}
infrared_result = {
"status": "succeeded",
@@ -296,6 +404,10 @@ def test_unified_result_preserves_cloud_infrared_attribution() -> None:
"command_plans": {},
"operation_results": [],
"decision_summary": [],
"trace_events": [
{"stage": "local_peripheral_plan", "duration_ms": 1},
{"stage": "nfc_peripheral_sequence", "duration_ms": 0},
],
}
entries = [
{
@@ -321,6 +433,368 @@ def test_unified_result_preserves_cloud_infrared_attribution() -> None:
assert merged["provider_used"] == "local_rules+gpt:gpt-5.6-luna"
assert merged["infrared_actions"][0]["parse_source"] == "cloud_model"
assert [event["stage"] for event in merged["trace_events"]] == [
"local_peripheral_plan",
"nfc_peripheral_sequence",
"compile",
]
def test_pure_infrared_request_publishes_compiled_commands() -> None:
handler = SeatDemoHandler.__new__(SeatDemoHandler)
handler.service = SimpleNamespace(
is_mode_profile_configuration_request=lambda _utterance: False,
conversations=SimpleNamespace(snapshot=lambda _conversation_id: {}),
persist_composed_result=lambda _result: None,
)
handler.extension = SimpleNamespace(list_peripheral_capabilities=lambda: [])
handler.nfc = SimpleNamespace(snapshot=lambda: {})
infrared_result = {
"request_id": "req-ir-only",
"utterance": "打开闪烁灯",
"status": "succeeded",
"function_configs": {
"self_extension": {
"spec": {
"domain": "self_extension",
"operations": [
{
"id": "infrared-1",
"capability": "infrared.emit",
}
],
}
}
},
"command_plans": {
"self_extension": {
"spec": {
"domain": "self_extension",
"commands": [
{
"operation_ref": "infrared-1",
"message": "infrared.emit",
"signals": {"slot": 1},
}
],
}
}
},
"trace_events": [
{"stage": "nfc_peripheral_route", "duration_ms": 0}
],
}
handler._execute_local_peripheral_actions = (
lambda _utterance, _entries, **_kwargs: infrared_result
)
validated_events: list[dict[str, object]] = []
partition = {
"peripheral_actions": [
{
"index": 0,
"clause": "打开闪烁灯",
"capability": {
"id": "toy-001-light-on",
"slot": 1,
},
}
],
"residual_utterance": "",
"unresolved_peripheral_clauses": [],
"denied_peripheral_actions": [],
}
with patch(
"cognitive_agent.server.partition_active_peripheral_intent",
return_value=partition,
):
result = handler._execute_request_with_peripherals(
"/api/requests/preview",
{
"utterance": "打开闪烁灯",
"provider": "local",
"execution_mode": "dry_run",
},
on_commands_validated=validated_events.append,
)
assert result is infrared_result
assert len(validated_events) == 2
assert validated_events[0]["status"] == "processing"
assert validated_events[0]["phase"] == "commands_validated"
assert validated_events[0]["command_plans"] == infrared_result["command_plans"]
assert validated_events[0]["trace_events"] == infrared_result["trace_events"]
assert validated_events[1] is infrared_result
def test_pure_unresolved_peripheral_returns_cloud_ready_local_preview(
tmp_path: Path,
) -> None:
handler = SeatDemoHandler.__new__(SeatDemoHandler)
handler.service = _service(tmp_path)
handler.extension = SimpleNamespace(list_peripheral_capabilities=lambda: [])
handler.nfc = SimpleNamespace(snapshot=lambda: {})
validated_events: list[dict[str, object]] = []
utterance = "让闪烁灯喜庆一点"
partition = {
"peripheral_actions": [],
"residual_utterance": "",
"unresolved_peripheral_clauses": [utterance],
"unresolved_candidates": [
{
"id": "light-festive",
"status": "learned",
"device_id": "toy-001",
"device_name": "闪烁灯",
"action": "喜庆灯效",
"slot": 2,
}
],
"denied_peripheral_actions": [],
}
with patch(
"cognitive_agent.server.partition_active_peripheral_intent",
return_value=partition,
):
result = handler._execute_request_with_peripherals(
"/api/requests/preview",
{
"utterance": utterance,
"provider": "local",
"conversation_id": "conv-unresolved-peripheral-preview",
"execution_mode": "dry_run",
},
on_commands_validated=validated_events.append,
)
assert result is not None
assert result["status"] == "no_action"
assert result["supplement_required"] is True
assert result["function_configs"] == {}
assert result["command_plans"] == {}
assert result["preview_request_id"] == result["request_id"]
assert result["local_control_budget_ms"] == 1000
assert result["local_control_elapsed_ms"] < 1000
assert result["local_control_within_budget"] is True
assert result["natural_language_routing"] == {
"original_utterance": utterance,
"parsed_clauses": [],
"resolved_clauses": [],
"unresolved_clauses": [utterance],
}
assert validated_events == [result]
assert (
handler.service.artifact_root
/ str(result["request_id"])
/ "result.json"
).is_file()
def test_mixed_request_finishes_all_local_phases_before_cloud_ir() -> None:
handler = SeatDemoHandler.__new__(SeatDemoHandler)
order: list[str] = []
validated_events: list[dict[str, object]] = []
local_capability = {
"id": "light-on",
"status": "learned",
"device_id": "toy-001",
"device_name": "闪烁灯",
"card_uid": "A1B2",
"action": "打开",
"slot": 1,
}
cloud_capability = {
**local_capability,
"id": "light-festive",
"action": "喜庆灯效",
"slot": 2,
}
def peripheral_result(
request_id: str,
capability: dict[str, object],
parse_source: str,
) -> dict[str, object]:
operation_id = str(capability["id"])
return {
"request_id": request_id,
"utterance": "空调温度设置到23度,打开闪烁灯,让闪烁灯喜庆一点",
"status": "succeeded",
"success": True,
"provider_used": (
"infrared_local_rules"
if parse_source == "local_rule"
else "gpt:test"
),
"execution_mode": "dry_run",
"control_transport": "gscu_dry_run",
"function_configs": {
"self_extension": {
"spec": {
"domain": "self_extension",
"operations": [
{
"id": operation_id,
"capability": "infrared.emit",
"parse_source": parse_source,
}
],
}
}
},
"command_plans": {
"self_extension": {
"spec": {
"domain": "self_extension",
"commands": [
{
"operation_ref": operation_id,
"message": "infrared.emit",
"signals": {"slot": capability["slot"]},
}
],
}
}
},
"trace_events": [],
"validation": [],
"operation_results": [],
"decision_summary": [],
"execution_report": None,
}
local_result = peripheral_result(
"req-local-ir",
local_capability,
"local_rule",
)
cloud_result = peripheral_result(
"req-cloud-ir",
cloud_capability,
"cloud_model",
)
can_result = {
"request_id": "req-local-can",
"utterance": "空调温度设置到23度",
"status": "succeeded",
"success": True,
"provider_used": "local_rules",
"execution_mode": "dry_run",
"control_transport": "wired_can",
"function_configs": {},
"command_plans": {},
"trace_events": [],
"validation": [],
"operation_results": [],
"decision_summary": [],
"execution_report": None,
"natural_language_routing": {
"original_utterance": "空调温度设置到23度,打开闪烁灯,让闪烁灯喜庆一点",
"parsed_clauses": ["打开闪烁灯", "空调温度设置到23度"],
"resolved_clauses": ["打开闪烁灯", "空调温度设置到23度"],
"unresolved_clauses": ["让闪烁灯喜庆一点"],
},
}
def preview_native_can(**kwargs: object) -> dict[str, object]:
order.append("local_can")
callback = kwargs.get("on_commands_validated")
if callable(callback):
callback(dict(can_result))
return dict(can_result)
handler.service = SimpleNamespace(
is_mode_profile_configuration_request=lambda _utterance: False,
conversations=SimpleNamespace(snapshot=lambda _conversation_id: {}),
preview_native_can=preview_native_can,
persist_composed_result=lambda _result: None,
)
def plan(payload: dict[str, object]) -> dict[str, object]:
order.append("cloud_plan")
assert payload["utterance"] == "让闪烁灯喜庆一点"
routing = payload["natural_language_routing"]
assert isinstance(routing, dict)
assert routing["original_utterance"] == (
"空调温度设置到23度,打开闪烁灯,让闪烁灯喜庆一点"
)
assert routing["parsed_clauses"] == [
"打开闪烁灯",
"空调温度设置到23度",
]
return {
"steps": [
{"kind": "peripheral", "capability_id": "light-festive"}
]
}
def execute_cloud(
_utterance: str,
_plan: dict[str, object],
catalog: list[dict[str, object]],
**_kwargs: object,
) -> dict[str, object]:
order.append("cloud_ir")
assert [item["id"] for item in catalog] == ["light-festive"]
return cloud_result
handler.extension = SimpleNamespace(
list_peripheral_capabilities=lambda: [],
plan=plan,
execute_peripheral_plan_request=execute_cloud,
)
handler.nfc = SimpleNamespace(snapshot=lambda: {})
def execute_local(
_utterance: str,
_entries: list[dict[str, object]],
**kwargs: object,
) -> dict[str, object]:
order.append("local_ir")
callback = kwargs.get("on_commands_validated")
if callable(callback):
callback(local_result)
return local_result
handler._execute_local_peripheral_actions = execute_local
partition = {
"peripheral_actions": [
{"index": 1, "clause": "打开闪烁灯", "capability": local_capability}
],
"residual_utterance": "空调温度设置到23度",
"unresolved_peripheral_clauses": ["让闪烁灯喜庆一点"],
"unresolved_candidates": [cloud_capability],
"denied_peripheral_actions": [],
}
with patch(
"cognitive_agent.server.partition_active_peripheral_intent",
return_value=partition,
):
result = handler._execute_request_with_peripherals(
"/api/requests/preview",
{
"utterance": (
"空调温度设置到23度,打开闪烁灯,让闪烁灯喜庆一点"
),
"provider": "auto",
"conversation_id": "conv-mixed-order",
"execution_mode": "dry_run",
},
on_commands_validated=validated_events.append,
)
assert order == ["local_ir", "local_can", "cloud_plan", "cloud_ir"]
assert result is not None
assert result["status"] == "succeeded"
assert any(event.get("local_control_within_budget") for event in validated_events)
operation_sources = {
operation["parse_source"]
for config in result["function_configs"].values()
for operation in config["spec"].get("operations", [])
}
assert operation_sources == {"local_rule", "cloud_model"}
def test_nfc_command_denial_keeps_allowed_result_and_reports_partial() -> None: