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-26"' in html assert 'src="/assistant-chat.js?v=codex-assistant-26"' 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-v277"' in service_worker assert '"/assistant-chat.css?v=codex-assistant-26"' in service_worker assert '"/assistant-chat.js?v=codex-assistant-26"' 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_unsupported_control_suggestions_are_clickable_accessible_and_deduplicated(): 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 'event.type === "control.suggestions"' in script assert "event.suggestions || event.recommended_commands" in script assert "function normalizeControlSuggestions(rawSuggestions)" in script assert "seenCommands.has(commandKey)" in script assert "MAX_CONTROL_SUGGESTIONS = 4" in script assert "function appendControlSuggestions(message, rawSuggestions)" in script assert 'message.article?.classList.add("assistant-message-control-suggestions")' in script assert 'group.setAttribute("role", "group")' in script assert 'group.setAttribute("aria-label", "可以尝试的其他控制指令")' in script assert 'button.type = "button"' in script assert 'button.setAttribute("aria-label", `执行控制指令:${suggestion.label}`)' in script assert "submitSuggestedCommand(suggestion.command, group, button)" in script assert "setSuggestionGroupPending(group, button, true)" in script assert "await submitAssistant(command)" in script assert ".assistant-control-suggestions" in css assert ".assistant-message-control-suggestions .assistant-message-body" in css assert ".assistant-control-suggestion:focus-visible" in css assert ".assistant-control-suggestion.is-sending" in css 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 'const existingModeId = String(draft.existing_mode_id || "").trim()' in save_flow assert "encodeURIComponent(existingModeId)" in save_flow assert '"/api/mode-profiles"' in save_flow assert "request(endpoint" 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 "updated: Boolean(existingModeId)" in save_flow assert "确认更新此模式" in assistant assert "已载入现有功能模式" in assistant for action_label in ( "座椅前后位置", "坐垫角度", "靠背角度", "腿托高度", "空调循环", "内循环", ): assert action_label in assistant assert "tryModeDraft?.({ ...draft, actions })" in assistant assert '["mode_profile_create", "mode_profile_update"].includes(' in assistant assert "appendModeCreatedState(message, event.payload.mode_profile, {" in assistant assert ( 'updated: event.payload.orchestration_route === "mode_profile_update"' in assistant ) assert "refreshModeProfiles?.(" 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 "const actionSignature = (action) =>" in app assert 'apiRequest("/api/mode-profile-drafts/trial"' in app assert "source_request_id: draft.source_request_id" in app assert "const expectedSignatures = Array.isArray(draft?.actions)" in app assert "signature === actualSignatures[index]" in app assert "|| !sameActions" 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_carries_only_the_active_mode_draft_reference_to_follow_up_turns(): assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8") request_body = assistant.split("function requestBody(message)", 1)[1].split( "async function readErrorResponse", 1 )[0] context_flow = assistant.split("function sanitizeModeDraftContext", 1)[1].split( "function loadStoredMessages", 1 )[0] assert ( 'const ACTIVE_MODE_DRAFT_STORAGE_KEY = "cognitive-agent.active-mode-draft"' in assistant ) assert "mode_profile_draft_context: modeProfileDraftContext" in request_body assert "requestModeDraftContext(message)" in request_body assert "name: context.name" in context_flow assert "source_request_id: context.source_request_id" in context_flow assert "vehicle_id: context.vehicle_id" in context_flow assert "actions" not in context_flow assert "rememberModeDraftContext(draft)" in assistant assert "activeModeDraftContext = loadActiveModeDraftContext()" in assistant assert "function isModeDraftUpdateRequest(message, context)" in assistant assert "normalizeModeName(matched[1]) === normalizeModeName(context.name)" in assistant def test_latest_same_name_mode_draft_retires_stale_card_and_lifecycle_clears_context(): 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") replace_flow = assistant.split("function supersedeModeDraftCards", 1)[1].split( "function appendModeDraftState", 1 )[0] save_flow = assistant.split("async function saveModeProfileDraft", 1)[1].split( "async function tryModeProfileDraft", 1 )[0] new_conversation_flow = assistant.split( 'if (!target?.closest("#newConversation")) return;', 1 )[1].split("}, true);", 1)[0] assert "card.dataset.assistantModeName !== nextName" in replace_flow assert "card.dataset.assistantModeDraft === nextRequestId" in replace_flow assert 'card.classList.add("is-superseded")' in replace_flow assert "button.disabled = true" in replace_flow assert "已由下方最新草稿替换" in replace_flow assert "clearActiveModeDraftContext(draft.source_request_id)" in save_flow assert "clearActiveModeDraftContext()" in new_conversation_flow assert ".assistant-mode-draft.is-superseded" in css assert ".assistant-mode-draft-status.superseded" 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