merge: integrate remote MQTT routing with seat-scoped dynamic modes
This commit is contained in:
@@ -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}});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user