feat: 完善 N7 智能座舱交互与控制链路
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_console_contains_ai_assistant_surface():
|
||||
html = (ROOT / "apps/web-console/index.html").read_text(encoding="utf-8")
|
||||
assert 'id="assistantTranscript"' in html
|
||||
assert 'id="assistantMessages"' in html
|
||||
assert "你好,我是 AI助手。你可以向我提问" in html
|
||||
assert 'aria-label="AI助手对话"' in html
|
||||
assert 'placeholder="向 AI助手提问或交代工作"' in html
|
||||
assert 'aria-label="发送消息"' in html
|
||||
assert 'href="/assistant-chat.css?v=codex-assistant-22"' in html
|
||||
assert 'src="/assistant-chat.js?v=codex-assistant-20"' in html
|
||||
|
||||
|
||||
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
|
||||
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v269"' in service_worker
|
||||
assert '"/assistant-chat.css?v=codex-assistant-22"' in service_worker
|
||||
assert '"/assistant-chat.js?v=codex-assistant-20"' in service_worker
|
||||
|
||||
|
||||
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
|
||||
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
assert '"/api/assistant/stream"' in script
|
||||
assert 'Accept: "application/x-ndjson"' in script
|
||||
assert '"action.started", "action.progress"' in script
|
||||
assert 'event.type === "action.completed"' in script
|
||||
assert 'event.type === "answer.delta"' in script
|
||||
assert '"work.started", "work.progress"' in script
|
||||
assert 'event.type === "work.completed"' in script
|
||||
assert 'Object.prototype.hasOwnProperty.call(event, "answer")' in script
|
||||
assert 'pendingController.abort("user_stop")' in script
|
||||
assert 'signal.addEventListener("abort", cancelReader, { once: true })' in script
|
||||
assert "reader.cancel(signal.reason)" in script
|
||||
assert 'signal.removeEventListener("abort", cancelReader)' in script
|
||||
assert "reader.releaseLock()" in script
|
||||
assert 'if (!answerCompleted) throw new Error("助手响应意外中断,请重试。")' in script
|
||||
assert "event.stopImmediatePropagation()" in script
|
||||
assert "confirmation" not in script.lower()
|
||||
|
||||
|
||||
def test_codex_request_keeps_trusted_vehicle_context_without_legacy_model_routing():
|
||||
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
request_body = script.split("function requestBody(message)", 1)[1].split(
|
||||
"async function readErrorResponse", 1
|
||||
)[0]
|
||||
assert 'client_type: "web_console_codex_assistant"' in request_body
|
||||
for field in (
|
||||
"execution_mode",
|
||||
"control_transport",
|
||||
"vehicle_id",
|
||||
"hardware_client_id",
|
||||
"simulation_fault",
|
||||
):
|
||||
assert field in request_body
|
||||
assert "history" not in request_body
|
||||
assert "provider" not in request_body
|
||||
assert "reasoning_effort" not in request_body
|
||||
|
||||
|
||||
def test_starting_a_new_codex_conversation_rotates_the_conversation_id():
|
||||
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
assert 'const CONVERSATION_STORAGE_KEY = "cognitive-agent.conversation-id"' in script
|
||||
assert "function createConversationId()" in script
|
||||
assert "sessionStorage.setItem(CONVERSATION_STORAGE_KEY, createConversationId())" in script
|
||||
|
||||
|
||||
def test_assistant_uses_the_shared_top_right_dock_and_opens_by_default():
|
||||
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
css = (ROOT / "apps/web-console/assistant-chat.css").read_text(encoding="utf-8")
|
||||
assert 'trigger.id = "assistantDrawerToggle"' in script
|
||||
assert 'drawer.id = "assistantDrawer"' in script
|
||||
assert 'drawer.setAttribute("role", "dialog")' in script
|
||||
assert 'drawer.setAttribute("aria-label", "AI助手")' in script
|
||||
assert 'trigger.setAttribute("aria-controls", drawer.id)' in script
|
||||
assert 'trigger.setAttribute("aria-pressed", "false")' in script
|
||||
assert 'trigger.classList.toggle("active", open)' in script
|
||||
assert 'trigger.setAttribute("aria-pressed", String(open))' in script
|
||||
assert 'drawer.toggleAttribute("inert", !open)' in script
|
||||
assert 'trigger.className = "assistant-drawer-toggle v2-panel-control-button"' in script
|
||||
assert '($("#v2PanelControlDock") || document.body).append(trigger)' in script
|
||||
assert 'id = "assistantDrawerClose"' not in script
|
||||
assert "setDrawerOpen(true)" in script
|
||||
assert 'event.key === "Escape"' in script
|
||||
assert "#v2PanelControlDock > .assistant-drawer-toggle" in css
|
||||
assert ".assistant-drawer-open .v2-operation-shell" in css
|
||||
assert "position: fixed" in css
|
||||
assert "position: relative" in css
|
||||
assert "grid-template-rows: 56px auto minmax(0, 1fr)" in css
|
||||
assert ".v2-output-shell" in css
|
||||
|
||||
|
||||
def test_assistant_mode_creation_previews_then_saves_to_the_compact_mode_strip():
|
||||
app = (ROOT / "apps/web-console/app.js").read_text(encoding="utf-8")
|
||||
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
css = (ROOT / "apps/web-console/assistant-chat.css").read_text(encoding="utf-8")
|
||||
|
||||
assert "function startModeCreation()" in assistant
|
||||
assert 'input.value = "请帮我创建一个"' in assistant
|
||||
assert "function appendModeDraftState(message, draft, actions)" in assistant
|
||||
assert "function modeDraftRows(draft, rawActions)" in assistant
|
||||
assert "event.payload.mode_profile_draft" in assistant
|
||||
assert 'event.payload.orchestration_route === "mode_profile_create_preview"' in assistant
|
||||
assert "appendModeDraftState(" in assistant
|
||||
assert "保存为功能模式" in assistant
|
||||
assert "先试运行" in assistant
|
||||
|
||||
save_flow = assistant.split(
|
||||
"async function saveModeProfileDraft", 1
|
||||
)[1].split("async function tryModeProfileDraft", 1)[0]
|
||||
assert 'request("/api/mode-profiles"' in save_flow
|
||||
assert 'method: "POST"' in save_flow
|
||||
for field in ("name", "instruction", "source_request_id", "vehicle_id"):
|
||||
assert field in save_flow
|
||||
assert "response.ok" in save_flow
|
||||
assert "refreshModeProfiles?.(payload.mode_profile?.id)" in save_flow
|
||||
assert "appendModeCreatedState(message, payload.mode_profile)" in save_flow
|
||||
|
||||
assert "tryModeDraft?.(draft)" in assistant
|
||||
assert "appendModeCreatedState(message, event.payload.mode_profile)" in assistant
|
||||
assert "revealModeProfile?.(String(profile.id), { focus: true })" in assistant
|
||||
assert "function revealModeProfile(modeProfileId" in app
|
||||
assert "refreshModeProfiles" in app
|
||||
assert "tryModeDraft" in app
|
||||
assert 'button.classList.add("just-created")' in app
|
||||
assert 'window.addEventListener("cognitive-agent:request-complete"' in app
|
||||
assert ".assistant-mode-draft" in css
|
||||
assert ".assistant-mode-draft-row" in css
|
||||
assert ".assistant-mode-draft-save" in css
|
||||
assert ".assistant-mode-draft-status.failed" in css
|
||||
assert ".assistant-mode-created" in css
|
||||
|
||||
|
||||
def test_assistant_composer_grows_to_a_cap_then_scrolls_its_own_content():
|
||||
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
css = (ROOT / "apps/web-console/assistant-chat.css").read_text(encoding="utf-8")
|
||||
|
||||
resize_flow = assistant.split(
|
||||
"function resizeComposerInput", 1
|
||||
)[1].split("function createMessage", 1)[0]
|
||||
assert "const COMPOSER_MIN_HEIGHT = 64" in assistant
|
||||
assert "const COMPOSER_MAX_HEIGHT = 116" in assistant
|
||||
assert 'input.style.height = "auto"' in resize_flow
|
||||
assert "input.scrollHeight" in resize_flow
|
||||
assert "Math.min(contentHeight, COMPOSER_MAX_HEIGHT)" in resize_flow
|
||||
assert 'input.style.height = `${nextHeight}px`' in resize_flow
|
||||
assert 'input.classList.toggle("is-scrollable", scrollable)' in resize_flow
|
||||
assert "if (!scrollable) input.scrollTop = 0" in resize_flow
|
||||
assert 'input.addEventListener("input", () => resizeComposerInput(input))' in assistant
|
||||
assert "resizeComposerInput(input)" in assistant
|
||||
|
||||
composer_styles = css.split(
|
||||
".v2-request-form .command-composer", 1
|
||||
)[1].split("}", 1)[0]
|
||||
assert "height: auto" in composer_styles
|
||||
input_styles = css.split(
|
||||
".v2-request-form #utterance", 1
|
||||
)[1].split("}", 1)[0]
|
||||
assert "max-height: 116px" in input_styles
|
||||
assert "overflow-y: hidden" in input_styles
|
||||
scrollable_styles = css.split("#utterance.is-scrollable", 1)[1].split("}", 1)[0]
|
||||
assert "overflow-y: auto" in scrollable_styles
|
||||
assert "scrollbar-width: thin" in scrollable_styles
|
||||
assert "overscroll-behavior: contain" in scrollable_styles
|
||||
|
||||
|
||||
def test_assistant_composer_keeps_the_microphone_visible_and_aligned():
|
||||
html = (ROOT / "apps/web-console/index.html").read_text(encoding="utf-8")
|
||||
css = (ROOT / "apps/web-console/assistant-chat.css").read_text(encoding="utf-8")
|
||||
assert 'id="speechDictationButton"' in html
|
||||
assert 'data-lucide="mic"' in html
|
||||
assert ".v2-request-form .speech-dictation" in css
|
||||
assert "right: 53px" in css
|
||||
assert "bottom: auto" in css
|
||||
assert "display: grid !important" in css
|
||||
assert "transform: translateY(-50%)" in css
|
||||
assert ".v2-request-form .speech-status:empty" in css
|
||||
|
||||
|
||||
def test_voice_input_supports_direct_manual_capture_and_background_wake_standby():
|
||||
html = (ROOT / "apps/web-console/index.html").read_text(encoding="utf-8")
|
||||
script = (ROOT / "apps/web-console/speech-input.js").read_text(encoding="utf-8")
|
||||
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
|
||||
assert 'aria-label="开始语音输入"' in html
|
||||
assert 'const DEFAULT_WAKE_WORD = "你好,双十一"' in script
|
||||
assert 'new Set(["arming", "armed", "capturing", "finalizing"])' in script
|
||||
assert "async function startManualDictation()" in script
|
||||
assert "async function startWakeStandby({ automatic = true } = {})" in script
|
||||
assert 'return startCapture("manual")' in script
|
||||
assert 'return startCapture("wake", { automatic })' in script
|
||||
assert 'else void startManualDictation()' in script
|
||||
assert 'const manualInput = speech.inputMode === "manual"' in script
|
||||
assert "const detectedWake = manualInput || wakeWord.detected === true" in script
|
||||
assert 'fetch("/api/speech/preview"' in script
|
||||
assert '"X-Speech-Preview-Sequence"' in script
|
||||
assert '"X-Speech-Input-Mode": speech.inputMode' in script
|
||||
assert 'result.stale === true' in script
|
||||
assert 'sequence <= speech.lastAppliedPreviewSequence' in script
|
||||
assert "wakeWord.remainder ?? result.live_text" in script
|
||||
assert "status.wake_word.primary" in script
|
||||
assert 'if (speech.wakeDetected && liveText)' in script
|
||||
assert 'replaceInput(liveText, "partial")' in script
|
||||
assert '"X-Speech-Input-Mode": inputMode' in script
|
||||
assert '"X-Speech-Wake-Word-Required": inputMode === "wake" ? "1" : "0"' in script
|
||||
assert (
|
||||
"result.assistant_text || result.clean_text || result.live_text || "
|
||||
"result.raw_text || result.text"
|
||||
) in script
|
||||
assert "commands.length" not in script
|
||||
assert "input.dataset.speechExecutionEligible =" not in script
|
||||
assert "void speech.setAssistantOpen(open)" in assistant
|
||||
assert '["capturing", "finalizing"].includes(speechPhase)' in assistant
|
||||
assert 'speechPhase === "arming" && speechInputMode === "manual"' in assistant
|
||||
assert "请等待语音整理完成后再发送" in assistant
|
||||
|
||||
|
||||
def test_voice_lifecycle_releases_hidden_or_collapsed_microphone():
|
||||
script = (ROOT / "apps/web-console/speech-input.js").read_text(encoding="utf-8")
|
||||
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
|
||||
assert 'document.addEventListener("visibilitychange"' in script
|
||||
assert "ACTIVE_PHASES.has(speech.phase)" in script
|
||||
assert 'window.addEventListener("pagehide"' in script
|
||||
assert "speech.stream?.getTracks().forEach((track) => track.stop())" in script
|
||||
assert "speech.previewController?.abort" in script
|
||||
assert "speech.finalController?.abort" in script
|
||||
assert 'typeof speech?.setAssistantOpen === "function"' in assistant
|
||||
assert "void speech.setAssistantOpen(open)" in assistant
|
||||
|
||||
|
||||
def test_model_text_is_rendered_through_text_nodes_not_raw_html():
|
||||
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
assert "document.createTextNode(part)" in script
|
||||
assert "code.textContent =" in script
|
||||
assert "contentNode.textContent = content" in script
|
||||
assert "innerHTML = content" not in script
|
||||
|
||||
|
||||
def test_chat_styles_include_accessible_responsive_and_reduced_motion_states():
|
||||
css = (ROOT / "apps/web-console/assistant-chat.css").read_text(encoding="utf-8")
|
||||
assert ".assistant-message-user" in css
|
||||
assert ".assistant-action-status.success" in css
|
||||
assert "@media (max-width: 900px)" in css
|
||||
assert "@media (max-width: 560px)" in css
|
||||
assert ".v2-unified-right-rail" in css
|
||||
assert "grid-template-columns: minmax(0, 1fr) !important" in css
|
||||
assert "@media (prefers-reduced-motion: reduce)" in css
|
||||
|
||||
|
||||
def test_assistant_does_not_restore_collapsed_sidebars_and_uses_mobile_overlays():
|
||||
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
css = (ROOT / "apps/web-console/assistant-chat.css").read_text(encoding="utf-8")
|
||||
assert "--assistant-drawer-width: clamp(340px, 26vw, 410px);" in css
|
||||
assert "@media (min-width: 901px)" in css
|
||||
assert ".assistant-drawer-open.v2-sidebar-collapsed .workspace" in css
|
||||
assert "grid-template-columns: 0 minmax(0, 1fr);" in css
|
||||
assert ".assistant-drawer-open.v2-sidebar-collapsed .command-panel.v2-sidebar" in css
|
||||
assert "visibility: hidden;" in css
|
||||
assert "grid-template-columns: var(--assistant-drawer-width) minmax(0, 1fr);" in css
|
||||
assert "#v2PanelControlDock > .assistant-drawer-toggle" in css
|
||||
assert "opacity: 1 !important;" in css
|
||||
assert "pointer-events: auto !important;" in css
|
||||
assert "@media (max-width: 900px)" in css
|
||||
assert "width: min(86vw, 320px);" in css
|
||||
assert "width: min(86vw, 360px);" in css
|
||||
assert "top: 60px;" in css
|
||||
assert 'event.key === "Escape"' in script
|
||||
Reference in New Issue
Block a user