from __future__ import annotations import json import shutil import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] def source(name: str) -> str: return (ROOT / "apps" / "web-console" / name).read_text(encoding="utf-8") def test_frontend_only_exposes_user_orchestration_and_extension() -> None: web_root = ROOT / "apps" / "web-console" html = source("index.html") console = source("console-v2.js") for removed in ( "self-calibration.js", "self-calibration.css", "self-repair.js", "self-repair.css", "self-repair-seat3d.js", "self-evolution.js", "self-evolution.css", ): assert not (web_root / removed).exists() assert removed not in html assert "511工程-智能座舱" in console assert '["self-orchestration", "self-extension"]' in console assert 'id="vehicleExtensionSync"' in console assert 'getRole: () => "user"' in console assert '"admin"' not in console assert "v2RoleSelect" not in console def test_retired_layout_is_removed_instead_of_permanently_hidden() -> None: html = source("index.html") app = source("app.js") console = source("console-v2.js") styles = source("console-v2.css") for retired_markup in ( 'class="topbar"', 'class="example-group"', 'id="safetyStrip"', 'class="pipeline"', 'id="gestureBackdrop"', ): assert retired_markup not in html for retired_runtime_hook in ( "v2-legacy-parking", "v2-legacy-hidden", "#connectionBadge", "#modelBadge", "#hardwareBadge", "#executionBadge", "#logoutButton", "#gestureToggleButton", "#pwaBadge", ): assert retired_runtime_hook not in app assert retired_runtime_hook not in console assert retired_runtime_hook not in styles def test_extension_pet_and_transient_vehicle_state_restart_contract() -> None: extension = source("self-extension.js") styles = source("self-extension.css") assert 'id="extensionDesktopPet"' in extension assert "function initializeDesktopPet()" in extension assert "saved.vehicleExecutions = {};" in extension assert "saved.lastProtocol = null;" in extension assert 'src="/assets/pets/self-extension-cat.png?v=3"' in extension assert 'class="v2-extension-pet-entrance-canvas"' in extension assert '"/assets/pets/self-extension-cat-entrance.png?v=3"' in extension assert "const ENTRANCE_KEYFRAME_COUNT = 12" in extension assert "const ENTRANCE_DURATION_MS = 2300" in extension assert "const ENTRANCE_WALK_STEPS = 5" in extension assert "const ENTRANCE_SETTLE_START_FRAME = 5" in extension assert "0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4" in extension assert "const easedStepProgress = 0.5 - (Math.cos(Math.PI * stepProgress) / 2)" in extension assert "function playEntrance()" in extension assert 'pet.dataset.entering = "true"' in extension assert "const drawEntrancePose =" in extension assert "window.requestAnimationFrame(renderEntrance)" in extension assert "function createThemedPetDataUrl(" in extension assert "blueDominance < 24" in extension assert 'window.addEventListener("cognitive-agent:demo-role-change"' not in extension assert 'class="v2-extension-pet-blink"' in extension assert "const scheduleBlink =" in extension assert 'pet.dataset.blinking = "true"' in extension assert 'class="v2-extension-pet-mouth-closed"' in extension assert "const animateMouth = () =>" in extension assert "mouth.animate?.(" in extension assert "{ opacity: 0, offset: 0.09 }" in extension assert "{ opacity: 0, offset: 0.41 }" in extension assert "{ opacity: 0, offset: 0.73 }" in extension assert "{ duration: 1260" in extension assert 'class="v2-extension-pet-tail-canvas"' in extension assert "const runTailWave =" in extension assert "const requestTailWave =" in extension assert 'prefers-reduced-motion: reduce' not in extension assert "tailWaveQueued = true" in extension assert "window.setTimeout(() => runTailWave(true), 180)" in extension assert "scheduleTailWave" not in extension assert 'pet.dataset.tailWaving = "true"' in extension assert "const tailMotionFrames = [" in extension assert "const sampleTailOffset =" in extension assert "sampleTailOffset(tailPose, distanceFromBase)" in extension assert "clampToSegment = false" in extension assert "Math.max(Math.min(start, end)" in extension assert "const sampleTravelingTailOffset =" not in extension assert 'document.visibilityState !== "visible"' in extension assert ".v2-extension-pet-body" in styles assert ".v2-extension-pet-entrance-canvas" in styles assert '.v2-extension-pet[data-entering="true"] .v2-extension-pet-image' in styles assert '.v2-extension-pet[data-blinking="true"] .v2-extension-pet-blink' in styles assert ".v2-extension-pet-mouth-closed" in styles assert "width: 176px;" in styles assert "font-size: 13px;" in styles assert '.v2-extension-pet[data-tail-waving="true"] .v2-extension-pet-tail-canvas' in styles assert '.v2-extension-pet[data-tail-waving="true"] .v2-extension-pet-image' in styles assert "position: fixed;" in styles assert "extension-pet-float" not in styles assert '[data-module="self-extension"][data-demo-role="admin"] .v2-sidebar' not in styles assert '[data-module="self-extension"][data-demo-role="admin"] .v2-avatar' not in styles def test_orchestration_session_reset_remains_available() -> None: orchestration = source("app.js") assert 'sessionStorage.removeItem("cognitive-agent.conversation-id")' in orchestration def test_vehicle_device_status_cards_prioritize_seats_and_expose_manual_controls() -> None: console = source("console-v2.js") styles = source("console-v2.css") passenger = console.index('id: "passenger", name: "副驾座椅"') driver = console.index('id: "driver", name: "主驾座椅"') windows = console.index('id: "windows", name: "车窗"') assert passenger < driver < windows for key_status in ( '["左前窗", readText("#frontLeftWindow")]', '["右前窗", readText("#frontRightWindow")]', '["左后窗", readText("#rearLeftWindow")]', '["右后窗", readText("#rearRightWindow")]', '["当前位置", readText("#shadePositionValue")]', '["按摩模式", massageMode]', '["按摩强度", massageStrength]', '["前排亮度", frontBrightness]', '["前排颜色", frontColor]', '["后排亮度", rearBrightness]', '["后排颜色", rearColor]', ): assert key_status in console assert 'data-device-control-toggle="${device.id}"' in console assert 'data-device-control-apply="${device.id}"' in console assert 'new CustomEvent("cognitive-agent:vehicle-can-control-request"' in console assert "window.CognitiveAgentConsole?.executeCanCommands" in console assert "device.commands(values).filter(Boolean)" in console assert 'controllable: false' in console assert '.v2-device-control-row input[type="range"]' in styles assert ".v2-device-control-select" in styles assert ".v2-device-control-apply" in styles def test_passenger_can0_status_ui_uses_only_seat_ota_four_axis_contract() -> None: app = source("app.js") console = source("console-v2.js") assert 'new Set(["longitudinal", "height", "backrest_recline", "legrest_height"])' in app assert 'if (thermalRow) thermalRow.hidden = seat === "passenger";' in app assert 'height: seatHeight,' in app assert '"seat.set_cushion_tilt": "调节座椅高度"' in app assert 'rows: () => [["前后", seatAxisValue("passenger", "longitudinal")], ["高度", seatAxisValue("passenger", "cushion_tilt")]' in console assert 'rangeControl("cushion_tilt", "高度"' in console assert 'rangeControl("legrest_extension"' not in console def test_left_sidebar_exposes_collapsed_parsed_can_command_panel() -> None: console = source("console-v2.js") styles = source("console-v2.css") assert 'make("details", "v2-can-command-panel"' in console assert 'canCommandPanel.id = "v2CanCommandPanel"' in console assert 'id="v2CanCommandCount">0' in console assert 'id="v2CanCommandList"' in console assert "canCommandPanel.open" not in console assert "function parsedCanCommandForOperation(operation = {})" in console assert "function parsedCanCommands(result = {})" 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 assert 'deliveryLabel: submitted ? "下发" : "未下发"' in console assert 'data-delivery-state="${escapeHtml(item.deliveryState)}"' in console 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 'window.addEventListener("cognitive-agent:result"' in console assert '$("#requestForm")?.addEventListener("submit"' in console for command_contract in ( "door rear ${action}", "door vehicle ${lockState}", "window ${target} ${motion}", "window ${target} position ${position}", "ac ${onOff}", "ac fan ${level}", "ac mode ${mode}", "ac temp ${target} ${temperature}", "ac control ${mode}", "ac cycle ${mode}", "outlet ${target} percent ${horizontal} ${vertical}", "outlet ${target} position ${horizontal} ${vertical}", "seat driver ${feature} ${setting}", "seat driver massage strength ${strength}", "seat driver ${axis} ${direction} ${duration}", "seat driver memory recall ${position}", "seat driver position ${position}", "light indoor ${onOff}", "light reading all ${onOff}", "light ambient color ${color}", "light ambient brightness ${level}", "shade position ${position}", "seat passenger ${axis[1]} position ${position}", "seat passenger dynamic ${dynamic}", "seat passenger dynamic stop", "nativeCan0Frame(0x01, axis[2], 0, position * 100, 30000)", "nativeCan0Frame(0x0B, axis[2], 0, 0x4B52, 120)", "nativeCan0Frame(0x0E, 0, 0, mode, 0)", "nativeCan0Frame(0x0F, 0, 0, 0, 0)", ): assert command_contract in console assert ".v2-can-command-panel[open]" in styles 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-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 def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands() -> 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": { "seat": { "spec": { "operations": [ { "capability": "seat.set_backrest_recline", "target": {"zone": "front_right"}, "parameters": {"position_pct": 36, "angle_deg": 120}, "parse_source": "local_rule", }, ] } }, "native_climate": { "spec": { "operations": [ { "capability": "climate.set_temperature", "target": {"zone": "driver"}, "parameters": {"temperature_c": 24}, "parse_source": "cloud_supplement", } ] } }, }, "command_plans": { "seat": {"spec": {"commands": [{}]}}, "native_climate": {"spec": {"commands": [{}]}}, }, } script = ( f"{helpers}\n" f"const sample = {json.dumps(sample)};\n" "process.stdout.write(JSON.stringify(parsedCanCommands(sample)));" ) completed = subprocess.run( [node, "-e", script], check=True, capture_output=True, text=True, ) assert json.loads(completed.stdout) == [ { "channel": "CAN0", "category": "副驾座椅靠背调节", "command": "seat passenger backrest position 36", "frame": "0x300 · 01 12 10 0E 30 75", "parseSource": "local_rule", "deliveryState": "not_submitted", "deliveryLabel": "未下发", "deliveryTransport": "有线 CAN", }, { "channel": "CAN1", "category": "空调温度", "command": "ac temp driver 24", "parseSource": "cloud_supplement", "deliveryState": "not_submitted", "deliveryLabel": "未下发", "deliveryTransport": "有线 CAN", }, ] def test_parsed_command_delivery_is_independent_of_transport_and_target() -> 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] base = { "function_configs": { "native_climate": { "spec": { "operations": [ { "id": "climate-power", "capability": "climate.set_power", "target": {"zone": "vehicle"}, "parameters": {"state": "on"}, "parse_source": "local_rule", } ] } } } } samples = [ {**base, "execution_mode": "dry_run", "control_transport": "wired_can"}, { **base, "execution_mode": "dry_run", "control_transport": "wireless_mqtt", }, { **base, "execution_mode": "hardware", "control_transport": "wired_can", "execution_report": { "spec": { "results": [ { "ref": "climate-power", "status": "succeeded", "telemetry": {"sent_frames": [{"frame_id": "0x1DE"}]}, } ] } }, }, { **base, "execution_mode": "hardware", "control_transport": "wireless_mqtt", "execution_report": { "spec": { "results": [ { "ref": "climate-power", "status": "succeeded", "telemetry": {"delivery_status": "queued"}, } ] } }, }, ] script = ( f"{helpers}\n" f"const samples = {json.dumps(samples)};\n" "process.stdout.write(JSON.stringify(samples.map((sample) => " "parsedCanCommands(sample))));" ) completed = subprocess.run( [node, "-e", script], check=True, capture_output=True, text=True, ) parsed = json.loads(completed.stdout) assert [len(commands) for commands in parsed] == [1, 1, 1, 1] assert [commands[0]["command"] for commands in parsed] == ["ac on"] * 4 assert [commands[0]["deliveryLabel"] for commands in parsed] == [ "未下发", "未下发", "下发", "下发", ] assert [commands[0]["deliveryTransport"] for commands in parsed] == [ "有线 CAN", "无线 CAN", "有线 CAN", "无线 CAN", ] def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences() -> None: app = source("app.js") styles = source("styles.css") html = source("index.html") assert 'class="mode-editor-flow-preview empty"' in html assert "function modeEditorReadableCommands(flow)" in app assert "function modeEditorMessagesForOperation(flow, readable, operationRef, plan)" in app assert 'command?.signals?.special_handler === "driver_seat_memory_recall"' in app assert "Array.isArray(special?.frame_sequence)" in app assert "function modeEditorAppendCommandCard(preview, item, flow)" in app assert 'card.className = "mode-editor-command-card"' in app assert 'title.textContent = flow?.control_transport === "wireless_mqtt" ? "MQTT 消息序列"' in app assert 'source: "published"' in app assert 'source: flow?.control_transport === "wireless_mqtt" ? "projected" : "can"' in app assert "function_configs: functionConfigs" in app assert "execution_steps: []" in app assert "requestNaturalLanguageControl(instruction" in app assert 'executionMode: "dry_run"' in app assert 'clientType: "mode_editor_compile"' in app assert "previewOnly: true" in app 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 assert 'const savedModeExecution = result.orchestration_route === "saved_mode_command_flow";' in app assert "`${result.mode_profile.name}执行完成`" in app assert "生成指令" in html assert ">执行" not in html assert ".mode-editor-command-card" in styles assert ".mode-editor-command-meta" in styles assert ".mode-editor-message-sequence" in styles assert '.mode-editor-command-card[data-can-channel="CAN0"]' in styles def test_mode_editor_prioritizes_can_flow_display_area() -> None: app = source("app.js") html = source("index.html") styles = source("styles.css") assert 'class="mode-editor-setup"' in html assert 'class="mode-editor-inputs"' in html assert "grid-template-rows: minmax(0, 30%) minmax(0, 1fr) auto auto;" in styles assert "grid-template-columns: minmax(150px, 0.32fr) minmax(0, 1fr);" in styles assert ".mode-editor-instruction-field textarea" in styles assert "resize: none;" in styles assert ".mode-editor-flow-preview" in styles assert "overflow: auto;" in styles assert 'id="modeEditorFlowExpand"' in html assert ".mode-editor.flow-expanded" in styles assert ".mode-editor-backdrop.flow-expanded" in styles assert "width: 100vw;" in styles assert "height: 100dvh;" in styles assert "grid-template-rows: minmax(0, 1fr);" in styles assert "touch-action: pan-y;" in styles assert "scrollbar-gutter: stable;" in styles assert "max-height: 220px;" not in styles assert "function setModeEditorFlowExpanded(expanded)" in app assert '$("#modeEditorFlowExpand").addEventListener("click", toggleModeEditorFlowExpanded);' in app def test_mode_editor_prepares_one_to_many_mqtt_message_sequences() -> None: node = shutil.which("node") if node is None: return app = source("app.js") console = source("console-v2.js") console_start = console.index(" const normalizedCanTarget") console_end = console.index("\n\n function renderParsedCanCommands", console_start) console_helpers = console[console_start:console_end] app_start = app.index("function modeEditorCommandCategory") app_end = app.index("\n\nfunction modeEditorAppendCommandCard", app_start) app_helpers = app[app_start:app_end] sample = { "control_transport": "wireless_mqtt", "function_configs": { "native_climate": { "spec": { "operations": [ { "id": "temperature", "capability": "climate.set_temperature", "target": {"zone": "driver"}, "parameters": {"temperature_c": 25}, } ] } } }, "plans": { "native_climate": { "spec": { "commands": [ { "operation_ref": "temperature", "signals": { "frame_id": "0x1DE", "data_hex": "00 00 00 00 19 00 00 00", "is_fd": False, "bitrate_switch": False, "debug_command": "ac temp driver 25", }, "send_offset_ms": 20, "repeat": {"count": 3, "period_ms": 100}, } ] } } }, "execution_steps": [], } script = ( f"{console_helpers}\n" "const state = {runtime:{vehicle_mqtt:{control_topic:" '"n7/n7-headunit/can/command",qos:1,feedback_timeout_ms:1500}}};\n' "const window = {CognitiveAgentConsoleV2:{getParsedCanCommands:" "(result)=>parsedCanCommands(result)}};\n" f"{app_helpers}\n" f"const flow = {json.dumps(sample)};\n" "process.stdout.write(JSON.stringify(modeEditorReadableCommands(flow)));" ) completed = subprocess.run( [node, "-e", script], check=True, capture_output=True, text=True, ) commands = json.loads(completed.stdout) assert len(commands) == 1 assert commands[0]["channel"] == "CAN1" assert commands[0]["category"] == "空调温度" assert commands[0]["command"] == "ac temp driver 25" assert [message["offset_ms"] for message in commands[0]["messages"]] == [ 20, 120, 220, ] assert all( message["topic"] == "n7/n7-headunit/can/command" and message["payload"]["canId"] == "0x1DE" and message["payload"]["command"] == "ac temp driver 25" for message in commands[0]["messages"] ) def test_interactive_console_keeps_actions_inside_composer_and_collapsed_rails_aligned() -> None: console = source("console-v2.js") styles = source("console-v2.css") assert 'const resetButton = $("#newConversation", originalForm);' in console assert 'const runButton = $("#runButton", originalForm);' in console assert "if (resetButton) composer.append(resetButton);" in console assert "if (runButton) composer.append(runButton);" in console assert ".v2-request-form .command-composer" in styles assert "overflow: hidden;" in styles assert "transform: translateY(-50%);" in styles assert '.joint-twin-shell[data-open-panels="none"]' in styles assert "grid-template-columns: 48px minmax(520px, 1fr) 48px;" in styles assert "writing-mode: horizontal-tb;" in styles assert "writing-mode: vertical-rl;" in styles def test_left_sync_rail_can_collapse_and_log_aligns_with_lower_workspace() -> None: console = source("console-v2.js") styles = source("console-v2.css") assert 'make("section", "v2-sync-sidebar"' in console assert '
' in console assert "syncOverview?.append(sidebarFooter);" in console assert '$(".v2-sync-sidebar-heading", syncSidebar)?.append(sidebarToggle);' in console assert "function syncSidebarLogBaseline()" in console assert "const requiredOverviewHeight = overviewRect" in console assert "overviewBottom - overviewRect.top + overviewPaddingBottom + 10" in console assert 'sidebar.style.setProperty("--v2-sync-log-top"' in console assert 'window.addEventListener("resize", syncSidebarLogBaseline);' in console assert "requestAnimationFrame(syncSidebarLogBaseline);" in console assert "--v2-sync-log-top: 291px;" in styles assert "grid-template-rows: calc(var(--v2-sync-log-top) - 10px) minmax(0, 1fr);" in styles assert "grid-auto-rows: max-content;" in styles assert "align-content: start;" in styles assert "min-height: max-content;" in styles assert "align-self: start;" in styles assert "overflow: visible;" in styles assert ".v2-sync-sidebar > .v2-sync-log" in styles assert "grid-template-columns: 64px minmax(0, 1fr);" in styles assert ".v2-sidebar-collapsed #v2SidebarToggle" in styles def test_twin_device_status_popover_has_clear_close_paths() -> None: console = source("console-v2.js") styles = source("console-v2.css") assert 'data-twin-popover-close aria-label="关闭${escapeHtml(device.name)}状态详情"' in console assert 'popover.setAttribute("role", "dialog");' in console assert 'popover.setAttribute("aria-label", "车端设备状态详情");' in console assert 'event.target.closest("[data-twin-popover-close]")' in console assert 'event.target.closest("[data-twin-device], #v2TwinDevicePopover")' in console assert "const samePinned = displaySync.pinnedDevice === focus.dataset.deviceFocus;" in console assert "clearTwinDevicePreview({ force: true });" in console assert ".v2-twin-popover-close" in styles assert ".v2-twin-popover-close:focus-visible" in styles def test_twin_device_hotspots_use_responsive_hover_and_leader_labels() -> None: console = source("console-v2.js") styles = source("console-v2.css") assert "previewClearTimer: null" in console assert "hoverSuppressedUntil: 0" in console assert 'class="v2-twin-hotspot-label"' in console assert 'button.addEventListener("pointerenter"' in console assert 'button.addEventListener("pointerleave"' in console assert "scheduleTwinDevicePreviewClear();" in console assert "}, 45);" in console assert "performance.now() + 160" in console assert 'hotspots.addEventListener("pointerover"' not in console assert 'hotspots.addEventListener("pointerout"' not in console assert ".v2-twin-hotspots button::before" in styles assert ".v2-twin-hotspots button::after" in styles assert ".v2-twin-hotspot-label" in styles assert "--v2-hotspot-label-x: -115px" in styles assert "--v2-hotspot-line-angle: -150deg" in styles assert '.v2-twin-device-popover[data-pinned="false"]' in styles assert "pointer-events: none;" in styles def test_validated_can_commands_survive_same_task_running_poll_and_keep_error_detail() -> None: console = source("console-v2.js") app = source("app.js") assert "function validatedCommandsBelongToTask(task = {})" in console assert "if (!validatedCommandsBelongToTask(task))" in console assert "A stale" in console assert "must not erase those commands" in console assert "rememberValidatedCommandContext(result);" in console assert "resetValidatedCommandContext();" in console assert "error.detail || error.message" in console assert "failed_or_blocked: \"失败或已阻止\"" in console assert "requestError.outcome = phaseEvent.outcome;" in app assert "requestError.detail = phaseEvent.detail;" in app