c812a1a595
Integrate the remote firmware workbench, OTA delivery, and timed mode schedules while preserving the local N7 scene and runtime changes.
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-v322-n7-firmware-merge"'
|
|
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[nameIndex]) === 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
|