c812a1a595
Integrate the remote firmware workbench, OTA delivery, and timed mode schedules while preserving the local N7 scene and runtime changes.
369 lines
14 KiB
Python
369 lines
14 KiB
Python
import base64
|
|
import hashlib
|
|
import re
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
WEB = ROOT / "apps" / "web-console"
|
|
|
|
|
|
def source(name: str) -> str:
|
|
content = (WEB / name).read_text(encoding="utf-8")
|
|
if name == "index.html":
|
|
content += "\n" + (WEB / "src" / "App.vue").read_text(encoding="utf-8")
|
|
return content
|
|
|
|
|
|
def test_shadcn_semantic_theme_tokens_drive_the_console() -> None:
|
|
styles = source("styles.css")
|
|
console = source("console-v2.css")
|
|
speech = source("speech-input.css")
|
|
|
|
expected_tokens = {
|
|
"--background: oklch(0.968 0.019 244)",
|
|
"--foreground: oklch(0.29 0.042 252)",
|
|
"--card-foreground: oklch(0.34 0.032 250)",
|
|
"--primary: oklch(0.57 0.19 258)",
|
|
"--primary-foreground: oklch(0.99 0.004 245)",
|
|
"--destructive: oklch(0.61 0.19 24)",
|
|
"--sidebar-primary: oklch(0.57 0.19 258)",
|
|
"--background: oklch(0.2 0.035 252)",
|
|
"--foreground: oklch(0.94 0.02 245)",
|
|
"--primary: oklch(0.7 0.15 252)",
|
|
"--sidebar-primary: oklch(0.7 0.15 252)",
|
|
}
|
|
token_lines = {line.strip() for line in styles.replace(";", "").splitlines()}
|
|
assert expected_tokens <= token_lines
|
|
assert ".dark {" in styles
|
|
assert "--v2-accent: var(--primary)" in console
|
|
assert "--v2-sidebar: var(--sidebar)" in console
|
|
assert "color: var(--destructive)" in speech
|
|
|
|
|
|
def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
|
|
main = source("src/main.js")
|
|
service_worker = source("service-worker.js")
|
|
|
|
for source_asset, cached_asset in (
|
|
('import "../styles.css"', "/styles.css?v=dual-seat-23"),
|
|
('import "../console-v2.css"', "/console-v2.css?v=delivery-state-69"),
|
|
('import "../self-extension.css"', "/self-extension.css?v=pet-settings-52"),
|
|
('import "../speech-input.css"', "/speech-input.css?v=sensevoice-7"),
|
|
('import "../codepen-glass-theme.css"', "/codepen-glass-theme.css?v=creative-cloud-6-performance"),
|
|
):
|
|
assert source_asset in main
|
|
assert f'"{cached_asset}"' in service_worker
|
|
assert 'import "./styles/design-system.css"' in main
|
|
assert 'const CACHE_VERSION = "cognitive-agent-pad-v322-n7-firmware-merge"' in service_worker
|
|
|
|
|
|
def test_reference_glass_theme_covers_the_complete_workspace() -> None:
|
|
html = source("index.html")
|
|
theme = source("codepen-glass-theme.css")
|
|
app = source("app.js")
|
|
|
|
assert '<meta name="theme-color" content="#17182b" id="themeColorMeta" />' in html
|
|
assert 'const key = "cognitive-agent.glass-theme"' in html
|
|
assert 'id="glassThemeToggle"' in html
|
|
assert 'data-theme-icon="sun"' in html
|
|
assert 'data-theme-icon="moon"' in html
|
|
assert 'const GLASS_THEME_STORAGE_KEY = "cognitive-agent.glass-theme"' in app
|
|
assert "localStorage.setItem(GLASS_THEME_STORAGE_KEY, normalized)" in app
|
|
assert "document.cookie = `${GLASS_THEME_STORAGE_KEY}=${normalized}" in app
|
|
assert 'new CustomEvent("cognitive-agent:theme-change"' in app
|
|
for contract in (
|
|
"--glass-shell:",
|
|
"--glass-panel:",
|
|
"--glass-card:",
|
|
"backdrop-filter: blur(22px) saturate(1.25)",
|
|
".main-panel > .v2-mode-rail",
|
|
"#assistantDrawer",
|
|
".joint-twin-shell > .twin-band",
|
|
"#v2ExtensionPanel .v2-extension-sync-mount",
|
|
".v2-pet-card",
|
|
".v2-pet-presence-card",
|
|
".v2-pet-settings-dialog",
|
|
".mode-gallery",
|
|
".v2-nfc-dialog",
|
|
".energy-monitor-page",
|
|
):
|
|
assert contract in theme
|
|
assert 'html[data-glass-theme="light"] body.console-v2' in theme
|
|
assert ".glass-theme-toggle" in theme
|
|
assert "width: 100vw !important" in theme
|
|
assert "height: 100dvh !important" in theme
|
|
assert "margin: 0 !important" in theme
|
|
assert "border: 0 !important" in theme
|
|
assert "border-radius: 0 !important" in theme
|
|
|
|
|
|
def test_theme_bootstrap_is_allowed_by_csp_without_unsafe_inline() -> None:
|
|
html = (WEB / "index.html").read_text(encoding="utf-8")
|
|
inline_script = re.search(r"<script>(.*?)</script>", html, flags=re.DOTALL)
|
|
assert inline_script is not None
|
|
digest = base64.b64encode(
|
|
hashlib.sha256(inline_script.group(1).encode("utf-8")).digest()
|
|
).decode("ascii")
|
|
server = (ROOT / "src" / "cognitive_agent" / "server.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
assert f"'sha256-{digest}'" in server
|
|
assert "'unsafe-inline'" not in server
|
|
|
|
|
|
def test_reference_theme_uses_one_transform_only_floating_card_contract() -> None:
|
|
theme = source("codepen-glass-theme.css")
|
|
|
|
for contract in (
|
|
"--glass-card-radius:",
|
|
"--glass-card-lift:",
|
|
"--glass-card-rest-shadow:",
|
|
"--glass-card-float-shadow:",
|
|
".assistant-dispatched-controls",
|
|
".assistant-mode-draft",
|
|
".mode-gallery-card",
|
|
".mode-editor-command-card",
|
|
".v2-device-card",
|
|
".v2-pet-card",
|
|
".energy-kpi",
|
|
"transform: translateY(var(--glass-card-lift))",
|
|
"@media (prefers-reduced-motion: reduce)",
|
|
"transition: none !important",
|
|
):
|
|
assert contract in theme
|
|
|
|
|
|
def test_dark_theme_uses_one_glass_scrim_for_every_modal_backdrop() -> None:
|
|
theme = source("codepen-glass-theme.css")
|
|
|
|
for contract in (
|
|
"--glass-dialog-scrim:",
|
|
"--glass-dialog-scrim-filter:",
|
|
".mode-gallery-backdrop",
|
|
".mode-editor-backdrop",
|
|
".energy-monitor-layer",
|
|
".device-power-backdrop",
|
|
".v2-extension-dialog::backdrop",
|
|
"background: var(--glass-dialog-scrim) !important",
|
|
"backdrop-filter: var(--glass-dialog-scrim-filter) !important",
|
|
):
|
|
assert contract in theme
|
|
|
|
|
|
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None:
|
|
styles = source("styles.css")
|
|
console = source("console-v2.css")
|
|
extension = source("self-extension.css")
|
|
|
|
expected_console_rules = {
|
|
".console-v2 .v2-twin-hotspot-label",
|
|
".console-v2 .v2-twin-device-popover",
|
|
".console-v2 .v2-reasoning-field",
|
|
".console-v2 .device-power-drawer",
|
|
".console-v2 .v2-sync-log li[data-log-status=\"error\"] > span",
|
|
"background: var(--primary)",
|
|
"background: var(--popover)",
|
|
}
|
|
expected_overlay_rules = {
|
|
".mode-gallery-backdrop",
|
|
".mode-editor-backdrop",
|
|
".gesture-drawer",
|
|
"background: var(--card)",
|
|
"border-color: var(--border)",
|
|
}
|
|
expected_extension_rules = {
|
|
".v2-extension-dialog::backdrop",
|
|
".v2-gscu-tcp-dialog",
|
|
".v2-nfc-dialog",
|
|
".v2-extension-toast.error",
|
|
".console-v2 #v2ExtensionPanel .v2-extension-gateway > button",
|
|
"background: var(--destructive)",
|
|
}
|
|
|
|
assert all(rule in console for rule in expected_console_rules)
|
|
assert all(rule in styles for rule in expected_overlay_rules)
|
|
assert all(rule in extension for rule in expected_extension_rules)
|
|
|
|
|
|
def test_operator_controls_share_status_and_typography_tokens() -> None:
|
|
html = source("index.html")
|
|
styles = source("styles.css")
|
|
console = source("console-v2.css")
|
|
script = source("console-v2.js")
|
|
|
|
for token in (
|
|
"--status-online:",
|
|
"--status-offline:",
|
|
"--status-info:",
|
|
"--status-success:",
|
|
"--status-warning:",
|
|
"--status-error:",
|
|
"--heading-foreground:",
|
|
"--body-foreground:",
|
|
"--subtle-foreground:",
|
|
"--text-caption:",
|
|
"--text-control:",
|
|
"--text-input:",
|
|
"--text-label:",
|
|
):
|
|
assert token in styles
|
|
assert '<div class="mode-rail" aria-label="功能模式">' in html
|
|
assert 'id="modeRailToggle"' in html
|
|
assert 'aria-controls="modeRailContent"' in html
|
|
assert 'id="modeRailContent"' in html
|
|
assert 'role="region"' in html
|
|
assert 'aria-label="功能模式列表"' in html
|
|
assert '<span>AI 新建</span>' in html
|
|
assert "模拟下发" in html
|
|
assert "真实下发" in html
|
|
assert 'id="simulationMode"' not in html
|
|
assert 'class="v2-sync-link-stack"' not in script
|
|
assert '.mode-option[data-connection-state="online"]::after' in styles
|
|
assert '.mode-option[data-connection-state="offline"]::after' in styles
|
|
assert "font-size: var(--text-control) !important" in console
|
|
|
|
|
|
def test_wired_can_link_requires_real_device_connection_evidence() -> None:
|
|
app = source("app.js")
|
|
|
|
assert 'runtime?.state_sync?.state === "online"' in app
|
|
assert '["online", "ready"].includes(runtime?.state_sync?.state)' not in app
|
|
assert 'wiredCanProbe: { passenger: false, vehicle: false }' in app
|
|
assert 'updateWiredCanProbe("passenger", true, requestedTransport)' in app
|
|
assert 'updateWiredCanProbe("vehicle", true, requestedTransport)' in app
|
|
assert 'updateWiredCanProbe("passenger", false, requestedTransport)' in app
|
|
assert 'updateWiredCanProbe("vehicle", false, requestedTransport)' in app
|
|
assert "resetTransportConnectionEvidence();" in app
|
|
assert 'label.textContent = `${transportLabel} · 信号链路断开`' in app
|
|
|
|
|
|
def test_command_fields_use_distinct_semantic_colors() -> None:
|
|
styles = source("styles.css")
|
|
console = source("console-v2.css")
|
|
|
|
for token in (
|
|
"--command-channel:",
|
|
"--command-source:",
|
|
"--command-delivered:",
|
|
"--command-pending:",
|
|
"--command-category:",
|
|
"--command-control:",
|
|
"--command-frame:",
|
|
):
|
|
assert token in styles
|
|
for mapping in (
|
|
"color: var(--command-channel)",
|
|
"color: var(--command-source)",
|
|
"color: var(--command-pending)",
|
|
"color: var(--command-delivered)",
|
|
"color: var(--command-category)",
|
|
"color: var(--command-control)",
|
|
"color: var(--command-frame)",
|
|
):
|
|
assert mapping in console
|
|
|
|
|
|
def test_sidebar_command_and_transport_status_use_theme_semantics() -> None:
|
|
console = source("console-v2.js")
|
|
design_system = source("src/styles/design-system.css")
|
|
|
|
assert '"v2-sidebar-footer v2-control-link-card"' in console
|
|
assert 'transport === "wireless_mqtt" ? "无线 CAN" : "有线 CAN"' in console
|
|
assert 'MQTT ${transportOnline ? "已连接" : "未连接"}' in console
|
|
assert 'footer.dataset.state = presentationState' in console
|
|
assert 'attributeFilter: ["aria-pressed", "class", "data-connection-state", "title"]' in console
|
|
assert ".v2-control-link-card" in design_system
|
|
assert 'data-state="online"' in design_system
|
|
assert 'data-state="offline"' in design_system
|
|
assert ".v2-can-command-actual code" in design_system
|
|
assert "color: var(--card-foreground) !important" in design_system
|
|
|
|
|
|
def test_function_modes_use_an_inline_accessible_collapsible() -> None:
|
|
html = source("index.html")
|
|
app = source("app.js")
|
|
console = source("console-v2.css")
|
|
|
|
assert '<div class="mode-rail-bar">' in html
|
|
assert 'id="modeRailSelection">静态模式</span>' in html
|
|
assert 'id="modeGalleryToggle"' in html
|
|
assert 'class="mode-rail-identity"' in html
|
|
assert '<span>AI 新建</span>' in html
|
|
assert 'aria-expanded="true"' in html
|
|
assert '<details class="mode-rail"' not in html
|
|
assert 'class="mode-rail-dropdown"' not in html
|
|
assert 'toggle.setAttribute("aria-expanded", String(open))' in app
|
|
assert "content.hidden = !open" in app
|
|
assert 'mainPanel.insertBefore(modeRail, output)' in source("console-v2.js")
|
|
assert 'grid-column: 1 / -1' in source("assistant-chat.css")
|
|
assert 'document.body.classList.toggle("mode-rail-expanded", open)' not in app
|
|
compact_styles = console.split("/* Compact function-mode strip", 1)[1]
|
|
assert '.mode-rail-content[hidden]' in compact_styles
|
|
assert ".mode-rail-expanded .main-panel" not in console
|
|
assert "position: static" in compact_styles
|
|
assert "overflow-x: auto" in compact_styles
|
|
assert "min-height: 54px" in compact_styles
|
|
|
|
|
|
def test_vehicle_device_cards_hide_stale_values_without_signal() -> None:
|
|
console = source("console-v2.js")
|
|
|
|
assert "function normalizeDeviceReadout(value)" in console
|
|
assert "function deviceFeedbackAvailable(deviceId)" in console
|
|
assert 'return rows.map(([label]) => [label, "-"])' in console
|
|
assert 'const waiting = rows.some(([, value]) => value === "-")' in console
|
|
assert '"cognitive-agent:vehicle-feedback-state"' in console
|
|
|
|
|
|
def test_final_theme_layers_do_not_reintroduce_raw_ui_colors() -> None:
|
|
sections = (
|
|
source("styles.css").split("/* Final semantic theme layer", 1)[1],
|
|
source("console-v2.css").split("/* Final semantic theme cascade", 1)[1],
|
|
source("self-extension.css").split(
|
|
"/* Final semantic theme layer for extension surfaces", 1
|
|
)[1],
|
|
)
|
|
raw_color = re.compile(r"#[0-9a-fA-F]{3,8}\b|rgba?\(")
|
|
|
|
for section in sections:
|
|
assert not raw_color.search(section)
|
|
|
|
|
|
def test_all_five_output_modules_are_covered_by_the_final_theme_layer() -> None:
|
|
console = source("console-v2.css")
|
|
final_layer = console.split("/* Final semantic theme cascade", 1)[1]
|
|
|
|
for selector in (
|
|
".joint-twin-shell.v2-output-page",
|
|
".decision-list li",
|
|
".result-workspace #panel-config.config-panel",
|
|
".plan-panel .frame-summary",
|
|
".execution-summary",
|
|
):
|
|
assert selector in final_layer
|
|
|
|
assert "--config-accent: var(--primary)" in final_layer
|
|
assert "background: var(--card)" in final_layer
|
|
assert "border-color: var(--border)" in final_layer
|
|
|
|
|
|
def test_ai_light_blue_theme_closes_after_legacy_color_layers() -> None:
|
|
console = source("console-v2.css")
|
|
assistant = source("assistant-chat.css")
|
|
extension = source("self-extension.css")
|
|
|
|
assert console.rfind("/* AI assistant light-blue final override") > console.rfind(
|
|
"/* Final semantic theme cascade"
|
|
)
|
|
assert assistant.rfind("/* Shared AI light-blue palette") > assistant.rfind(
|
|
"@media (prefers-reduced-motion: reduce)"
|
|
)
|
|
assert extension.rfind("/* AI light-blue closure") > extension.rfind(
|
|
"/* Final semantic theme layer for extension surfaces"
|
|
)
|
|
assert "color: var(--body-foreground)" in console
|
|
assert "color: var(--subtle-foreground)" in console
|
|
assert "background: var(--status-success-soft)" in assistant
|
|
assert "background: var(--primary)" in assistant
|