Files
Cognitive_Agent/tests/unit/test_requested_frontend_enhancements.py
T

657 lines
27 KiB
Python

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</strong>' 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 <SEQ_LO> <SEQ_HI> 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: executionSteps" in app
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 '<div class="v2-sync-sidebar-overview">' 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