feat: improve mode and peripheral orchestration
This commit is contained in:
@@ -560,6 +560,64 @@ test("mode creation always requests dry-run even while the console is in hardwar
|
||||
assert.equal(Object.hasOwn(request, "hardware_client_id"), false);
|
||||
});
|
||||
|
||||
test("hardware requests use only the console's validated hardware client id", async () => {
|
||||
const events = [
|
||||
{ type: "message.accepted", route: "mode_profile_execute", engine: "n7web-mode-profile-runner" },
|
||||
{ type: "answer.completed", route: "mode_profile_execute", answer: "阅读模式执行完成。" },
|
||||
];
|
||||
const validHarness = createHarness(events, "hardware");
|
||||
validHarness.sandbox.CognitiveAgentConsole.getHardwareClientId = (
|
||||
() => "hwc-runtime-client-1234"
|
||||
);
|
||||
validHarness.input.value = "进入阅读模式";
|
||||
|
||||
await validHarness.sandbox.CognitiveAgentAssistant.submit();
|
||||
|
||||
const validRequest = JSON.parse(validHarness.requestCalls[0].options.body);
|
||||
assert.equal(validRequest.hardware_client_id, "hwc-runtime-client-1234");
|
||||
|
||||
const invalidHarness = createHarness(events, "hardware");
|
||||
invalidHarness.sandbox.CognitiveAgentConsole.getHardwareClientId = (
|
||||
() => "legacy_invalid_client"
|
||||
);
|
||||
invalidHarness.input.value = "进入浪漫观影模式";
|
||||
|
||||
await invalidHarness.sandbox.CognitiveAgentAssistant.submit();
|
||||
|
||||
const invalidRequest = JSON.parse(invalidHarness.requestCalls[0].options.body);
|
||||
assert.equal(Object.hasOwn(invalidRequest, "hardware_client_id"), false);
|
||||
});
|
||||
|
||||
test("partially completed imported mode uses neutral delegated wording", () => {
|
||||
const harness = createHarness([]);
|
||||
|
||||
harness.sandbox.CognitiveAgentAssistant.announceModeExecution(
|
||||
{ id: "cinema", name: "影院模式", command_flow: { function_configs: {}, plans: {} } },
|
||||
{
|
||||
status: "partially_succeeded",
|
||||
execution_mode: "hardware",
|
||||
execution_outcome: "submitted_unverified",
|
||||
deferred_requests: [
|
||||
{ execution_backend: "n7_vehicle_transfer" },
|
||||
{ execution_backend: "n7_vehicle_transfer" },
|
||||
],
|
||||
function_configs: {},
|
||||
command_plans: {},
|
||||
execution_report: null,
|
||||
operation_results: [],
|
||||
},
|
||||
{ command: "进入影院模式" },
|
||||
);
|
||||
|
||||
const assistantMessage = harness.messages.children.at(-1);
|
||||
assert.equal(
|
||||
assistantMessage.dataset.messageContent,
|
||||
"影院模式指令已提交,未由 PC 执行的 2 项动作将由车机继续处理。",
|
||||
);
|
||||
const actionStatus = assistantMessage.querySelector(".assistant-action-status").children.at(-1);
|
||||
assert.equal(actionStatus.textContent, "功能模式指令已提交");
|
||||
});
|
||||
|
||||
test("a follow-up turn carries only the active authoritative draft reference", async () => {
|
||||
const events = modeDraftEvents("req-draft-one", 24);
|
||||
const harness = createHarness(events);
|
||||
|
||||
@@ -21,17 +21,17 @@ def test_console_contains_ai_assistant_surface():
|
||||
assert 'placeholder="向 AI助手提问或交代工作"' in template
|
||||
assert 'aria-label="发送消息"' in template
|
||||
assert 'import "../assistant-chat.css";' in vite_entry
|
||||
assert 'src="/assistant-chat.js?v=codex-assistant-35"' in html
|
||||
assert 'src="/assistant-chat.js?v=codex-assistant-39-video-media"' 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-v329-mode-preview"'
|
||||
'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"'
|
||||
in service_worker
|
||||
)
|
||||
assert '"/assistant-chat.css?v=codex-assistant-30"' in service_worker
|
||||
assert '"/assistant-chat.js?v=codex-assistant-35"' in service_worker
|
||||
assert '"/assistant-chat.js?v=codex-assistant-39-video-media"' in service_worker
|
||||
|
||||
|
||||
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
|
||||
@@ -95,6 +95,8 @@ def test_validated_controls_are_mirrored_into_a_compact_assistant_card():
|
||||
assert 'count.className = "assistant-dispatched-controls-count"' in script
|
||||
assert 'list.className = "assistant-dispatched-controls-list"' in script
|
||||
assert "value.textContent = assistantControlValue(item)" in script
|
||||
assert 'return ({ 1: "3档", 2: "1档" })[level]' in script
|
||||
assert '{ 1: "3档(高)", 2: "1档(低)" }' in script
|
||||
assert "语义控制指令:" in script
|
||||
assert "实际控制指令:" in script
|
||||
assert "appendDispatchedControls(message, event.payload)" in script
|
||||
@@ -105,6 +107,9 @@ def test_validated_controls_are_mirrored_into_a_compact_assistant_card():
|
||||
assert "appendDispatchedControls(message, modeExecutionPreviewResult(profile))" in script
|
||||
assert "beginModeExecution," in script
|
||||
assert "completeModeExecution," in script
|
||||
assert "功能模式部分完成" not in script
|
||||
assert "功能模式指令已提交" in script
|
||||
assert "未由 PC 执行的" in script
|
||||
assert "const operationByRef = new Map()" in script
|
||||
assert 'deliveryState: "submitting"' in script
|
||||
assert 'deliveryLabel: "正在下发"' in script
|
||||
@@ -113,6 +118,10 @@ def test_validated_controls_are_mirrored_into_a_compact_assistant_card():
|
||||
assert '"seat.set_legrest_height": "腿托"' in script
|
||||
assert '"seat.set_legrest_extension": "腿托"' in script
|
||||
assert '"ambient_light.set_illumination": "氛围灯颜色与亮度"' in script
|
||||
assert '"android.launch_app": "打开视频媒体"' in script
|
||||
assert 'if (capability === "android.launch_app")' in script
|
||||
assert "parameters.package_name" in script
|
||||
assert "parameters.activity_name" in script
|
||||
assert '.replaceAll("_", "-")' in script
|
||||
assert ".assistant-dispatched-controls > summary" in css
|
||||
assert ".assistant-dispatched-controls-list > li" in css
|
||||
@@ -130,6 +139,16 @@ def test_explicit_nfc_authorization_block_is_rendered_as_safety_blocked():
|
||||
assert '? "安全阻止"' in console
|
||||
|
||||
|
||||
def test_n7_video_launch_uses_chinese_mode_card_labels():
|
||||
app = (ROOT / "apps/web-console/app.js").read_text(encoding="utf-8")
|
||||
|
||||
assert 'android: "车机媒体"' in app
|
||||
assert 'headunit: "车机"' in app
|
||||
assert '"android.launch_app": "打开视频媒体"' in app
|
||||
assert 'package_name: "应用包名"' in app
|
||||
assert 'activity_name: "启动页面"' in app
|
||||
|
||||
|
||||
def test_latest_dispatched_control_group_is_the_only_group_expanded():
|
||||
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(
|
||||
encoding="utf-8"
|
||||
@@ -189,6 +208,18 @@ def test_codex_request_keeps_trusted_vehicle_context_without_legacy_model_routin
|
||||
assert 'const executionMode = modeConfigurationPreview ? "dry_run"' in request_body
|
||||
|
||||
|
||||
def test_codex_request_reuses_only_the_console_validated_hardware_client_id():
|
||||
app = (ROOT / "apps/web-console/app.js").read_text(encoding="utf-8")
|
||||
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
|
||||
assert "const HARDWARE_CLIENT_ID_PATTERN" in app
|
||||
assert "function validatedHardwareClientId()" in app
|
||||
assert "getHardwareClientId()" in app
|
||||
assert "return validatedHardwareClientId();" in app
|
||||
assert "window.CognitiveAgentConsole?.getHardwareClientId?.()" in assistant
|
||||
assert "/^hwc-[a-z0-9-]{8,80}$/.test(value)" in assistant
|
||||
|
||||
|
||||
def test_starting_a_new_codex_conversation_rotates_the_conversation_id():
|
||||
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
assert 'const CONVERSATION_STORAGE_KEY = "cognitive-agent.conversation-id"' in script
|
||||
|
||||
@@ -24,6 +24,14 @@ def compiled_mixed_result() -> dict[str, object]:
|
||||
"provider_used": "local_rules+infrared_local_rules",
|
||||
"execution_mode": "dry_run",
|
||||
"control_transport": "wired_can",
|
||||
"parallel_dispatch": {
|
||||
"enabled": True,
|
||||
"preflight_status": "succeeded",
|
||||
"dispatch": {
|
||||
"started_skew_ms": 1.2,
|
||||
"total_ms": 120.5,
|
||||
},
|
||||
},
|
||||
"function_configs": {
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
@@ -155,6 +163,10 @@ class ValidatedControlProjectionTest(unittest.TestCase):
|
||||
"compile",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
projection["parallel_dispatch"]["preflight_status"],
|
||||
"succeeded",
|
||||
)
|
||||
serialized = json.dumps(projection, ensure_ascii=False)
|
||||
self.assertNotIn("command_plans", serialized)
|
||||
self.assertNotIn("data_hex", serialized)
|
||||
@@ -362,6 +374,10 @@ class ValidatedControlProjectionTest(unittest.TestCase):
|
||||
)
|
||||
self.assertNotIn("command_plans", safe_result)
|
||||
self.assertNotIn("validated_control_commands", safe_result)
|
||||
self.assertEqual(
|
||||
safe_result["parallel_dispatch"]["preflight_status"],
|
||||
"succeeded",
|
||||
)
|
||||
|
||||
def test_agent_tool_path_queues_projection_for_the_browser_thread(self) -> None:
|
||||
client = type("UnavailableClient", (), {"available": False})()
|
||||
|
||||
@@ -724,6 +724,50 @@ class Can1SemanticWhitelistTests(unittest.TestCase):
|
||||
self.assertEqual(preview["unresolved_clauses"], [])
|
||||
self.assertEqual(self._debug_commands(preview), [])
|
||||
|
||||
def test_driver_ventilation_numeric_gear_followup_uses_occupant_scale(self) -> None:
|
||||
cases = (
|
||||
(
|
||||
"打开主驾座椅通风,挡位调到3档",
|
||||
"seat driver vent high",
|
||||
1,
|
||||
),
|
||||
(
|
||||
"打开主驾座椅通风,挡位调到1档",
|
||||
"seat driver vent low",
|
||||
2,
|
||||
),
|
||||
)
|
||||
for index, (utterance, command, internal_level) in enumerate(cases):
|
||||
with self.subTest(utterance=utterance):
|
||||
result = self.service.process(
|
||||
utterance,
|
||||
provider="local",
|
||||
conversation_id=f"conv-driver-vent-gear-{index}",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(self._debug_commands(result), [command])
|
||||
operation = result["function_configs"]["native_seat"]["spec"][
|
||||
"operations"
|
||||
][0]
|
||||
self.assertEqual(operation["parameters"]["level"], internal_level)
|
||||
|
||||
middle = self.service.process(
|
||||
"打开主驾座椅通风,挡位调到2档",
|
||||
provider="local",
|
||||
conversation_id="conv-driver-vent-gear-middle",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
self.assertEqual(middle["status"], "needs_confirmation")
|
||||
self.assertEqual(self._debug_commands(middle), [])
|
||||
self.assertIn(
|
||||
"driver_seat_thermal_middle_level_unsupported",
|
||||
{
|
||||
item.get("reason")
|
||||
for item in middle["intent_ir"]["spec"]["ambiguities"]
|
||||
},
|
||||
)
|
||||
|
||||
def test_vehicle_position_aliases_stay_scoped_to_the_device_domain(self) -> None:
|
||||
cases = (
|
||||
("打开左前车窗", ["window front-left open"]),
|
||||
|
||||
@@ -523,6 +523,56 @@ def test_registered_mode_execution_uses_fast_local_route_without_codex(
|
||||
assert "模拟编排" in result["answer"]
|
||||
|
||||
|
||||
def test_partially_supported_imported_mode_uses_submitted_not_partial_wording() -> None:
|
||||
client = FakeCodexClient(available=False)
|
||||
assistant = CodexAssistantService(client)
|
||||
catalog = {
|
||||
"modes": [
|
||||
{
|
||||
"id": "cinema",
|
||||
"name": "影院模式",
|
||||
"aliases": [],
|
||||
"assistant_callable": True,
|
||||
}
|
||||
],
|
||||
"source_status": {"modes": "ready"},
|
||||
}
|
||||
|
||||
def execute(instruction: str, progress: Callable[[dict[str, object]], None]):
|
||||
result = {
|
||||
"request_id": "req-imported-cinema-partial",
|
||||
"utterance": instruction,
|
||||
"status": "partially_succeeded",
|
||||
"execution_mode": "hardware",
|
||||
"execution_outcome": "submitted_unverified",
|
||||
"deferred_requests": [
|
||||
{
|
||||
"execution_backend": "n7_vehicle_transfer",
|
||||
"capability": "android.launch_app",
|
||||
}
|
||||
],
|
||||
}
|
||||
progress(result)
|
||||
return result
|
||||
|
||||
result, events = _stream(
|
||||
assistant,
|
||||
"进入影院模式",
|
||||
execute_control=execute,
|
||||
capability_snapshot=catalog,
|
||||
)
|
||||
|
||||
assert result["route"] == "mode_profile_execute"
|
||||
assert result["success"] is True
|
||||
assert "已提交" in result["answer"]
|
||||
assert "部分完成" not in result["answer"]
|
||||
assert "未能完成" not in result["answer"]
|
||||
assert any(
|
||||
event.get("display_text") == "功能模式已提交,等待车辆状态确认"
|
||||
for event in events
|
||||
)
|
||||
|
||||
|
||||
def test_registered_name_only_mode_keeps_the_fast_local_route_without_codex() -> None:
|
||||
client = FakeCodexClient(available=False)
|
||||
assistant = CodexAssistantService(client)
|
||||
|
||||
@@ -554,6 +554,48 @@ class DriverSeatNativeCanTests(unittest.TestCase):
|
||||
self.assertEqual(bus.send_calls, 4)
|
||||
self.assertEqual(len(bus.sent), 3)
|
||||
|
||||
def test_window_already_at_target_is_verified_success_without_resend(self) -> None:
|
||||
config = self.runtime.build_function_config(
|
||||
"window",
|
||||
[
|
||||
{
|
||||
"domain": "window",
|
||||
"capability": "window.set_position",
|
||||
"target": "rear_left",
|
||||
"operation": "set",
|
||||
"parameters": {"position_pct": 0},
|
||||
}
|
||||
],
|
||||
"req-window-already-closed",
|
||||
"local_rules",
|
||||
)
|
||||
plan = self.runtime.compile(config)
|
||||
feedback = CanFrame(
|
||||
0x388,
|
||||
bytes.fromhex("00 00 01 00 01 00 66 00"),
|
||||
channel=1,
|
||||
is_fd=True,
|
||||
bitrate_switch=True,
|
||||
)
|
||||
bus = _FakeBus([feedback])
|
||||
|
||||
report = NativeCanExecutor(
|
||||
lambda: bus,
|
||||
feedback_timeout_ms=1,
|
||||
feedback_required=False,
|
||||
).execute(self.runtime, plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
operation = report["spec"]["results"][0]
|
||||
self.assertEqual(operation["status"], "succeeded")
|
||||
self.assertEqual(operation["telemetry"]["sent_frames"], [])
|
||||
self.assertEqual(
|
||||
operation["telemetry"]["verification_status"],
|
||||
"verified_success",
|
||||
)
|
||||
self.assertEqual(operation["telemetry"]["delivery_status"], "not_required")
|
||||
self.assertTrue(operation["telemetry"]["physical_effect_confirmed"])
|
||||
|
||||
def test_memory_compile_rejects_missing_or_malformed_learned_slots(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "driver_memory.json"
|
||||
|
||||
@@ -37,11 +37,11 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
|
||||
assert f'"{asset}"' in service_worker
|
||||
assert 'import "../energy-monitor.css";' in vite_entry
|
||||
assert "/energy-monitor.js?v=energy-monitor-5-performance" in html
|
||||
assert html.index("/assistant-chat.js?v=codex-assistant-35") < html.index(
|
||||
assert html.index("/assistant-chat.js?v=codex-assistant-39-video-media") < html.index(
|
||||
"/energy-monitor.js?v=energy-monitor-5-performance"
|
||||
)
|
||||
assert (
|
||||
'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"'
|
||||
'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"'
|
||||
in service_worker
|
||||
)
|
||||
|
||||
|
||||
@@ -157,6 +157,42 @@ def test_authorization_requires_exact_enabled_learned_bound_ability() -> None:
|
||||
authorize_extension_control_actions(parsed, disallowed)
|
||||
|
||||
|
||||
def test_authorization_injects_same_device_power_on_before_color() -> None:
|
||||
parsed = parse_extension_control_payload(
|
||||
encoded(request(action(3, "切换为红色")))
|
||||
)
|
||||
catalog = [
|
||||
capability("light-on", 2, "打开"),
|
||||
capability("light-red", 3, "切换为红色"),
|
||||
]
|
||||
|
||||
authorized = authorize_extension_control_actions(parsed, catalog)
|
||||
|
||||
ordered = sorted(
|
||||
authorized,
|
||||
key=lambda item: (item["at_ms"], item["sequence_order"]),
|
||||
)
|
||||
assert [item["action"]["parameters"]["slot"] for item in ordered] == [2, 3]
|
||||
assert ordered[0]["auto_inserted"] is True
|
||||
assert ordered[0]["dependency"] == "power_on_before_color"
|
||||
assert ordered[1]["auto_inserted"] is False
|
||||
|
||||
|
||||
def test_authorization_rejects_color_without_same_device_power_on() -> None:
|
||||
parsed = parse_extension_control_payload(
|
||||
encoded(request(action(3, "切换为红色")))
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
ExtensionControlAuthorizationError,
|
||||
match="power-on action",
|
||||
):
|
||||
authorize_extension_control_actions(
|
||||
parsed,
|
||||
[capability("light-red", 3, "切换为红色")],
|
||||
)
|
||||
|
||||
|
||||
def test_consumer_validates_entire_request_then_executes_by_at_ms() -> None:
|
||||
catalog = [
|
||||
capability("light-on", 2, "打开"),
|
||||
@@ -184,15 +220,21 @@ def test_consumer_validates_entire_request_then_executes_by_at_ms() -> None:
|
||||
)
|
||||
)
|
||||
assert wait_until(lambda: consumer.snapshot()["state"] == "completed")
|
||||
assert [item[1]["parameters"]["slot"] for item in executions] == [3, 2]
|
||||
assert [item[1]["parameters"]["slot"] for item in executions] == [2, 3, 2]
|
||||
status = consumer.snapshot()
|
||||
assert status["processed_count"] == 1
|
||||
assert status["executed_action_count"] == 2
|
||||
assert [item["slot"] for item in status["last_results"]] == [3, 2]
|
||||
assert [item["slot"] for item in status["action_statuses"]] == [3, 2]
|
||||
assert status["executed_action_count"] == 3
|
||||
assert [item["slot"] for item in status["last_results"]] == [2, 3, 2]
|
||||
assert [item["slot"] for item in status["action_statuses"]] == [2, 3, 2]
|
||||
assert [item["auto_inserted"] for item in status["action_statuses"]] == [
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
]
|
||||
assert [item["state"] for item in status["action_statuses"]] == [
|
||||
"submitted_unverified",
|
||||
"submitted_unverified",
|
||||
"submitted_unverified",
|
||||
]
|
||||
assert all(
|
||||
item["state_label"] == "已下发 · 物理效果未确认"
|
||||
|
||||
@@ -92,12 +92,12 @@ def test_performance_assets_have_a_new_cache_generation() -> None:
|
||||
worker = source("service-worker.js")
|
||||
|
||||
for asset in (
|
||||
"/app.js?v=dual-seat-145-n7-dispatch",
|
||||
"/console-v2.js?v=local-first-77-performance",
|
||||
"/self-extension.js?v=nfc-mqtt-56",
|
||||
"/app.js?v=dual-seat-147-video-media",
|
||||
"/console-v2.js?v=local-first-79-cinema-status",
|
||||
"/self-extension.js?v=nfc-persistent-57",
|
||||
"/energy-monitor.js?v=energy-monitor-5-performance",
|
||||
):
|
||||
assert asset in html
|
||||
assert f'"{asset}"' in worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"' in worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"' in worker
|
||||
assert '"/codepen-glass-theme.css?v=creative-cloud-6-performance"' in worker
|
||||
|
||||
@@ -54,7 +54,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
|
||||
assert source_asset in main
|
||||
assert f'"{cached_asset}"' in service_worker
|
||||
assert 'import "./styles/design-system.css"' in main
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"' in service_worker
|
||||
|
||||
|
||||
def test_reference_glass_theme_covers_the_complete_workspace() -> None:
|
||||
|
||||
@@ -205,6 +205,55 @@ def cinema_scene_card() -> dict[str, object]:
|
||||
}
|
||||
|
||||
|
||||
def cinema_peripheral_catalog() -> list[dict[str, object]]:
|
||||
def capability(
|
||||
identity: str,
|
||||
device_id: str,
|
||||
device_name: str,
|
||||
action: str,
|
||||
slot: int,
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"id": identity,
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": device_id,
|
||||
"device_name": device_name,
|
||||
"device_aliases": [device_name],
|
||||
"object_names": [device_name],
|
||||
"card_uid": "2CC8B103" if "flash" in device_id else "1A3AB103",
|
||||
"action": action,
|
||||
"aliases": [action],
|
||||
"action_names": [action],
|
||||
"risk": "low",
|
||||
"slot": slot,
|
||||
}
|
||||
|
||||
return [
|
||||
capability("flash-on", "flash-light", "闪烁灯", "打开", 1),
|
||||
capability("flash-blue", "flash-light", "闪烁灯", "切换为蓝色", 3),
|
||||
capability("flash-red", "flash-light", "闪烁灯", "切换为红色", 4),
|
||||
capability("strip-on", "light-strip", "灯带", "打开", 21),
|
||||
capability("strip-blue", "light-strip", "灯带", "切换为蓝色", 23),
|
||||
capability("strip-red", "light-strip", "灯带", "切换为红色", 24),
|
||||
]
|
||||
|
||||
|
||||
def cinema_video_launch_action() -> dict[str, object]:
|
||||
return {
|
||||
"type": "android_app_launch",
|
||||
"at_ms": 3000,
|
||||
"domain": "android",
|
||||
"capability": "android.launch_app",
|
||||
"target_scope": "headunit",
|
||||
"operation": "launch",
|
||||
"parameters": {
|
||||
"activity_name": "com.byd.videoplay.activity.WelComeActivity",
|
||||
"package_name": "com.byd.videoplay",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def wait_until(predicate, timeout: float = 1.5) -> bool:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
@@ -337,6 +386,269 @@ def test_import_upserts_yaml_refreshes_catalog_and_does_not_echo(
|
||||
assert "upstream_origin: user" in yaml_text
|
||||
|
||||
|
||||
def test_later_n7_cinema_mode_inherits_base_and_adds_external_light_colors(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = configured_service(tmp_path)
|
||||
service.set_mode_scene_peripheral_catalog_provider(cinema_peripheral_catalog)
|
||||
base = cinema_scene_card()
|
||||
base_action_count = len(base["actions"])
|
||||
base_result = service.import_n7_mode_scene(base)
|
||||
assert "cinema_augmentation" not in base_result
|
||||
|
||||
variant = cinema_scene_card()
|
||||
variant.update(
|
||||
id="blue-cinema",
|
||||
label="蓝色影院模式",
|
||||
name="蓝色影院模式",
|
||||
aliases=["蓝色观影模式"],
|
||||
description="在车机影院模式基础上增加蓝色外设灯光。",
|
||||
domains=["climate"],
|
||||
operation_count=1,
|
||||
actions=[
|
||||
{
|
||||
"type": "climate_temperature",
|
||||
"at_ms": 0,
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": {"temperature_c": 23},
|
||||
}
|
||||
],
|
||||
profile_revision="blue-cinema-revision",
|
||||
)
|
||||
|
||||
imported = service.import_n7_mode_scene(variant)
|
||||
augmentation = imported["cinema_augmentation"]
|
||||
assert augmentation == {
|
||||
"base_scene_id": "cinema",
|
||||
"base_action_count": base_action_count,
|
||||
"incoming_action_count": 1,
|
||||
"peripheral_action_count": 4,
|
||||
"merged_action_count": base_action_count + 4,
|
||||
}
|
||||
|
||||
profile = service.get_mode_profile_card("blue-cinema", "denza_n7")
|
||||
source_scene = profile.command_flow["source_scene"]
|
||||
actions = source_scene["actions"]
|
||||
assert source_scene["operation_count"] == base_action_count + 4
|
||||
assert "self_extension" in source_scene["domains"]
|
||||
temperatures = [
|
||||
action["parameters"]["temperature_c"]
|
||||
for action in actions
|
||||
if action["capability"] == "climate.set_temperature"
|
||||
]
|
||||
assert temperatures == [23]
|
||||
peripheral_actions = [
|
||||
action
|
||||
for action in actions
|
||||
if action["capability"] == "infrared.emit"
|
||||
]
|
||||
assert [
|
||||
(
|
||||
action["target_scope"],
|
||||
action["parameters"]["action"],
|
||||
action["parameters"]["slot"],
|
||||
)
|
||||
for action in peripheral_actions
|
||||
] == [
|
||||
("flash-light", "打开", 1),
|
||||
("flash-light", "切换为蓝色", 3),
|
||||
("light-strip", "打开", 21),
|
||||
("light-strip", "切换为蓝色", 23),
|
||||
]
|
||||
|
||||
repeated = service.import_n7_mode_scene(variant)
|
||||
assert (
|
||||
repeated["cinema_augmentation"]["merged_action_count"]
|
||||
== base_action_count + 4
|
||||
)
|
||||
reloaded = service.get_mode_profile_card("blue-cinema", "denza_n7")
|
||||
assert (
|
||||
len(reloaded.command_flow["source_scene"]["actions"])
|
||||
== base_action_count + 4
|
||||
)
|
||||
|
||||
|
||||
def test_n7_cinema_variant_is_not_augmented_before_base_arrives(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = configured_service(tmp_path)
|
||||
service.set_mode_scene_peripheral_catalog_provider(cinema_peripheral_catalog)
|
||||
variant = cinema_scene_card()
|
||||
variant.update(
|
||||
id="family-movie",
|
||||
name="家庭影院模式",
|
||||
label="家庭影院模式",
|
||||
profile_revision="family-movie-revision",
|
||||
)
|
||||
|
||||
imported = service.import_n7_mode_scene(variant)
|
||||
|
||||
assert "cinema_augmentation" not in imported
|
||||
profile = service.get_mode_profile_card("family-movie", "denza_n7")
|
||||
assert not any(
|
||||
operation.capability == "infrared.emit"
|
||||
for operation in profile.operations
|
||||
)
|
||||
|
||||
|
||||
def test_created_romantic_cinema_persists_and_transfers_n7_video_launch(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = configured_service(tmp_path)
|
||||
base = cinema_scene_card()
|
||||
base["actions"] = [*base["actions"], cinema_video_launch_action()]
|
||||
base["operation_count"] = len(base["actions"])
|
||||
base["domains"] = [*base["domains"], "android"]
|
||||
service.import_n7_mode_scene(base)
|
||||
|
||||
request_id = "req-romantic-cinema-video"
|
||||
instruction = "进入浪漫观影模式"
|
||||
result_dir = service.artifact_root / request_id
|
||||
result_dir.mkdir(parents=True)
|
||||
(result_dir / "result.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"request_id": request_id,
|
||||
"status": "succeeded",
|
||||
"execution_mode": "dry_run",
|
||||
"control_transport": "wireless_mqtt",
|
||||
"utterance": instruction,
|
||||
"vehicle": {"id": "denza_n7"},
|
||||
"function_configs": {
|
||||
"climate": {
|
||||
"spec": {
|
||||
"domain": "climate",
|
||||
"operations": [
|
||||
{
|
||||
"id": "vehicle-climate-power",
|
||||
"capability": "climate.set_power",
|
||||
"target": {"zone": "vehicle"},
|
||||
"parameters": {"state": "on"},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"command_plans": {
|
||||
"climate": {
|
||||
"spec": {
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": "vehicle-climate-power",
|
||||
"message": "climate.set_power",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
saved = service.create_mode_profile_card(
|
||||
"浪漫观影模式",
|
||||
instruction,
|
||||
request_id,
|
||||
"denza_n7",
|
||||
)
|
||||
profile = service.get_mode_profile_card(
|
||||
str(saved["mode_profile"]["id"]),
|
||||
"denza_n7",
|
||||
)
|
||||
|
||||
assert [
|
||||
operation.capability for operation in profile.operations
|
||||
] == ["climate.set_power", "android.launch_app"]
|
||||
assert profile.command_flow["n7_transfer_actions"] == [
|
||||
cinema_video_launch_action()
|
||||
]
|
||||
local_requests, transfer_actions = service._mode_requests_and_n7_transfers(
|
||||
profile
|
||||
)
|
||||
assert [request["capability"] for request in local_requests] == [
|
||||
"climate.set_power"
|
||||
]
|
||||
assert transfer_actions == [cinema_video_launch_action()]
|
||||
|
||||
exported = next(
|
||||
scene
|
||||
for scene in service.mode_scene_catalog("denza_n7")["scenes"]
|
||||
if scene["id"] == profile.id
|
||||
)
|
||||
video = next(
|
||||
action
|
||||
for action in exported["actions"]
|
||||
if action["capability"] == "android.launch_app"
|
||||
)
|
||||
assert video == cinema_video_launch_action()
|
||||
|
||||
|
||||
def test_existing_local_cinema_mode_publishes_video_action_only_in_hardware(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = configured_service(tmp_path)
|
||||
base = cinema_scene_card()
|
||||
base["actions"] = [*base["actions"], cinema_video_launch_action()]
|
||||
base["operation_count"] = len(base["actions"])
|
||||
base["domains"] = [*base["domains"], "android"]
|
||||
service.import_n7_mode_scene(base)
|
||||
profile = service.mode_profiles.save_mode(
|
||||
"浪漫观影模式",
|
||||
[
|
||||
{
|
||||
"domain": "android",
|
||||
"capability": "android.launch_app",
|
||||
"target_scope": "headunit",
|
||||
"operation": "launch",
|
||||
"parameters": deepcopy(cinema_video_launch_action()["parameters"]),
|
||||
}
|
||||
],
|
||||
vehicle_id="denza_n7",
|
||||
instruction="进入浪漫观影模式",
|
||||
command_flow={
|
||||
"schema": "cognitive-agent-mode-command-flow/v1",
|
||||
"plans": {"saved": {"spec": {"commands": [{"message": "saved"}]}}},
|
||||
},
|
||||
)
|
||||
publisher = MagicMock(
|
||||
return_value={
|
||||
"topic": "transto_n7/ctl",
|
||||
"qos": 1,
|
||||
"retain": False,
|
||||
"delivery_status": "published",
|
||||
"message_id": 19,
|
||||
}
|
||||
)
|
||||
service._vehicle_mqtt_transport.publish_n7_control_transfer = publisher
|
||||
|
||||
preview = service.execute_mode_profile_card(
|
||||
profile.id,
|
||||
execution_mode="dry_run",
|
||||
control_transport="wireless_mqtt",
|
||||
)
|
||||
publisher.assert_not_called()
|
||||
assert preview["n7_transfer_candidates"] == [cinema_video_launch_action()]
|
||||
|
||||
executed = service.execute_mode_profile_card(
|
||||
profile.id,
|
||||
execution_mode="hardware",
|
||||
control_transport="wireless_mqtt",
|
||||
)
|
||||
|
||||
assert executed["n7_control_transfer"]["state"] == "published"
|
||||
publisher.assert_called_once()
|
||||
payload = publisher.call_args.args[0]
|
||||
assert payload["scene_id"] == profile.id
|
||||
assert payload["scene_name"] == "浪漫观影模式"
|
||||
assert payload["operation_count"] == 1
|
||||
assert payload["actions"] == [cinema_video_launch_action()]
|
||||
|
||||
|
||||
def test_unknown_n7_action_is_forwarded_once_only_during_hardware_execution(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None:
|
||||
vite_entry = source("src/main.js")
|
||||
|
||||
assert 'data-open-nfc-binding' in script
|
||||
assert '靠近读卡器后' in script
|
||||
assert '请将 NFC 卡靠近读卡器' in script
|
||||
assert '<h2>NFC 设备授权</h2>' in script
|
||||
assert 'NFC 设备授权' in script
|
||||
assert 'api("nfc/status")' in script
|
||||
@@ -55,23 +55,25 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None:
|
||||
assert '学习指令已下发,但物理槽位回读为空' in script
|
||||
assert 'signalSource: "n7web"' in script
|
||||
assert 'if (capability.signalSource === "n7web") return' in script
|
||||
assert 'const activeCatalog = sensedCardUid' in script
|
||||
assert 'normalizeCardUid(item.card_uid) === sensedCardUid' in script
|
||||
assert 'const persistentCatalog = (' in script
|
||||
assert 'durablePeripheralCatalogHydrated' in script
|
||||
assert 'durablePeripheralCatalog' in script
|
||||
assert 'normalizeCardUid(item.card_uid)' in script
|
||||
assert '&& item.status === "learned"' in script
|
||||
assert 'slot: capability.slot' in script
|
||||
assert 'device_aliases: device?.aliases || []' in script
|
||||
assert 'async function hydratePeripheralCatalog()' in script
|
||||
assert 'api("peripherals")' in script
|
||||
assert 'signalSource: "backend_catalog"' in script
|
||||
assert '当前未感应到外设' in script
|
||||
assert '将已绑定设备 ID 的 NFC 卡靠近读卡器后显示命令' in script
|
||||
assert '当前没有已绑定外设' in script
|
||||
assert '外设绑定 NFC 后将持续显示,解除绑定后移除' in script
|
||||
|
||||
assert '.v2-nfc-inline' in styles
|
||||
assert '.v2-nfc-dialog' in styles
|
||||
assert '.v2-nfc-command-list' in styles
|
||||
assert '.v2-nfc-command-option' in styles
|
||||
assert '.v2-nfc-select-all' in styles
|
||||
assert 'self-extension.js?v=nfc-mqtt-56' in html
|
||||
assert 'self-extension.js?v=nfc-persistent-57' in html
|
||||
assert 'import "../self-extension.css";' in vite_entry
|
||||
|
||||
|
||||
@@ -219,5 +221,5 @@ def test_main_parsed_control_panel_renders_validated_infrared_operations() -> No
|
||||
assert "下发控制指令" in console
|
||||
assert "已解析控制指令" not in console
|
||||
assert 'li[data-can-channel="IR"]' in styles
|
||||
assert "console-v2.js?v=local-first-77-performance" in html
|
||||
assert "console-v2.js?v=local-first-79-cinema-status" in html
|
||||
assert 'speech_semantic: "语音语义"' in console
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cognitive_agent.server import SeatDemoHandler
|
||||
|
||||
|
||||
def _result(mode: str, *, status: str = "succeeded") -> dict[str, object]:
|
||||
return {
|
||||
"request_id": f"req-{mode}-{status}",
|
||||
"status": status,
|
||||
"success": status == "succeeded",
|
||||
"execution_mode": mode,
|
||||
"decision_summary": [],
|
||||
"function_configs": {},
|
||||
"command_plans": {},
|
||||
"trace_events": [],
|
||||
"operation_results": [],
|
||||
"execution_report": None,
|
||||
"control_transport": "test",
|
||||
}
|
||||
|
||||
|
||||
def _entries() -> list[dict[str, object]]:
|
||||
return [
|
||||
{
|
||||
"index": 0,
|
||||
"clause": "打开闪烁灯",
|
||||
"capability": {
|
||||
"id": "ir-slot:flash:2",
|
||||
"device_id": "flash",
|
||||
"device_name": "闪烁灯",
|
||||
"action": "打开",
|
||||
"slot": 2,
|
||||
"parse_source": "local_rule",
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_hardware_branches_preflight_then_start_together() -> None:
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
lock = threading.Lock()
|
||||
preflight_completed: set[str] = set()
|
||||
hardware_started: dict[str, float] = {}
|
||||
|
||||
def mark_preflight(name: str) -> dict[str, object]:
|
||||
time.sleep(0.03)
|
||||
with lock:
|
||||
preflight_completed.add(name)
|
||||
return _result("dry_run")
|
||||
|
||||
def execute_can() -> dict[str, object]:
|
||||
with lock:
|
||||
assert preflight_completed == {"can", "peripheral"}
|
||||
hardware_started["can"] = time.perf_counter()
|
||||
time.sleep(0.12)
|
||||
return _result("hardware")
|
||||
|
||||
def execute_peripheral(
|
||||
_utterance: str,
|
||||
_entries_value: list[dict[str, object]],
|
||||
*,
|
||||
execution_mode: str,
|
||||
**_kwargs: object,
|
||||
) -> dict[str, object]:
|
||||
if execution_mode == "dry_run":
|
||||
return mark_preflight("peripheral")
|
||||
with lock:
|
||||
assert preflight_completed == {"can", "peripheral"}
|
||||
hardware_started["peripheral"] = time.perf_counter()
|
||||
time.sleep(0.12)
|
||||
return _result("hardware")
|
||||
|
||||
with patch.object(
|
||||
handler,
|
||||
"_execute_local_peripheral_actions",
|
||||
side_effect=execute_peripheral,
|
||||
):
|
||||
result = handler._execute_can_and_peripheral_branches(
|
||||
utterance="打开空调和闪烁灯",
|
||||
entries=_entries(),
|
||||
execution_mode="hardware",
|
||||
provider="local",
|
||||
conversation_id="conv-parallel-unit",
|
||||
can_execute=execute_can,
|
||||
can_preflight=lambda: mark_preflight("can"),
|
||||
parallel_eligible=True,
|
||||
)
|
||||
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["parallel_dispatch"]["enabled"] is True
|
||||
assert result["parallel_dispatch"]["preflight_status"] == "succeeded"
|
||||
assert abs(hardware_started["can"] - hardware_started["peripheral"]) < 0.05
|
||||
dispatch = result["parallel_dispatch"]["dispatch"]
|
||||
branch_durations = [
|
||||
item["duration_ms"] for item in dispatch["branches"].values()
|
||||
]
|
||||
assert dispatch["total_ms"] < sum(branch_durations)
|
||||
|
||||
|
||||
def test_failed_preflight_starts_neither_hardware_branch() -> None:
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
hardware_calls: list[str] = []
|
||||
|
||||
def execute_peripheral(
|
||||
_utterance: str,
|
||||
_entries_value: list[dict[str, object]],
|
||||
*,
|
||||
execution_mode: str,
|
||||
**_kwargs: object,
|
||||
) -> dict[str, object]:
|
||||
if execution_mode == "hardware":
|
||||
hardware_calls.append("peripheral")
|
||||
return _result(execution_mode)
|
||||
|
||||
with patch.object(
|
||||
handler,
|
||||
"_execute_local_peripheral_actions",
|
||||
side_effect=execute_peripheral,
|
||||
):
|
||||
result = handler._execute_can_and_peripheral_branches(
|
||||
utterance="打开空调和闪烁灯",
|
||||
entries=_entries(),
|
||||
execution_mode="hardware",
|
||||
provider="local",
|
||||
conversation_id="conv-parallel-preflight-failure",
|
||||
can_execute=lambda: hardware_calls.append("can") or _result("hardware"),
|
||||
can_preflight=lambda: _result(
|
||||
"dry_run",
|
||||
status="validation_failed",
|
||||
),
|
||||
parallel_eligible=True,
|
||||
)
|
||||
|
||||
assert hardware_calls == []
|
||||
assert result["status"] == "validation_failed"
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
assert result["parallel_dispatch"]["enabled"] is False
|
||||
assert result["parallel_dispatch"]["preflight_status"] == "failed"
|
||||
|
||||
|
||||
def test_wireless_parallel_dispatch_does_not_require_local_bench_lease() -> None:
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
authorization_calls: list[str | None] = []
|
||||
handler.service = SimpleNamespace(
|
||||
hardware_access=SimpleNamespace(
|
||||
ensure_authorized=lambda client_id: authorization_calls.append(client_id)
|
||||
)
|
||||
)
|
||||
|
||||
handler._ensure_parallel_hardware_authorized(
|
||||
execution_mode="hardware",
|
||||
control_transport="wireless_mqtt",
|
||||
hardware_access_bypass=False,
|
||||
hardware_client_id=None,
|
||||
)
|
||||
|
||||
assert authorization_calls == []
|
||||
|
||||
|
||||
def test_wired_parallel_dispatch_keeps_local_bench_lease_gate() -> None:
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
authorization_calls: list[str | None] = []
|
||||
handler.service = SimpleNamespace(
|
||||
hardware_access=SimpleNamespace(
|
||||
ensure_authorized=lambda client_id: authorization_calls.append(client_id)
|
||||
)
|
||||
)
|
||||
|
||||
handler._ensure_parallel_hardware_authorized(
|
||||
execution_mode="hardware",
|
||||
control_transport="wired_can",
|
||||
hardware_access_bypass=False,
|
||||
hardware_client_id="hwc-wired-parallel-test",
|
||||
)
|
||||
|
||||
assert authorization_calls == ["hwc-wired-parallel-test"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"utterance",
|
||||
(
|
||||
"先打开空调,再打开闪烁灯",
|
||||
"打开空调,然后打开闪烁灯",
|
||||
"打开空调,2秒后打开闪烁灯",
|
||||
),
|
||||
)
|
||||
def test_explicit_cross_transport_order_disables_parallelism(
|
||||
utterance: str,
|
||||
) -> None:
|
||||
assert SeatDemoHandler._mixed_request_requires_transport_order(utterance)
|
||||
|
||||
|
||||
def test_ineligible_flow_keeps_existing_can_then_peripheral_order() -> None:
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
calls: list[str] = []
|
||||
|
||||
def execute_peripheral(
|
||||
_utterance: str,
|
||||
_entries_value: list[dict[str, object]],
|
||||
*,
|
||||
execution_mode: str,
|
||||
**_kwargs: object,
|
||||
) -> dict[str, object]:
|
||||
calls.append(f"peripheral:{execution_mode}")
|
||||
return _result(execution_mode)
|
||||
|
||||
def unexpected_preflight() -> dict[str, object]:
|
||||
raise AssertionError("ordered flow must not run a duplicate preflight")
|
||||
|
||||
with patch.object(
|
||||
handler,
|
||||
"_execute_local_peripheral_actions",
|
||||
side_effect=execute_peripheral,
|
||||
):
|
||||
result = handler._execute_can_and_peripheral_branches(
|
||||
utterance="先打开空调,再打开闪烁灯",
|
||||
entries=_entries(),
|
||||
execution_mode="hardware",
|
||||
provider="local",
|
||||
conversation_id="conv-ordered-unit",
|
||||
can_execute=lambda: calls.append("can:hardware") or _result("hardware"),
|
||||
can_preflight=unexpected_preflight,
|
||||
parallel_eligible=False,
|
||||
)
|
||||
|
||||
assert calls == ["can:hardware", "peripheral:hardware"]
|
||||
assert result["status"] == "succeeded"
|
||||
assert "parallel_dispatch" not in result
|
||||
|
||||
|
||||
def test_saved_static_mode_uses_parallel_can_and_peripheral_path() -> None:
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
can_modes: list[str] = []
|
||||
peripheral_modes: list[str] = []
|
||||
persisted: list[dict[str, object]] = []
|
||||
profile = SimpleNamespace(
|
||||
id="mode-parallel",
|
||||
schedule=None,
|
||||
command_flow={"execution_steps": []},
|
||||
public_dict=lambda: {"id": "mode-parallel", "name": "并行模式"},
|
||||
)
|
||||
|
||||
def execute_mode_profile_card(
|
||||
_mode_id: str,
|
||||
**kwargs: object,
|
||||
) -> dict[str, object]:
|
||||
mode = str(kwargs.get("execution_mode") or "")
|
||||
can_modes.append(mode)
|
||||
return _result(mode)
|
||||
|
||||
handler.service = SimpleNamespace(
|
||||
execute_mode_profile_card=execute_mode_profile_card,
|
||||
persist_composed_result=lambda result: persisted.append(result),
|
||||
)
|
||||
|
||||
def execute_peripheral(
|
||||
_utterance: str,
|
||||
_entries_value: list[dict[str, object]],
|
||||
*,
|
||||
execution_mode: str,
|
||||
**_kwargs: object,
|
||||
) -> dict[str, object]:
|
||||
peripheral_modes.append(execution_mode)
|
||||
return _result(execution_mode)
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
handler,
|
||||
"_exact_local_peripheral_mode_invocation",
|
||||
return_value=profile,
|
||||
),
|
||||
patch.object(handler, "_exact_n7_mode_invocation", return_value=None),
|
||||
patch.object(handler, "_saved_mode_infrared_entries", return_value=_entries()),
|
||||
patch.object(
|
||||
handler,
|
||||
"_execute_local_peripheral_actions",
|
||||
side_effect=execute_peripheral,
|
||||
),
|
||||
):
|
||||
result = handler._execute_n7_named_mode_request(
|
||||
"/api/requests",
|
||||
{
|
||||
"utterance": "进入并行模式",
|
||||
"execution_mode": "hardware",
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-saved-parallel",
|
||||
"vehicle_id": "denza_n7",
|
||||
"control_transport": "wired_can",
|
||||
},
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert can_modes == ["dry_run", "hardware"]
|
||||
assert peripheral_modes == ["dry_run", "hardware"]
|
||||
assert result["parallel_dispatch"]["enabled"] is True
|
||||
assert result["provider_used"] == "saved_mode_command_flow"
|
||||
assert persisted == [result]
|
||||
@@ -937,6 +937,22 @@ def test_delivery_status_distinguishes_blocked_failed_aborted_and_verified() ->
|
||||
"physical_effect_confirmed": True,
|
||||
},
|
||||
),
|
||||
sample(
|
||||
{
|
||||
"id": "window-feedback-confirmed",
|
||||
"capability": "window.set_position",
|
||||
"target": {"zone": "rear-left"},
|
||||
"parameters": {"position_pct": 0},
|
||||
},
|
||||
status="failed",
|
||||
message="MQTT 白名单控制帧已进入客户端发布队列;车辆效果以状态话题回读为准。",
|
||||
telemetry={
|
||||
"delivery_status": "submitted",
|
||||
"verification_status": "verified_success",
|
||||
"feedback_match": True,
|
||||
"physical_effect_confirmed": True,
|
||||
},
|
||||
),
|
||||
sample(
|
||||
{
|
||||
"id": "climate-post-submit-failed",
|
||||
@@ -970,6 +986,7 @@ def test_delivery_status_distinguishes_blocked_failed_aborted_and_verified() ->
|
||||
"下发失败",
|
||||
"已中止",
|
||||
"执行已确认",
|
||||
"执行已确认",
|
||||
"执行失败",
|
||||
]
|
||||
assert [item["deliveryState"] for item in commands] == [
|
||||
@@ -977,6 +994,7 @@ def test_delivery_status_distinguishes_blocked_failed_aborted_and_verified() ->
|
||||
"failed",
|
||||
"aborted",
|
||||
"submitted",
|
||||
"submitted",
|
||||
"failed",
|
||||
]
|
||||
assert "0x388 实时位置" in commands[0]["deliveryDetail"]
|
||||
|
||||
@@ -1406,6 +1406,15 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
def test_main_peripheral_sequence_executes_validated_actions_and_delays(self) -> None:
|
||||
service = SelfExtensionService(SimulationGateway())
|
||||
catalog = [
|
||||
{
|
||||
"id": "on",
|
||||
"status": "learned",
|
||||
"device_id": "third-party-light",
|
||||
"device_name": "第三方氛围灯",
|
||||
"card_uid": "1A53DD95",
|
||||
"action": "打开灯光",
|
||||
"slot": 1,
|
||||
},
|
||||
{
|
||||
"id": "red",
|
||||
"status": "learned",
|
||||
@@ -1457,16 +1466,24 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
result["orchestration_route"], "nfc_peripheral_sequence"
|
||||
)
|
||||
self.assertEqual(result["sequence"]["step_count"], 3)
|
||||
self.assertEqual(result["sequence"]["action_count"], 2)
|
||||
self.assertEqual(result["sequence"]["emit_count"], 2)
|
||||
self.assertEqual(result["sequence"]["step_count"], 4)
|
||||
self.assertEqual(result["sequence"]["action_count"], 3)
|
||||
self.assertEqual(result["sequence"]["emit_count"], 3)
|
||||
self.assertEqual(result["sequence"]["total_delay_ms"], 5000)
|
||||
self.assertEqual(result["sequence"]["steady_blink_fallbacks"], 1)
|
||||
self.assertEqual(
|
||||
[item["status"] for item in result["operation_results"]],
|
||||
["succeeded", "succeeded", "succeeded"],
|
||||
["succeeded", "succeeded", "succeeded", "succeeded"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
item["parameters"]["slot"]
|
||||
for item in result["function_config"]["spec"]["operations"]
|
||||
],
|
||||
[1, 3, 4],
|
||||
)
|
||||
sleep.assert_any_call(5.0)
|
||||
sleep.assert_any_call(0.8)
|
||||
self.assertNotIn(0.5, [call.args[0] for call in sleep.call_args_list])
|
||||
|
||||
def test_failed_first_emit_retains_complete_compiled_infrared_plan(self) -> None:
|
||||
@@ -1630,6 +1647,107 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def test_color_only_request_automatically_executes_power_on_first(self) -> None:
|
||||
with TemporaryDirectory() as directory:
|
||||
service = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(directory, "infrared.sqlite3"),
|
||||
)
|
||||
service.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
"id": "flash-on",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "flash-light",
|
||||
"device_name": "闪烁灯",
|
||||
"object_names": ["闪烁灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "打开",
|
||||
"action_names": ["打开", "开启", "启动"],
|
||||
"slot": 1,
|
||||
},
|
||||
{
|
||||
"id": "flash-blue",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "flash-light",
|
||||
"device_name": "闪烁灯",
|
||||
"object_names": ["闪烁灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "切换为蓝色",
|
||||
"action_names": ["蓝色", "设置为蓝色"],
|
||||
"slot": 3,
|
||||
},
|
||||
]
|
||||
)
|
||||
color = next(
|
||||
item
|
||||
for item in service.list_peripheral_capabilities()
|
||||
if item["id"] == "flash-blue"
|
||||
)
|
||||
|
||||
result = service.execute_peripheral_request(
|
||||
"把闪烁灯设置为蓝色",
|
||||
color,
|
||||
conversation_id="conv-color-power-dependency",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(result["sequence"]["action_count"], 2)
|
||||
self.assertEqual(
|
||||
[
|
||||
item["parameters"]["slot"]
|
||||
for item in result["function_config"]["spec"]["operations"]
|
||||
],
|
||||
[1, 3],
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
item["signals"]["slot"]
|
||||
for item in result["command_plan"]["spec"]["commands"]
|
||||
],
|
||||
[1, 3],
|
||||
)
|
||||
service.close()
|
||||
|
||||
def test_color_request_is_rejected_when_power_on_is_not_authorized(self) -> None:
|
||||
with TemporaryDirectory() as directory:
|
||||
service = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(directory, "infrared.sqlite3"),
|
||||
)
|
||||
service.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
"id": "strip-red",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "light-strip",
|
||||
"device_name": "灯带",
|
||||
"object_names": ["灯带"],
|
||||
"card_uid": "1A3AB103",
|
||||
"action": "切换为红色",
|
||||
"action_names": ["红色", "设置为红色"],
|
||||
"slot": 24,
|
||||
}
|
||||
]
|
||||
)
|
||||
color = service.list_peripheral_capabilities()[0]
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
ExtensionIntegrationError,
|
||||
"设置颜色前需要先打开",
|
||||
):
|
||||
service.execute_peripheral_request(
|
||||
"把灯带设置为红色",
|
||||
color,
|
||||
conversation_id="conv-color-missing-power",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
service.close()
|
||||
|
||||
def test_color_sequence_never_cycles_the_power_off_capability(self) -> None:
|
||||
service = SelfExtensionService(SimulationGateway())
|
||||
catalog = [
|
||||
|
||||
@@ -37,11 +37,29 @@ def test_peripheral_catalog_sync_is_serialized_and_tracks_backend_revision() ->
|
||||
)
|
||||
|
||||
assert "peripheralCatalogSyncQueue" in sync
|
||||
assert "const nextCatalog = peripheralCatalog();" in sync
|
||||
assert "catalog_revision: peripheralCatalogRevision" in sync
|
||||
assert "result.catalog_revision" in sync
|
||||
assert "applyDurablePeripheralCatalog(" in sync
|
||||
assert "if (throwOnError) throw error;" in sync
|
||||
|
||||
|
||||
def test_backend_catalog_is_authoritative_for_persistent_device_cards() -> None:
|
||||
source = SOURCE_PATH.read_text(encoding="utf-8")
|
||||
hydrate = _function_source(
|
||||
source,
|
||||
" async function hydratePeripheralCatalog() {",
|
||||
"\n function vehicleCatalog",
|
||||
)
|
||||
bootstrap = source[source.index(" void hydratePeripheralCatalog()") :]
|
||||
|
||||
assert "applyDurablePeripheralCatalog(" in hydrate
|
||||
assert "if (!catalog.length)" in hydrate
|
||||
assert "renderAll();" in hydrate
|
||||
assert ".finally(() => syncPeripheralCatalog())" not in bootstrap
|
||||
assert "return syncPeripheralCatalog();" in bootstrap
|
||||
|
||||
|
||||
def test_bound_card_can_be_reassigned_to_a_new_device() -> None:
|
||||
source = SOURCE_PATH.read_text(encoding="utf-8")
|
||||
render = _function_source(
|
||||
|
||||
@@ -429,23 +429,26 @@ class SpeechCapabilityAlignmentTests(unittest.TestCase):
|
||||
result = self.processor.process("打开座椅通风并调到三档")
|
||||
self.assertEqual(
|
||||
[command.capability for command in result.commands],
|
||||
[
|
||||
"seat.set_driver_ventilation_native",
|
||||
"seat.set_driver_ventilation_native",
|
||||
],
|
||||
["seat.set_driver_ventilation_native"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[command.target_scope for command in result.commands],
|
||||
["driver", "driver"],
|
||||
["driver"],
|
||||
)
|
||||
self.assertIn("3档", result.clean_text)
|
||||
self.assertEqual(result.semantic_status, "recognized_unsupported")
|
||||
self.assertFalse(result.execution_eligible)
|
||||
self.assertEqual(result.commands[0].parameters, {"state": "on", "level": 1})
|
||||
self.assertEqual(result.commands[0].qualifiers["display_level"], "3档")
|
||||
self.assertEqual(result.semantic_status, "recognized_executable")
|
||||
self.assertTrue(result.execution_eligible)
|
||||
|
||||
passenger = self.processor.process("打开副驾座椅通风并调到三档")
|
||||
self.assertEqual(
|
||||
[command.target_scope for command in passenger.commands],
|
||||
["passenger", "passenger"],
|
||||
["passenger"],
|
||||
)
|
||||
self.assertEqual(
|
||||
passenger.commands[0].parameters,
|
||||
{"state": "on", "level": 3},
|
||||
)
|
||||
self.assertTrue(passenger.execution_eligible)
|
||||
|
||||
@@ -489,7 +492,11 @@ class SpeechCapabilityAlignmentTests(unittest.TestCase):
|
||||
self.assertNotIn("设置为", result.clean_text)
|
||||
|
||||
explicit_level = self.processor.process("主驾座椅通风开到一档")
|
||||
self.assertEqual(explicit_level.commands[0].parameters["level"], 1)
|
||||
self.assertEqual(explicit_level.commands[0].parameters["level"], 2)
|
||||
self.assertEqual(
|
||||
explicit_level.commands[0].qualifiers["display_level"],
|
||||
"1档",
|
||||
)
|
||||
|
||||
def test_discourse_connectors_preserve_actions_without_leaking_scope(self) -> None:
|
||||
cases = (
|
||||
|
||||
@@ -31,7 +31,7 @@ def test_vue_vite_application_is_the_primary_frontend_entry() -> None:
|
||||
assert package["scripts"]["build"] == "vite build"
|
||||
assert 'import vue from "@vitejs/plugin-vue"' in vite
|
||||
assert 'outDir: outputRoot' in vite
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"' in service_worker
|
||||
assert 'createHash("sha256")' in vite
|
||||
assert 'runtime-assets.json' in vite
|
||||
assert 'rewriteRuntimeReferences' in vite
|
||||
|
||||
Reference in New Issue
Block a user