Files
Cognitive_Agent/tests/unit/test_assistant_frontend_contract.py
T

404 lines
21 KiB
Python

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-28"' in html
assert 'src="/assistant-chat.js?v=codex-assistant-32"' 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-v298"' in service_worker
assert '"/assistant-chat.css?v=codex-assistant-28"' in service_worker
assert '"/assistant-chat.js?v=codex-assistant-32"' 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 '"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_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