1564 lines
63 KiB
Python
1564 lines
63 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:
|
|
web_root = ROOT / "apps" / "web-console"
|
|
content = (web_root / name).read_text(encoding="utf-8")
|
|
if name == "index.html":
|
|
content += "\n" + (web_root / "src" / "App.vue").read_text(encoding="utf-8")
|
|
return content
|
|
|
|
|
|
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工程-智能座舱" not 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_ambient_status_uses_its_own_power_feedback_not_combined_light_badge() -> None:
|
|
console = source("console-v2.js")
|
|
ambient_start = console.index('id: "ambient", name: "氛围灯"')
|
|
ambient_end = console.index('id: "headlight", name: "前大灯"', ambient_start)
|
|
ambient = console[ambient_start:ambient_end]
|
|
|
|
assert 'function ambientPowerState()' in console
|
|
assert 'dataset.ambientPower' in console
|
|
assert 'const powerState = ambientPowerState();' in ambient
|
|
assert 'powerState === "on" ? "开启"' in ambient
|
|
assert 'ambientPowerState() === "off" ? "off" : "on"' in ambient
|
|
assert 'readText("#ambientPower")' not in ambient
|
|
|
|
|
|
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 'id="v2CommandTiming"' in console
|
|
assert "panel.open = true;" in console
|
|
assert "if (ui.sidebarCollapsed) setSidebarCollapsed(false);" in console
|
|
assert "function parsedCanCommandForOperation(operation = {})" in console
|
|
assert "function parsedCanCommands(result = {})" in console
|
|
assert "function semanticControlInstruction(operation = {})" in console
|
|
assert "function compiledCommandsForOperation(result, configKey, operation = {})" in console
|
|
assert "function parsedCommandTimingRows(result = {}, timingState = {})" in console
|
|
assert "result.validated_control_commands" in console
|
|
assert "getParsedCanCommands: (result) => parsedCanCommands(result)" in console
|
|
assert "const configurations = result.function_configs || {};" in console
|
|
assert "if (!configurationEntries.length && result.function_config)" in console
|
|
assert "const safetyBlocked = executionFailed && !submitted" in console
|
|
assert 'safetyBlocked\n ? "安全阻止"' 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 'class="v2-can-command-layer v2-can-command-semantic"' in console
|
|
assert 'class="v2-can-command-layer v2-can-command-actual"' in console
|
|
assert console.index("语义控制指令") < console.index("实际控制指令")
|
|
assert 'recordTiming: true' in console
|
|
assert 'reveal: true' in console
|
|
assert 'window.addEventListener("cognitive-agent:speech-state"' in console
|
|
assert 'window.addEventListener("cognitive-agent:speech-result"' in console
|
|
assert 'window.addEventListener("cognitive-agent:control-request-start"' in console
|
|
assert 'window.addEventListener("cognitive-agent:result"' in console
|
|
assert '$("#requestForm")?.addEventListener("submit"' in console
|
|
assert "function commandResultMatchesTimedRequest(result = {})" in console
|
|
assert "matchesTimedRequest && !validatedCommandsVisible" in console
|
|
|
|
for command_contract in (
|
|
"door rear ${action}",
|
|
"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}",
|
|
"Ambient_RGB_Control_32B_2B(color=${color}, brightness=${brightness}%)",
|
|
"Ambient_Mode_Control_496_04(mode=${mode})",
|
|
"Climate_Control_064_04(${target}=${temperature}°C)",
|
|
"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-command-timing" in styles
|
|
assert ".v2-can-command-semantic" in styles
|
|
assert ".v2-can-command-actual" in styles
|
|
assert '.v2-can-command-delivery[data-delivery-state="submitted"]' in styles
|
|
assert 'li[data-can-channel="CAN0"]' in styles
|
|
assert ".v2-sidebar-collapsed .v2-can-command-panel" in styles
|
|
|
|
|
|
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, "-"],
|
|
input=script,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
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",
|
|
"semantic": "seat.set_backrest_recline(target=front-right, angle_deg=120, position_pct=36)",
|
|
"operationRef": "",
|
|
"deliveryState": "not_submitted",
|
|
"deliveryLabel": "未下发",
|
|
"deliveryTransport": "有线 CAN",
|
|
},
|
|
{
|
|
"channel": "CAN1",
|
|
"category": "空调温度",
|
|
"command": "ac temp driver 24",
|
|
"parseSource": "cloud_supplement",
|
|
"semantic": "climate.set_temperature(target=driver, temperature_c=24)",
|
|
"operationRef": "",
|
|
"deliveryState": "not_submitted",
|
|
"deliveryLabel": "未下发",
|
|
"deliveryTransport": "有线 CAN",
|
|
},
|
|
]
|
|
|
|
|
|
def test_local_mixed_command_uses_compiled_can_and_ir_details_with_stage_timings() -> None:
|
|
node = shutil.which("node")
|
|
if node is None:
|
|
return
|
|
console = source("console-v2.js")
|
|
start = console.index(" const normalizedCanTarget")
|
|
end = console.index("\n\n function renderParsedCanCommands", start)
|
|
helpers = console[start:end]
|
|
sample = {
|
|
"provider_used": "local_rules",
|
|
"function_configs": {
|
|
"native_climate": {
|
|
"spec": {
|
|
"domain": "climate",
|
|
"operations": [
|
|
{
|
|
"id": "climate-temperature-1",
|
|
"capability": "climate.set_temperature",
|
|
"target": {"zone": "driver"},
|
|
"parameters": {"temperature_c": 23},
|
|
"parse_source": "local_rule",
|
|
}
|
|
],
|
|
}
|
|
},
|
|
"self_extension": {
|
|
"spec": {
|
|
"domain": "self_extension",
|
|
"operations": [
|
|
{
|
|
"id": "infrared-1",
|
|
"capability": "infrared.emit",
|
|
"target": {"device": "toy-001", "name": "闪灯玩具"},
|
|
"parameters": {"slot": 1, "action": "开启"},
|
|
"parse_source": "local_rule",
|
|
}
|
|
],
|
|
}
|
|
},
|
|
},
|
|
# Deliberately reverse the plan order and include a distracting command;
|
|
# the renderer must join only on the exact operation_ref.
|
|
"command_plans": {
|
|
"self_extension": {
|
|
"spec": {
|
|
"domain": "self_extension",
|
|
"commands": [
|
|
{
|
|
"operation_ref": "unrelated-ir",
|
|
"message": "infrared.emit",
|
|
"signals": {"slot": 9},
|
|
},
|
|
{
|
|
"operation_ref": "infrared-1",
|
|
"bus": "TCP",
|
|
"message": "infrared.emit",
|
|
"signals": {"slot": 1},
|
|
},
|
|
],
|
|
}
|
|
},
|
|
"native_climate": {
|
|
"spec": {
|
|
"domain": "climate",
|
|
"commands": [
|
|
{
|
|
"operation_ref": "climate-temperature-1",
|
|
"bus": "body_can",
|
|
"message": "Native_0x1DE",
|
|
"signals": {
|
|
"debug_command": "ac temp driver 23",
|
|
"frame_id": "0x1DE",
|
|
"data_hex": "00 00 00 00 17 00 00 00",
|
|
},
|
|
}
|
|
],
|
|
}
|
|
},
|
|
},
|
|
"trace_events": [
|
|
{"stage": "local_peripheral_plan", "duration_ms": 1},
|
|
{"stage": "nfc_peripheral_sequence", "duration_ms": 0},
|
|
{"stage": "intent_parse", "duration_ms": 12},
|
|
{"stage": "context_resolve", "duration_ms": 1},
|
|
{"stage": "compile", "duration_ms": 2},
|
|
{"stage": "execute", "duration_ms": 999},
|
|
],
|
|
}
|
|
timing_state = {
|
|
"inputMode": "speech",
|
|
"speechStartedAt": 0,
|
|
"speechResultAt": 80,
|
|
"speechTimings": {
|
|
"audio_read_ms": 1,
|
|
"vad_ms": 2,
|
|
"asr_ms": 30,
|
|
"assembly_ms": 1,
|
|
"semantic_ms": 3,
|
|
"command_resolution_ms": 1,
|
|
"recovery_ms": 0,
|
|
"total_ms": 37,
|
|
},
|
|
"requestStartedAt": 100,
|
|
"validatedAt": 125,
|
|
"displayedAt": 126,
|
|
"renderDurationMs": 1,
|
|
}
|
|
browser_projection = {
|
|
"provider_used": "local_rules+infrared_local_rules",
|
|
"execution_mode": "dry_run",
|
|
"validated_control_commands": [
|
|
{
|
|
"operation": sample["function_configs"]["native_climate"]["spec"][
|
|
"operations"
|
|
][0],
|
|
"domain": "climate",
|
|
"channel": "CAN1",
|
|
"actual_command": "ac temp driver 23",
|
|
"frames": [
|
|
{
|
|
"label": "CAN1 帧",
|
|
"value": "0x1DE · 00 00 00 00 17 00 00 00",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"operation": sample["function_configs"]["self_extension"]["spec"][
|
|
"operations"
|
|
][0],
|
|
"domain": "self_extension",
|
|
"channel": "IR",
|
|
"actual_command": "infrared emit 1",
|
|
"frames": [
|
|
{
|
|
"label": "GSCU1B TCP",
|
|
"value": '{"action":"emit","data":{"no":1},"type":"infrared"}',
|
|
}
|
|
],
|
|
},
|
|
],
|
|
"trace_events": sample["trace_events"][:3],
|
|
}
|
|
script = (
|
|
f"{helpers}\n"
|
|
f"const sample = {json.dumps(sample, ensure_ascii=False)};\n"
|
|
f"const browserProjection = {json.dumps(browser_projection, ensure_ascii=False)};\n"
|
|
f"const timingState = {json.dumps(timing_state)};\n"
|
|
"process.stdout.write(JSON.stringify({"
|
|
"commands: parsedCanCommands(sample), "
|
|
"projectedCommands: parsedCanCommands(browserProjection), "
|
|
"timings: parsedCommandTimingRows(sample, timingState)"
|
|
"}));"
|
|
)
|
|
completed = subprocess.run(
|
|
[node, "-"],
|
|
input=script,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
parsed = json.loads(completed.stdout)
|
|
climate, infrared = parsed["commands"]
|
|
projected_climate, projected_infrared = parsed["projectedCommands"]
|
|
assert climate["semantic"] == (
|
|
"climate.set_temperature(target=driver, temperature_c=23)"
|
|
)
|
|
assert climate["command"] == "ac temp driver 23"
|
|
assert climate["frame"] == "0x1DE · 00 00 00 00 17 00 00 00"
|
|
assert climate["frameLabel"] == "CAN1 帧"
|
|
assert climate["parseSource"] == "local_rule"
|
|
assert infrared["semantic"] == (
|
|
"infrared.emit(target=toy-001, action=开启, slot=1)"
|
|
)
|
|
assert infrared["command"] == "infrared emit 1"
|
|
assert infrared["frameLabel"] == "GSCU1B TCP"
|
|
assert json.loads(infrared["frame"])["data"]["no"] == 1
|
|
assert infrared["parseSource"] == "local_rule"
|
|
assert projected_climate["semantic"] == climate["semantic"]
|
|
assert projected_climate["command"] == climate["command"]
|
|
assert projected_climate["frames"] == climate["frames"]
|
|
assert projected_infrared["semantic"] == infrared["semantic"]
|
|
assert projected_infrared["command"] == infrared["command"]
|
|
assert projected_infrared["frames"] == infrared["frames"]
|
|
|
|
timing_by_key = {item["key"]: item for item in parsed["timings"]}
|
|
assert timing_by_key["local_peripheral_plan-0"]["label"] == "红外本地规则编译"
|
|
assert timing_by_key["nfc_peripheral_sequence-1"]["label"] == "红外指令序列生成"
|
|
assert timing_by_key["intent_parse-2"]["label"] == "本地规则意图解析"
|
|
assert timing_by_key["request_to_validated"]["durationMs"] == 25
|
|
assert timing_by_key["dom_render"]["durationMs"] == 1
|
|
assert timing_by_key["request_to_display"]["durationMs"] == 26
|
|
assert timing_by_key["speech_to_display"]["durationMs"] == 126
|
|
assert timing_by_key["total_ms"]["durationMs"] == 37
|
|
assert all(item["durationMs"] >= 0 for item in parsed["timings"])
|
|
assert not any(item["key"].startswith("execute") for item in parsed["timings"])
|
|
|
|
|
|
def test_duplicate_operation_refs_do_not_borrow_another_configs_plan() -> None:
|
|
node = shutil.which("node")
|
|
if node is None:
|
|
return
|
|
console = source("console-v2.js")
|
|
start = console.index(" const normalizedCanTarget")
|
|
end = console.index("\n\n function renderParsedCanCommands", start)
|
|
helpers = console[start:end]
|
|
sample = {
|
|
"function_configs": {
|
|
"native_climate": {
|
|
"spec": {
|
|
"domain": "climate",
|
|
"operations": [
|
|
{
|
|
"id": "op-1",
|
|
"capability": "climate.set_temperature",
|
|
"target": {"zone": "driver"},
|
|
"parameters": {"temperature_c": 23},
|
|
}
|
|
],
|
|
}
|
|
},
|
|
"native_seat": {
|
|
"spec": {
|
|
"domain": "seat",
|
|
"operations": [
|
|
{
|
|
"id": "op-1",
|
|
"capability": "seat.set_heating",
|
|
"target": {"zone": "passenger"},
|
|
"parameters": {"level": 2},
|
|
}
|
|
],
|
|
}
|
|
},
|
|
},
|
|
"command_plans": {
|
|
"native_seat": {
|
|
"spec": {
|
|
"domain": "seat",
|
|
"commands": [
|
|
{
|
|
"operation_ref": "op-1",
|
|
"bus": "seat_can0",
|
|
"signals": {
|
|
"debug_command": "seat passenger heat 2",
|
|
"frame_id": "0x300",
|
|
"data_hex": "01 02 03 04 05 06 07 08",
|
|
},
|
|
}
|
|
],
|
|
}
|
|
}
|
|
},
|
|
}
|
|
script = (
|
|
f"{helpers}\n"
|
|
f"const sample = {json.dumps(sample)};\n"
|
|
"process.stdout.write(JSON.stringify(parsedCanCommands(sample)));"
|
|
)
|
|
|
|
completed = subprocess.run(
|
|
[node, "-"],
|
|
input=script,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
parsed = json.loads(completed.stdout)
|
|
by_semantic = {item["semantic"]: item for item in parsed}
|
|
climate = by_semantic[
|
|
"climate.set_temperature(target=driver, temperature_c=23)"
|
|
]
|
|
seat = by_semantic["seat.set_heating(target=passenger, level=2)"]
|
|
assert climate["command"] == "ac temp driver 23"
|
|
assert "frame" not in climate
|
|
assert climate["channel"] == "CAN1"
|
|
assert seat["command"] == "seat passenger heat 2"
|
|
assert seat["channel"] == "CAN0"
|
|
|
|
|
|
def test_command_timing_rejects_stale_request_events() -> None:
|
|
node = shutil.which("node")
|
|
if node is None:
|
|
return
|
|
console = source("console-v2.js")
|
|
correlation_start = console.index(" function commandCorrelationIds")
|
|
correlation_end = console.index(
|
|
"\n\n function rememberValidatedCommandContext",
|
|
correlation_start,
|
|
)
|
|
correlation_helper = console[correlation_start:correlation_end]
|
|
start = console.index(" const normalizedCanTarget")
|
|
end = console.index("\n\n function renderParsedCanCommands", start)
|
|
helpers = console[start:end]
|
|
script = (
|
|
"let commandTimingState = {requestStartedAt: 1, requestId: '', "
|
|
"utterance: '空调温度设置到23度,打开闪烁灯'};\n"
|
|
f"{correlation_helper}\n"
|
|
f"{helpers}\n"
|
|
"const first = commandResultMatchesTimedRequest({request_id: 'req-current', "
|
|
"utterance: '空调温度设置到23度,打开闪烁灯'});\n"
|
|
"const staleId = commandResultMatchesTimedRequest({request_id: 'req-old', "
|
|
"utterance: '空调温度设置到23度,打开闪烁灯'});\n"
|
|
"const staleText = commandResultMatchesTimedRequest({request_id: 'req-current', "
|
|
"utterance: '打开车窗'});\n"
|
|
"process.stdout.write(JSON.stringify({first, staleId, staleText, "
|
|
"boundRequestId: commandTimingState.requestId}));"
|
|
)
|
|
|
|
completed = subprocess.run(
|
|
[node, "-"],
|
|
input=script,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
assert json.loads(completed.stdout) == {
|
|
"first": True,
|
|
"staleId": False,
|
|
"staleText": False,
|
|
"boundRequestId": "req-current",
|
|
}
|
|
|
|
|
|
def test_parsed_command_delivery_is_independent_of_transport_and_target() -> None:
|
|
node = shutil.which("node")
|
|
if node is None:
|
|
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",
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
blocked_shade = {
|
|
"function_configs": {
|
|
"native_roof": {
|
|
"spec": {
|
|
"operations": [
|
|
{
|
|
"id": "shade-position",
|
|
"capability": "roof.set_shade_position",
|
|
"target": {"zone": "vehicle"},
|
|
"parameters": {"position_pct": 30},
|
|
"parse_source": "local_rule",
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"execution_mode": "hardware",
|
|
"control_transport": "wireless_mqtt",
|
|
"execution_report": {
|
|
"spec": {
|
|
"results": [
|
|
{
|
|
"ref": "shade-position",
|
|
"status": "failed",
|
|
"message": (
|
|
"no fresh valid 0x41A shade position; "
|
|
"refusing blind motion"
|
|
),
|
|
"telemetry": {
|
|
"verification_status": "failed_or_blocked"
|
|
},
|
|
}
|
|
]
|
|
}
|
|
},
|
|
}
|
|
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"},
|
|
}
|
|
]
|
|
}
|
|
},
|
|
},
|
|
blocked_shade,
|
|
]
|
|
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, "-"],
|
|
input=script,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
parsed = json.loads(completed.stdout)
|
|
assert [len(commands) for commands in parsed] == [1, 1, 1, 1, 1]
|
|
assert [commands[0]["command"] for commands in parsed] == [
|
|
"ac on",
|
|
"ac on",
|
|
"ac on",
|
|
"ac on",
|
|
"shade position 30",
|
|
]
|
|
assert [commands[0]["deliveryLabel"] for commands in parsed] == [
|
|
"未下发",
|
|
"未下发",
|
|
"已下发 · 待状态确认",
|
|
"已下发 · 待状态确认",
|
|
"安全阻止",
|
|
]
|
|
assert [commands[0]["deliveryState"] for commands in parsed] == [
|
|
"not_submitted",
|
|
"not_submitted",
|
|
"submitted",
|
|
"submitted",
|
|
"blocked",
|
|
]
|
|
assert "0x41A 实时位置" in parsed[-1][0]["deliveryDetail"]
|
|
assert [commands[0]["deliveryTransport"] for commands in parsed] == [
|
|
"有线 CAN",
|
|
"无线 CAN",
|
|
"有线 CAN",
|
|
"无线 CAN",
|
|
"无线 CAN",
|
|
]
|
|
|
|
|
|
def test_delivery_status_distinguishes_blocked_failed_aborted_and_verified() -> None:
|
|
node = shutil.which("node")
|
|
if node is None:
|
|
return
|
|
console = source("console-v2.js")
|
|
assert 'class="v2-can-command-delivery-detail"' in console
|
|
assert "<b>状态说明</b>" in console
|
|
start = console.index(" const normalizedCanTarget")
|
|
end = console.index("\n\n function renderParsedCanCommands", start)
|
|
helpers = console[start:end]
|
|
|
|
def sample(
|
|
operation: dict,
|
|
*,
|
|
status: str,
|
|
message: str = "",
|
|
telemetry: dict | None = None,
|
|
) -> dict:
|
|
return {
|
|
"execution_mode": "hardware",
|
|
"control_transport": "wireless_mqtt",
|
|
"function_configs": {
|
|
"sample": {"spec": {"operations": [operation]}}
|
|
},
|
|
"execution_report": {
|
|
"spec": {
|
|
"results": [
|
|
{
|
|
"ref": operation["id"],
|
|
"status": status,
|
|
"message": message,
|
|
"telemetry": telemetry or {},
|
|
}
|
|
]
|
|
}
|
|
},
|
|
}
|
|
|
|
samples = [
|
|
sample(
|
|
{
|
|
"id": "window-blocked",
|
|
"capability": "window.set_position",
|
|
"target": {"zone": "front-right"},
|
|
"parameters": {"position_pct": 20},
|
|
},
|
|
status="failed",
|
|
message="未收到有效且新鲜的 0x388 车窗位置,拒绝启动机构",
|
|
telemetry={"verification_status": "failed_or_blocked"},
|
|
),
|
|
sample(
|
|
{
|
|
"id": "infrared-dispatch-failed",
|
|
"capability": "infrared.emit",
|
|
"target": {"device": "reading-light", "name": "阅读灯"},
|
|
"parameters": {"action": "开启", "slot": 2},
|
|
},
|
|
status="failed",
|
|
message="GSCU1B 尚未连接到本地 TCP 桥接",
|
|
telemetry={"delivery_status": "failed"},
|
|
),
|
|
sample(
|
|
{
|
|
"id": "infrared-aborted",
|
|
"capability": "infrared.emit",
|
|
"target": {"device": "reading-light", "name": "阅读灯"},
|
|
"parameters": {"action": "切换为暖光", "slot": 5},
|
|
},
|
|
status="aborted",
|
|
message="前序红外指令失败,未执行",
|
|
telemetry={"delivery_status": "not_submitted"},
|
|
),
|
|
sample(
|
|
{
|
|
"id": "climate-verified",
|
|
"capability": "climate.set_power",
|
|
"target": {"zone": "vehicle"},
|
|
"parameters": {"state": "on"},
|
|
},
|
|
status="succeeded",
|
|
telemetry={
|
|
"delivery_status": "delivered",
|
|
"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",
|
|
"capability": "climate.set_power",
|
|
"target": {"zone": "vehicle"},
|
|
"parameters": {"state": "on"},
|
|
},
|
|
status="failed",
|
|
message="网关提交后执行失败",
|
|
telemetry={"delivery_status": "submitted"},
|
|
),
|
|
]
|
|
script = (
|
|
f"{helpers}\n"
|
|
f"const samples = {json.dumps(samples, ensure_ascii=False)};\n"
|
|
"process.stdout.write(JSON.stringify(samples.map((sample) => "
|
|
"parsedCanCommands(sample)[0])));"
|
|
)
|
|
completed = subprocess.run(
|
|
[node, "-"],
|
|
input=script,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
commands = json.loads(completed.stdout)
|
|
assert [item["deliveryLabel"] for item in commands] == [
|
|
"安全阻止",
|
|
"下发失败",
|
|
"已中止",
|
|
"执行已确认",
|
|
"执行已确认",
|
|
"执行失败",
|
|
]
|
|
assert [item["deliveryState"] for item in commands] == [
|
|
"blocked",
|
|
"failed",
|
|
"aborted",
|
|
"submitted",
|
|
"submitted",
|
|
"failed",
|
|
]
|
|
assert "0x388 实时位置" in commands[0]["deliveryDetail"]
|
|
assert commands[1]["deliveryDetail"] == "GSCU1B 尚未连接到本地 TCP 桥接"
|
|
|
|
|
|
def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences() -> None:
|
|
app = source("app.js")
|
|
console = source("console-v2.js")
|
|
styles = source("styles.css")
|
|
html = source("index.html")
|
|
|
|
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
|
|
compile_flow = app.split(
|
|
"async function executeModeEditorInstruction", 1
|
|
)[1].split("async function completeModeEditor", 1)[0]
|
|
assert "renderResult(" not in compile_flow
|
|
assert "cognitive-agent:validated-can-commands" not in compile_flow
|
|
assert "cognitive-agent:result" not in compile_flow
|
|
assert 'const nonDispatchTaskSources = new Set(["mode_editor_compile"])' in console
|
|
assert "if (isDispatchTask(task))" in console
|
|
assert "下发控制指令" in console
|
|
assert "已解析控制指令" not in console
|
|
assert 'async function executeSavedModeProfile(mode, { command = "" } = {})' in app
|
|
assert "/api/mode-profiles/${encodeURIComponent(mode.id)}/execute" in app
|
|
assert "assistant?.announceModeExecution?.(mode, payload" in app
|
|
assert "void executeSavedModeProfile(mode, { command: modeCommand });" in app
|
|
assert "announceModeExecution" in source("assistant-chat.js")
|
|
assert 'const savedModeExecution = result.orchestration_route === "saved_mode_command_flow";' in app
|
|
assert "`${result.mode_profile.name}执行完成`" in app
|
|
|
|
assert "生成指令" in html
|
|
assert ">执行</span></button>" not in html
|
|
assert 'id="hardwareNote"' not in html
|
|
assert 'id="hardwareAliasNote"' not in html
|
|
assert 'id="vehicleTransportNote"' not in html
|
|
|
|
assert ".mode-editor-command-card" in styles
|
|
assert ".mode-editor-command-meta" in styles
|
|
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, "-"],
|
|
input=script,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
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_left_sidebar_extracts_speech_and_peripheral_control_commands() -> None:
|
|
node = shutil.which("node")
|
|
if node is None:
|
|
return
|
|
console = source("console-v2.js")
|
|
helper_start = console.index(" const normalizedCanTarget")
|
|
helper_end = console.index("\n\n function renderParsedCanCommands", helper_start)
|
|
helpers = console[helper_start:helper_end]
|
|
sample = {
|
|
"execution_mode": "dry_run",
|
|
"function_configs": {
|
|
"native_climate": {
|
|
"spec": {
|
|
"operations": [
|
|
{
|
|
"id": "speech-temperature",
|
|
"capability": "climate.set_temperature",
|
|
"target": {"device": "hvac", "zone": "driver"},
|
|
"parameters": {"temperature_c": 24},
|
|
"parse_source": "speech_semantic",
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"self_extension": {
|
|
"spec": {
|
|
"operations": [
|
|
{
|
|
"id": "speech-peripheral",
|
|
"capability": "infrared.emit",
|
|
"target": {
|
|
"device": "third-party-light",
|
|
"name": "第三方氛围灯",
|
|
},
|
|
"parameters": {"action": "开启", "slot": 9},
|
|
"parse_source": "local_rule",
|
|
}
|
|
]
|
|
}
|
|
},
|
|
},
|
|
}
|
|
script = (
|
|
f"{helpers}\n"
|
|
f"const result = {json.dumps(sample, ensure_ascii=False)};\n"
|
|
"process.stdout.write(JSON.stringify({"
|
|
"commands:parsedCanCommands(result),"
|
|
'speechLabel:parsedCommandSourceLabel("speech_semantic")'
|
|
"}));"
|
|
)
|
|
completed = subprocess.run(
|
|
[node, "-"],
|
|
input=script,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
output = json.loads(completed.stdout)
|
|
|
|
assert output["speechLabel"] == "语音语义"
|
|
assert len(output["commands"]) == 2
|
|
assert {item["channel"] for item in output["commands"]} == {"CAN1", "IR"}
|
|
assert {item["command"] for item in output["commands"]} == {
|
|
"ac temp driver 24",
|
|
"infrared emit 9",
|
|
}
|
|
|
|
|
|
def test_interactive_console_uses_one_top_right_icon_dock_for_all_five_panels() -> None:
|
|
console = source("console-v2.js")
|
|
assistant = source("assistant-chat.js")
|
|
energy = source("energy-monitor.js")
|
|
styles = source("console-v2.css")
|
|
extension = source("self-extension.js")
|
|
|
|
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 console.index('id="v2DevicePanel"') < console.index('id="v2ExtensionPanel"')
|
|
assert 'class="v2-side-status-stack"' in console
|
|
assert 'class="v2-panel-control-dock"' in console
|
|
assert "function buildModuleSwitcher(mainPanel, modeRail)" in console
|
|
assert 'const modeRailBar = $(".mode-rail-bar", modeRail);' in console
|
|
assert "if (modeRailBar) modeRailBar.prepend(switcher);" in console
|
|
assert 'const homepageHeader = moduleSwitcher.closest(".v2-mode-rail") || moduleSwitcher;' in extension
|
|
assert 'homepageHeader.insertAdjacentElement("afterend", root);' in extension
|
|
assert 'class="v2-unified-brand"' not in console
|
|
assert "网页控制链路" not in console
|
|
assert '<div class="v2-sync-sidebar-heading">' not in console
|
|
assert 'panelDock.append(sidebarToggle)' in console
|
|
assert 'panelDock.append(deviceToggle, extensionToggle)' in console
|
|
assert 'class="v2-side-panel-dock"' not in console
|
|
assert 'deviceToggle.dataset.panelToggle = "devices"' in console
|
|
assert 'extensionToggle.dataset.panelToggle = "extensions"' in console
|
|
assert 'data-lucide="car-front"' in console
|
|
assert 'data-lucide="usb"' in console
|
|
assert 'data-lucide="bot-message-square"' in assistant
|
|
assert 'const panelDock = $("#v2PanelControlDock")' in assistant
|
|
assert "panelDock.insertBefore(trigger, firstRightPanelToggle)" in assistant
|
|
assert 'trigger.className = "assistant-drawer-toggle v2-panel-control-button"' in assistant
|
|
assert 'trigger.className = "energy-monitor-toggle v2-panel-control-button"' in energy
|
|
assert '$("#v2PanelControlDock").append(trigger)' in energy
|
|
assert 'trigger.setAttribute("aria-controls", "energyMonitorLayer")' in energy
|
|
assert "panel.hidden = !visible;" in console
|
|
assert 'rightRail.hidden = collapsed;' in console
|
|
assert 'rightRail.toggleAttribute("inert", collapsed)' in console
|
|
assert "data-panel-toggle" not in extension
|
|
assert 'setSidePanelVisibility("devices", false)' in console
|
|
assert 'setSidePanelVisibility("extensions", false)' in console
|
|
assert "setDrawerOpen(true)" in assistant
|
|
assert ".v2-panel-control-dock" in styles
|
|
assert "#v2PanelControlDock > .v2-panel-control-button" in styles
|
|
assert '.v2-panel-control-button[aria-pressed="true"]' in styles
|
|
assert "background: var(--primary);" in styles
|
|
assert "width: 38px !important;" in styles
|
|
assert "Unified homepage header: panel controls and function modes share one row." in styles
|
|
assert "grid-template-columns: auto auto minmax(0, 1fr) auto;" in styles
|
|
final_styles = styles.split("/* AI assistant light-blue final override", 1)[1]
|
|
assert 'data-open-panels="none"' in final_styles
|
|
assert "grid-template-columns: minmax(0, 1fr);" in final_styles
|
|
assert ".v2-unified-right-rail[hidden]" in final_styles
|
|
|
|
|
|
def test_runtime_hardware_coverage_defines_the_linked_readiness_state() -> None:
|
|
app = source("app.js")
|
|
|
|
definition = "const linkedHardwareReady = seatHardwareReady && vehicleHardwareReady;"
|
|
assert definition in app
|
|
assert app.index(definition) < app.index(": linkedHardwareReady")
|
|
|
|
|
|
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);' not in console
|
|
assert 'commandPanel.id = "v2SidebarPanel"' in console
|
|
assert 'sidebarToggle.setAttribute("aria-controls", commandPanel.id)' in console
|
|
assert "sidebarCollapsed: true" in console
|
|
assert 'sidebar.toggleAttribute("inert", ui.sidebarCollapsed)' in console
|
|
assert 'button.setAttribute("aria-pressed", String(expanded))' in console
|
|
assert "function syncSidebarLogBaseline()" in console
|
|
assert "if (!sidebar || !lowerPanel || ui.sidebarCollapsed) return;" 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
|
|
final_styles = styles.split("/* AI assistant light-blue final override", 1)[1]
|
|
assert "grid-template-columns: 0 minmax(0, 1fr);" in final_styles
|
|
assert ".command-panel.v2-sidebar" in final_styles
|
|
assert "visibility: hidden;" in final_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:
|
|
app = source("app.js")
|
|
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 "width: 94px;" in styles
|
|
assert "max-width: 94px;" in styles
|
|
assert "box-sizing: border-box;" in styles
|
|
assert "text-overflow: ellipsis;" in styles
|
|
assert "function twinDeviceWorldAnchor(" in app
|
|
assert "function positionTwinDeviceHotspots()" in app
|
|
assert "projectedHotspot.copy(hotspotWorldAnchor).project(camera)" in app
|
|
assert 'button.dataset.anchorSource = "projected-3d"' in app
|
|
assert 'hotspotLayer.dataset.anchorLayout = "projected-3d"' in app
|
|
assert "clampLabelCoordinate(" in app
|
|
assert "positionTwinDeviceHotspots();" in app
|
|
assert "function positionTwinDevicePopover(" in console
|
|
assert "function twinPopoverIntersectionArea(" in console
|
|
assert 'Object.keys(availableSpace).sort' in console
|
|
assert 'popover.dataset.anchorDevice = deviceId' in console
|
|
assert 'popover.dataset.placement = best.placement' in console
|
|
assert 'popover.dataset.shadeOverlap = String(Math.round(best.shadeOverlap))' in console
|
|
assert 'popover.dataset.labelOverlap = String(Math.round(best.labelOverlap))' in console
|
|
assert 'popover.dataset.layoutSignature === geometrySignature' in console
|
|
assert 'window.CognitiveAgentConsoleV2?.positionTwinDevicePopover?.();' in app
|
|
popover_styles = styles.split(".console-v2 .v2-twin-device-popover {", 1)[1].split("}", 1)[0]
|
|
assert "top: 52px;" not in popover_styles
|
|
assert "left: 50%;" not in popover_styles
|
|
assert "transform: translateX(-50%);" not in popover_styles
|
|
assert 'width: min(280px, calc(100% - 24px));' in styles
|
|
assert 'repeat(auto-fit, minmax(44px, 1fr))' in styles
|
|
for device_id in (
|
|
"windows", "shade", "driver", "passenger",
|
|
"reading", "ambient", "headlight", "climate",
|
|
):
|
|
assert f"{device_id}: {{ normalized:" in app
|
|
assert '.v2-twin-device-popover[data-pinned="false"]' in styles
|
|
assert "pointer-events: none;" in styles
|
|
|
|
|
|
def test_vehicle_twin_keeps_whole_car_centered_and_removes_axis_callouts() -> None:
|
|
html = source("index.html")
|
|
app = source("app.js")
|
|
styles = source("console-v2.css")
|
|
|
|
assert 'twin.view.targetX = 0;' in app
|
|
assert 'twin.view.yaw = -0.62;' in app
|
|
assert 'canvas.dataset.framing = "whole-vehicle-centered"' in app
|
|
assert "1.28 / Math.max(0.64, camera.aspect)" in app
|
|
assert 'class="twin-callout-layer"' not in html
|
|
assert 'class="twin-callout' not in html
|
|
assert "data-callout=" not in html
|
|
assert "function positionTwinCallouts()" not in app
|
|
assert "renderTwinCalloutValues" not in app
|
|
assert ".joint-twin-shell > .twin-band" in styles
|
|
assert "container-type: inline-size;" in styles
|
|
assert "@container (max-width: 420px)" in styles
|
|
assert "grid-template-columns: minmax(0, 1fr);" 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
|
|
|
|
|
|
def test_compact_mode_strip_supports_wheel_drag_and_keyboard_navigation() -> None:
|
|
app = source("app.js")
|
|
styles = source("console-v2.css")
|
|
|
|
scroller = app.split("function installModeChipScroller()", 1)[1].split(
|
|
"function populateModeChips", 1
|
|
)[0]
|
|
assert 'group.dataset.modeScrollerBound = "true"' in scroller
|
|
assert "group.tabIndex = 0" in scroller
|
|
assert "滚轮、拖动或方向键横向浏览" in scroller
|
|
assert 'group.addEventListener("wheel"' in scroller
|
|
assert "Math.abs(event.deltaX) > Math.abs(event.deltaY)" in scroller
|
|
assert "scrollBy(dominantDelta * multiplier)" in scroller
|
|
assert "event.preventDefault()" in scroller
|
|
assert "{ passive: false }" in scroller
|
|
|
|
assert 'group.addEventListener("pointerdown"' in scroller
|
|
assert 'event.pointerType !== "mouse"' in scroller
|
|
assert "group.setPointerCapture(pointerId)" in scroller
|
|
assert scroller.index("group.setPointerCapture(pointerId)") > scroller.index(
|
|
"Math.abs(distance) < 5"
|
|
)
|
|
assert "let pointerCaptured = false;" in scroller
|
|
assert 'group.addEventListener("pointermove"' in scroller
|
|
assert "Math.abs(distance) < 5" in scroller
|
|
assert 'group.classList.add("is-dragging")' in scroller
|
|
assert 'group.addEventListener("pointerup", finishPointerDrag)' in scroller
|
|
assert 'group.addEventListener("pointercancel", finishPointerDrag)' in scroller
|
|
assert "event.stopImmediatePropagation()" in scroller
|
|
|
|
assert 'group.addEventListener("keydown"' in scroller
|
|
for key in ("ArrowLeft", "ArrowRight", "PageUp", "PageDown", "Home", "End"):
|
|
assert f'event.key === "{key}"' in scroller
|
|
assert "new MutationObserver" in scroller
|
|
assert 'new ResizeObserver(updateScrollState).observe(group)' in scroller
|
|
assert "installModeChipScroller();" in app
|
|
|
|
mode_strip_styles = styles.split("/* Compact function-mode strip", 1)[1]
|
|
assert "overflow-x: auto" in mode_strip_styles
|
|
assert "overflow-y: hidden" in mode_strip_styles
|
|
assert "touch-action: pan-x" in mode_strip_styles
|
|
assert "cursor: grab" in mode_strip_styles
|
|
assert ".mode-chips:focus-visible" in mode_strip_styles
|
|
assert ".mode-chips.is-dragging" in mode_strip_styles
|
|
assert "cursor: grabbing" in mode_strip_styles
|
|
|
|
|
|
def test_assistant_mode_preview_groups_validated_actions_for_the_card() -> None:
|
|
node = shutil.which("node")
|
|
if node is None:
|
|
return
|
|
assistant = source("assistant-chat.js")
|
|
helper_start = assistant.index(" function modeDraftRows")
|
|
helper_end = assistant.index("\n\n async function saveModeProfileDraft", helper_start)
|
|
helper = assistant[helper_start:helper_end]
|
|
draft = {
|
|
"name": "观影模式",
|
|
"instruction": "遮阳帘关闭,车窗关闭,副驾座椅调整为零重力模式,灯光全部关闭",
|
|
}
|
|
actions = [
|
|
{
|
|
"domain": "roof",
|
|
"capability": "roof.set_shade_motion",
|
|
"target": {"zone": "vehicle"},
|
|
"parameters": {"motion": "close"},
|
|
},
|
|
{
|
|
"domain": "window",
|
|
"capability": "window.set_motion",
|
|
"target": {"zone": "all"},
|
|
"parameters": {"motion": "close"},
|
|
},
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_longitudinal_position",
|
|
"target": {"zone": "front_right", "instance": "passenger"},
|
|
"parameters": {"position_pct": 100},
|
|
},
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_backrest_recline",
|
|
"target": {"zone": "front_right", "instance": "passenger"},
|
|
"parameters": {"position_pct": 81, "angle_deg": 150},
|
|
},
|
|
{
|
|
"domain": "interior_light",
|
|
"capability": "interior_light.set_indoor_power",
|
|
"target": {"zone": "vehicle"},
|
|
"parameters": {"state": "off"},
|
|
},
|
|
{
|
|
"domain": "ambient_light",
|
|
"capability": "ambient_light.set_power",
|
|
"target": {"zone": "vehicle"},
|
|
"parameters": {"state": "off"},
|
|
},
|
|
]
|
|
script = (
|
|
f"{helper}\n"
|
|
f"const draft = {json.dumps(draft, ensure_ascii=False)};\n"
|
|
f"const actions = {json.dumps(actions, ensure_ascii=False)};\n"
|
|
"process.stdout.write(JSON.stringify(modeDraftRows(draft, actions)));"
|
|
)
|
|
completed = subprocess.run(
|
|
[node, "-"],
|
|
input=script,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
assert json.loads(completed.stdout) == [
|
|
{"label": "遮阳帘", "value": "关闭", "icon": "blinds", "order": 0},
|
|
{"label": "全部车窗", "value": "关闭", "icon": "panels-top-left", "order": 6},
|
|
{"label": "副驾座椅", "value": "零重力模式", "icon": "armchair", "order": 11},
|
|
{"label": "座舱灯光", "value": "全部关闭", "icon": "lightbulb-off", "order": 24},
|
|
]
|