feat: integrate production cockpit AI assistant
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
WEB = ROOT / "apps" / "web-console"
|
||||
|
||||
@@ -17,7 +16,7 @@ def test_runtime_mode_catalog_exposes_expected_static_and_dynamic_membership() -
|
||||
modes = registry.public_modes(vehicle_id="denza_n7")
|
||||
grouped = {
|
||||
behavior: {mode["id"] for mode in modes if mode["behavior"] == behavior}
|
||||
for behavior in {"static", "cyclic"}
|
||||
for behavior in ("static", "cyclic")
|
||||
}
|
||||
|
||||
assert grouped["static"] == {
|
||||
@@ -37,6 +36,35 @@ def test_runtime_mode_catalog_exposes_expected_static_and_dynamic_membership() -
|
||||
assert len(modes) == len(grouped["static"] | grouped["cyclic"])
|
||||
|
||||
|
||||
def test_runtime_mode_catalog_exposes_safe_executable_action_details() -> None:
|
||||
registry = ModeProfileRegistry(ROOT / "config" / "modes")
|
||||
office = next(
|
||||
mode
|
||||
for mode in registry.public_modes(vehicle_id="denza_n7")
|
||||
if mode["id"] == "office"
|
||||
)
|
||||
|
||||
assert len(office["operations"]) == office["operation_count"] == 10
|
||||
assert office["operations"][0] == {
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
"target_scope": "passenger",
|
||||
"operation": "set",
|
||||
"parameters": {"position_pct": 72},
|
||||
}
|
||||
assert all(
|
||||
set(operation) == {
|
||||
"domain",
|
||||
"capability",
|
||||
"target_scope",
|
||||
"operation",
|
||||
"parameters",
|
||||
}
|
||||
for operation in office["operations"]
|
||||
)
|
||||
assert all("reason" not in operation for operation in office["operations"])
|
||||
|
||||
|
||||
def node_runtime() -> str:
|
||||
candidates = [
|
||||
shutil.which("node"),
|
||||
@@ -63,7 +91,7 @@ def node_runtime() -> str:
|
||||
raise AssertionError("Node.js is required to execute the mode gallery grouping contract")
|
||||
|
||||
|
||||
def test_mode_gallery_classifies_runtime_profiles_into_two_behavior_groups() -> None:
|
||||
def test_mode_gallery_classifies_profiles_into_exclusive_custom_static_dynamic_groups() -> None:
|
||||
app = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
start = app.index("const MODE_GALLERY_GROUPS")
|
||||
end = app.index("\n\nfunction createModeGalleryCard", start)
|
||||
@@ -102,12 +130,13 @@ console.log(JSON.stringify(context.result));
|
||||
result = json.loads(completed.stdout)
|
||||
|
||||
assert [(group["id"], group["title"]) for group in result["groups"]] == [
|
||||
("custom", "自定义模式"),
|
||||
("static", "静态模式"),
|
||||
("dynamic", "动态模式"),
|
||||
]
|
||||
assert result["groups"][0]["modeIds"] == ["user-static", "office", "unknown"]
|
||||
assert result["groups"][1]["modeIds"] == [
|
||||
"future-cycle",
|
||||
assert result["groups"][0]["modeIds"] == ["user-static", "future-cycle"]
|
||||
assert result["groups"][1]["modeIds"] == ["office", "unknown"]
|
||||
assert result["groups"][2]["modeIds"] == [
|
||||
"rocking_horse",
|
||||
"timeline-only",
|
||||
"future-behavior",
|
||||
@@ -115,7 +144,7 @@ console.log(JSON.stringify(context.result));
|
||||
assert result["dynamicFlags"] == [False, False, True, True, True, False, True]
|
||||
|
||||
|
||||
def test_mode_gallery_layout_and_copy_expose_only_static_and_dynamic_sections() -> None:
|
||||
def test_mode_gallery_layout_and_copy_expose_three_management_sections() -> None:
|
||||
app = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
html = (WEB / "index.html").read_text(encoding="utf-8")
|
||||
styles = (WEB / "styles.css").read_text(encoding="utf-8")
|
||||
@@ -124,13 +153,98 @@ def test_mode_gallery_layout_and_copy_expose_only_static_and_dynamic_sections()
|
||||
: app.index("const MODE_DOMAIN_LABELS")
|
||||
]
|
||||
|
||||
assert definitions.count('title: "自定义模式"') == 1
|
||||
assert definitions.count('title: "静态模式"') == 1
|
||||
assert definitions.count('title: "动态模式"') == 1
|
||||
for legacy_group in ('id: "personal"', 'id: "frequent"', 'id: "comfort"'):
|
||||
assert legacy_group not in definitions
|
||||
assert 'if (group.id === "static") grid.append(createModeGalleryAddCard())' in app
|
||||
assert 'if (group.id === "custom") grid.append(createModeGalleryAddCard())' in app
|
||||
assert "function createModeGalleryEmpty(group)" in app
|
||||
assert 'section.setAttribute("aria-labelledby", headingId)' in app
|
||||
assert "静态模式 · 动态模式" in html
|
||||
assert "按运行方式整理,预设与自定义模式统一归类" in html
|
||||
assert "自定义模式 · 静态模式 · 动态模式" in html
|
||||
assert "按来源与运行方式整理,快速定位并管理模式" in html
|
||||
assert "grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));" in styles
|
||||
|
||||
|
||||
def test_top_mode_cards_render_real_actions_in_a_body_level_hover_and_focus_preview() -> None:
|
||||
app = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
styles = (WEB / "console-v2.css").read_text(encoding="utf-8")
|
||||
|
||||
assert "function modeExecutionActionViews(mode)" in app
|
||||
assert "Array.isArray(mode?.operations)" in app
|
||||
assert "function renderModeActionPopover(mode)" in app
|
||||
assert 'popover.setAttribute("role", "tooltip")' in app
|
||||
assert "document.body.append(popover)" in app
|
||||
assert 'device.innerHTML = "<b>设备</b>"' in app
|
||||
assert 'target.innerHTML = "<b>目标</b>"' in app
|
||||
assert 'parameters.innerHTML = "<b>参数</b>"' in app
|
||||
assert 'button.setAttribute("aria-describedby", popover.id)' in app
|
||||
assert 'group.addEventListener("pointerover"' in app
|
||||
assert 'group.addEventListener("pointerout"' in app
|
||||
assert 'group.addEventListener("focusin"' in app
|
||||
assert 'group.addEventListener("focusout"' in app
|
||||
assert 'group.addEventListener("scroll", () => hideModeActionPopover({ immediate: true })' in app
|
||||
assert 'window.addEventListener("resize", () => hideModeActionPopover({ immediate: true })' in app
|
||||
assert "installModeActionPreview();" in app
|
||||
assert ".mode-chip-action-popover" in styles
|
||||
popover_styles = styles.split(".mode-chip-action-popover {", 1)[1].split("}", 1)[0]
|
||||
assert "position: fixed" in popover_styles
|
||||
assert "max-height:" in popover_styles
|
||||
assert "overflow: hidden" in popover_styles
|
||||
assert ".mode-chip-action-popover ol" in styles
|
||||
assert "overflow-y: auto" in styles.split(".mode-chip-action-popover ol", 1)[1].split("}", 1)[0]
|
||||
|
||||
|
||||
def test_top_mode_action_formatter_uses_device_target_action_and_parameter_values() -> None:
|
||||
registry = ModeProfileRegistry(ROOT / "config" / "modes")
|
||||
office = next(
|
||||
mode
|
||||
for mode in registry.public_modes(vehicle_id="denza_n7")
|
||||
if mode["id"] == "office"
|
||||
)
|
||||
app = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
start = app.index("const MODE_TARGET_LABELS")
|
||||
end = app.index("\n\nfunction isUserModeProfile", start)
|
||||
snippet = app[start:end]
|
||||
script = (
|
||||
'const MODE_DOMAIN_LABELS = { seat: "座椅", climate: "空调", roof: "天幕", '
|
||||
'ambient_light: "氛围灯" };\n'
|
||||
'const CONFIG_PARAMETER_LABELS = { position_pct: "目标位置", '
|
||||
'temperature_c: "目标温度", level: "档位", mode: "工作模式", '
|
||||
'shade_position_pct: "遮阳帘位置", brightness_pct: "目标亮度", '
|
||||
'color_hex: "目标颜色" };\n'
|
||||
'const CONFIG_CAPABILITY_LABELS = { '
|
||||
'"seat.set_longitudinal_position": "调节座椅前后位置", '
|
||||
'"climate.set_temperature": "设置空调温度" };\n'
|
||||
'function formatConfigValue(name, value) { '
|
||||
'if (name === "temperature_c") return `${value} ℃`; '
|
||||
'if (name.endsWith("_pct")) return `${value}%`; return String(value); }\n'
|
||||
'function modeSecondsLabel(value) { return String(Number(value) / 1000); }\n'
|
||||
+ snippet
|
||||
+ "\nconst mode = "
|
||||
+ json.dumps(office, ensure_ascii=False)
|
||||
+ "; console.log(JSON.stringify(modeExecutionActionViews(mode)));"
|
||||
)
|
||||
completed = subprocess.run(
|
||||
[node_runtime(), "-e", script],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
actions = json.loads(completed.stdout)
|
||||
|
||||
assert len(actions) == office["operation_count"]
|
||||
assert actions[0] == {
|
||||
"device": "座椅",
|
||||
"target": "副驾",
|
||||
"action": "调节座椅前后位置",
|
||||
"parameters": "目标位置:72%",
|
||||
}
|
||||
temperature = next(action for action in actions if action["action"] == "设置空调温度")
|
||||
assert temperature == {
|
||||
"device": "空调",
|
||||
"target": "副驾",
|
||||
"action": "设置空调温度",
|
||||
"parameters": "目标温度:25 ℃",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user