519 lines
26 KiB
Python
519 lines
26 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
WEB = ROOT / "apps" / "web-console"
|
|
|
|
|
|
def vue_shell() -> str:
|
|
return (WEB / "src" / "App.vue").read_text(encoding="utf-8")
|
|
|
|
|
|
def test_console_contains_ai_assistant_surface():
|
|
html = (WEB / "index.html").read_text(encoding="utf-8")
|
|
template = vue_shell()
|
|
vite_entry = (WEB / "src" / "main.js").read_text(encoding="utf-8")
|
|
assert 'id="assistantTranscript"' in template
|
|
assert 'id="assistantMessages"' in template
|
|
assert "你好,我是 AI助手。你可以向我提问" in template
|
|
assert 'aria-label="AI助手对话"' in template
|
|
assert 'placeholder="向 AI助手提问或交代工作"' in template
|
|
assert 'aria-label="发送消息"' in template
|
|
assert 'import "../assistant-chat.css";' in vite_entry
|
|
assert 'src="/assistant-chat.js?v=codex-assistant-34"' 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-v321-n7-dispatch"'
|
|
in service_worker
|
|
)
|
|
assert '"/assistant-chat.css?v=codex-assistant-29"' in service_worker
|
|
assert '"/assistant-chat.js?v=codex-assistant-34"' 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 'event.type === "validated_control_commands"' in script
|
|
assert '"cognitive-agent:validated-can-commands"' in script
|
|
assert '"cognitive-agent:control-request-start"' in script
|
|
assert "speech_utterance_id:" in script
|
|
assert "speech_value:" 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_validated_controls_are_mirrored_into_a_compact_assistant_card():
|
|
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 "function appendDispatchedControls(message, result = {})" in script
|
|
assert "window.CognitiveAgentConsoleV2?.getParsedCanCommands?.(result)" in script
|
|
assert "fallbackAssistantControlCommands(result)" in script
|
|
assert 'card.className = "assistant-dispatched-controls"' in script
|
|
assert 'heading.textContent = "下发控制指令"' in script
|
|
assert 'count.className = "assistant-dispatched-controls-count"' in script
|
|
assert 'list.className = "assistant-dispatched-controls-list"' in script
|
|
assert "value.textContent = assistantControlValue(item)" in script
|
|
assert "语义控制指令:" in script
|
|
assert "实际控制指令:" in script
|
|
assert "appendDispatchedControls(message, event.payload)" in script
|
|
assert "function announceModeExecution(" in script
|
|
assert "announceModeExecution," in script
|
|
assert "function beginModeExecution(" in script
|
|
assert "function completeModeExecution(" in script
|
|
assert "appendDispatchedControls(message, modeExecutionPreviewResult(profile))" in script
|
|
assert "beginModeExecution," in script
|
|
assert "completeModeExecution," in script
|
|
assert "const operationByRef = new Map()" in script
|
|
assert 'deliveryState: "submitting"' in script
|
|
assert 'deliveryLabel: "正在下发"' in script
|
|
assert '"seat.set_cushion_tilt": "坐盆"' in script
|
|
assert '"seat.set_backrest_recline": "靠背"' in script
|
|
assert '"seat.set_legrest_height": "腿托"' in script
|
|
assert '"seat.set_legrest_extension": "腿托"' in script
|
|
assert '"ambient_light.set_illumination": "氛围灯颜色与亮度"' in script
|
|
assert '.replaceAll("_", "-")' in script
|
|
assert ".assistant-dispatched-controls > summary" in css
|
|
assert ".assistant-dispatched-controls-list > li" in css
|
|
assert ".assistant-dispatched-control-value" in css
|
|
assert ".assistant-dispatched-controls[open]" in css
|
|
|
|
|
|
def test_explicit_nfc_authorization_block_is_rendered_as_safety_blocked():
|
|
console = (ROOT / "apps/web-console/console-v2.js").read_text(encoding="utf-8")
|
|
|
|
assert 'executionStatus === "blocked"' in console
|
|
assert "Boolean(telemetry.authorization_reason)" in console
|
|
assert 'deliveryState = aborted' in console
|
|
assert '? "blocked"' in console
|
|
assert '? "安全阻止"' in console
|
|
|
|
|
|
def test_latest_dispatched_control_group_is_the_only_group_expanded():
|
|
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
vue_ui = (ROOT / "apps/web-console/vue-ui.js").read_text(encoding="utf-8")
|
|
|
|
assert "function focusLatestDispatchedControls(container)" in assistant
|
|
assert 'transcript.querySelectorAll(".assistant-dispatched-controls")' in assistant
|
|
assert "card.open = card === activeCard" in assistant
|
|
assert "focusLatestDispatchedControls(host)" in assistant
|
|
assert "focusLatestDispatchedControls(card)" in assistant
|
|
assert 'querySelectorAll(".assistant-dispatched-controls[open]")' in assistant
|
|
assert 'querySelectorAll(".assistant-dispatched-controls[open]")' in vue_ui
|
|
assert "if (card !== event.currentTarget) card.open = false" in vue_ui
|
|
|
|
|
|
def test_saved_mode_execution_opens_with_pending_flow_before_http_completion():
|
|
app = (ROOT / "apps/web-console/app.js").read_text(encoding="utf-8")
|
|
flow = app.split(
|
|
"async function executeSavedModeProfile", 1
|
|
)[1].split("function applyModeCommand", 1)[0]
|
|
|
|
begin_index = flow.index("assistant?.beginModeExecution?.(mode")
|
|
request_index = flow.index("await postControlPhase(")
|
|
assert begin_index < request_index
|
|
assert "assistant.completeModeExecution(assistantModeHandle, mode, payload" in flow
|
|
assert "assistant.completeModeExecution(assistantModeHandle, mode, null" in flow
|
|
|
|
|
|
def test_seat_control_cards_name_the_controlled_seat_component():
|
|
console = (ROOT / "apps/web-console/console-v2.js").read_text(encoding="utf-8")
|
|
|
|
assert '"seat.set_cushion_tilt": ["副驾座椅坐盆调节"' in console
|
|
assert 'backrest: "主驾座椅靠背调节"' in console
|
|
assert 'basin: "主驾座椅坐盆调节"' in console
|
|
assert 'legrest: "主驾座椅腿托调节"' in console
|
|
assert 'horizontal: "主驾座椅前后调节"' in console
|
|
|
|
|
|
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 "const nextConversationId = createConversationId()" in script
|
|
assert (
|
|
"sessionStorage.setItem(CONVERSATION_STORAGE_KEY, nextConversationId)"
|
|
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 'const panelDock = $("#v2PanelControlDock")' in script
|
|
assert "panelDock.insertBefore(trigger, firstRightPanelToggle)" in script
|
|
assert "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(
|
|
"function resetConversation()", 1
|
|
)[1].split("function initialize()", 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_new_conversation_resets_chat_in_place_and_keeps_panel_order_spatial():
|
|
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"
|
|
)
|
|
reset_flow = assistant.split("function resetConversation()", 1)[1].split(
|
|
"function initialize()", 1
|
|
)[0]
|
|
app_flow = app.split("function startNewConversation", 1)[1].split(
|
|
"window.CognitiveAgentConsole", 1
|
|
)[0]
|
|
|
|
assert 'activeController.abort("new_conversation")' in reset_flow
|
|
assert "sessionStorage.removeItem(STORAGE_KEY)" in reset_flow
|
|
assert "renderInitialMessages()" in reset_flow
|
|
assert "setComposerPending(false)" in reset_flow
|
|
assert '"cognitive-agent:new-conversation"' in reset_flow
|
|
assert "window.location.reload()" not in app_flow
|
|
assert (
|
|
'window.addEventListener("cognitive-agent:new-conversation", '
|
|
"startNewConversation)"
|
|
) in app
|
|
assert 'const firstRightPanelToggle = $("[data-panel-toggle]", panelDock)' in assistant
|
|
assert "panelDock.insertBefore(trigger, firstRightPanelToggle)" in assistant
|
|
|
|
|
|
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 = vue_shell()
|
|
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 = vue_shell()
|
|
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_dispatched_control_card_spans_the_full_assistant_message_row():
|
|
css = (ROOT / "apps/web-console/assistant-chat.css").read_text(encoding="utf-8")
|
|
layout = css.split("/* Compact command receipt", 1)[1].split(
|
|
"/* The function-mode strip", 1
|
|
)[0]
|
|
|
|
assert "grid-template-columns: 30px minmax(0, 1fr)" in layout
|
|
assert "display: contents" in layout
|
|
assert "grid-column: 1 / -1" in layout
|
|
assert "max-width: none" in layout
|
|
|
|
|
|
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
|