from pathlib import Path from fastapi.testclient import TestClient from gateway.app import app def test_operator_ui_route_exists(): client = TestClient(app) response = client.get("/operator") assert response.status_code == 200 assert 'id="profileName"' in response.text assert 'id="profileRole"' in response.text assert 'id="browserPhoneCallOverlay"' in response.text def test_operator_ui_config_disables_whatsapp_by_default(): client = TestClient(app) response = client.get("/operator/config") assert response.status_code == 200 assert response.json()["features"]["whatsapp"] is False def test_login_ui_route_exists(): client = TestClient(app) response = client.get("/") assert response.status_code == 200 assert "login/assets/app.js" in response.text def test_sales_ui_route_exists(): client = TestClient(app) response = client.get("/sales") assert response.status_code == 200 assert "sales/assets/app.js" in response.text assert 'id="sendTelegramReplyBtn"' in response.text def test_legal_routes_exist_for_meta_review(): client = TestClient(app) privacy = client.get("/privacy") terms = client.get("/terms") deletion = client.get("/data-deletion") assert privacy.status_code == 200 assert "KonturAI Privacy Policy" in privacy.text assert terms.status_code == 200 assert "KonturAI Terms of Service" in terms.text assert deletion.status_code == 200 assert "Data Deletion" in deletion.text def test_contracts_shape(): client = TestClient(app) response = client.get("/contracts") assert response.status_code == 200 payload = response.json() assert "stage_1" in payload assert "stage_2" in payload assert "stage_3" in payload assert "stage_4" in payload assert "stage_5" in payload assert "stage_6" in payload assert "stage_11" in payload assert "stage_12" in payload assert "stage_14" in payload assert "stage_15" in payload assert "stage_16" in payload assert "stage_17" in payload assert "stage_18" in payload assert "/integrations/webchat/messages" in payload["stage_2"] assert "/integrations/email/messages" in payload["stage_2"] assert "/integrations/whatsapp/webhook" in payload["stage_2"] assert "/recordings/*" in payload["stage_4"] assert "/ivr/flows" in payload["stage_5"] assert "/ivr/sessions" in payload["stage_5"] assert "/reports/coverage" in payload["stage_6"] assert "/reports/agents/overview" in payload["stage_6"] assert "/reports/agents/timeseries" in payload["stage_6"] assert "/reports/drilldown" in payload["stage_6"] assert "/reports/export" in payload["stage_6"] assert "/reports/facts/interactions" in payload["stage_6"] assert "/reports/kpi" in payload["stage_6"] assert "/reports/timeseries" in payload["stage_6"] assert "/reports/views" in payload["stage_6"] assert "/reports/views/*" in payload["stage_6"] assert "/asterisk/live-calls" in payload["stage_11"] assert "/asterisk/recent-calls" in payload["stage_12"] assert "/asterisk/browser-softphone/config" in payload["stage_14"] assert "/integrations/telegram/threads" in payload["stage_15"] assert "/integrations/whatsapp/threads" in payload["stage_15"] assert "/ai/whatsapp/threads/*" in payload["stage_16"] assert "/ai/analytics/overview" in payload["stage_16"] assert "/ai/analytics/timeseries" in payload["stage_16"] assert "/ai/analytics/voice-name-flow/overview" in payload["stage_16"] assert "/ai/analytics/voice-name-flow/timeseries" in payload["stage_16"] assert "/ai/analytics/drilldown" in payload["stage_16"] assert "/ai/analytics/sessions/*" in payload["stage_16"] assert "/asterisk/live-calls/*/ai-summary" in payload["stage_17"] assert "/api/v1/leads" in payload["stage_18"] assert "/api/v1/deals" in payload["stage_18"] assert "/api/v1/messages/inbound-webhook" in payload["stage_18"] assert "/api/v1/calls/inbound-webhook" in payload["stage_18"] def test_operator_ui_includes_favicon_link(): client = TestClient(app) response = client.get("/operator") assert response.status_code == 200 assert "/operator/assets/favicon.svg" in response.text def test_operator_app_syncs_role_after_login(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") assert "state.role = data.role;" in app_js assert "$('sessionRole').value = data.role;" in app_js def test_operator_app_defaults_to_admin_and_syncs_from_form(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert "role: 'admin'" in app_js assert "function syncSessionFromInputs()" in app_js assert "syncSessionFromInputs();" in app_js assert '' in index_html def test_operator_app_demo_polish_no_prompt_and_has_summaries(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert "prompt(" not in app_js assert "function renderSupervisorSummary" in app_js assert "function renderKpiSummary" in app_js assert 'id="supervisorSummary"' not in index_html assert 'id="kpiSummary"' not in index_html assert "Сценарий демонстрации" not in index_html def test_operator_ui_contains_corporate_login_path(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert "function startCorporateLogin()" in app_js assert "headers.Authorization = `Bearer ${state.token}`;" in app_js assert "window.addEventListener('message', handleOidcMessage);" in app_js assert 'id="corporateLoginBtn"' in index_html def test_operator_ui_keeps_webchat_channel_option_without_demo_block(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert '' in index_html assert 'id="sendWebchatMessageBtn"' not in index_html assert "function sendWebchatMessage()" not in app_js def test_operator_ui_contains_unified_inbox_panel(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert 'id="unifiedInbox"' in index_html assert 'id="refreshUnifiedInboxBtn"' in index_html assert 'id="unifiedInboxSummary"' in index_html assert 'id="unifiedInboxBoard"' in index_html assert "const UNIFIED_INBOX_COLUMNS = [" in app_js assert "function buildUnifiedInboxCollections()" in app_js assert "function renderUnifiedInbox()" in app_js assert "function focusLiveCall(callId)" in app_js assert "function openUnifiedInboxItem(button)" in app_js assert "function handleUnifiedInboxClick(event)" in app_js assert "function refreshUnifiedInbox()" in app_js assert "window.location.hash = '#messages';" in app_js assert "await selectMessengerConversation(messengerConversationId('telegram', threadId));" in app_js assert "window.location.hash = '#calls';" in app_js assert "window.location.hash = '#interactions';" in app_js def test_operator_ui_loads_backend_customer_history_for_profile(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") assert "historyById: {}" in app_js assert "pendingHistoryById: {}" in app_js assert "function customerHistoryPayload(customerId)" in app_js assert "function ensureCustomerHistoryLoaded(customerId, options = {})" in app_js assert "api('customer', `customers/${encodeURIComponent(customerId)}/history`)" in app_js assert "const historyPayload = customerHistoryPayload(customer.customer_id);" in app_js assert "ensureCustomerHistoryLoaded(customer.customer_id).catch(() => {});" in app_js def test_operator_ui_customer_profile_supports_case_and_call_shortcuts(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") styles_css = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "styles.css").read_text(encoding="utf-8") assert "function customerHistoryEventActions(event, context = {})" in app_js assert "function renderCustomerHistoryActions(event, context = {})" in app_js assert 'data-customer-action="interaction"' in app_js assert 'data-customer-action="call"' in app_js assert "Открыть кейс" in app_js assert "Открыть звонок" in app_js assert "focusLiveCall(callId);" in app_js assert ".customer-history-actions {" in styles_css assert ".customer-history-link {" in styles_css def test_operator_ui_contains_leads_table_front(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert 'id="customersView"' in index_html assert 'data-operator-view="customers"' in index_html assert 'data-operator-view-link="customers"' in index_html assert 'href="#customers-page"' in index_html assert 'id="toggleCustomerLeadFormBtn"' in index_html assert 'id="exportCustomersBtn"' in index_html assert 'id="customerPageSummary"' in index_html assert 'id="customerPrevPageBtn"' in index_html assert 'id="customerNextPageBtn"' in index_html assert "function renderCustomerList" in app_js assert "function exportCustomersCsv" in app_js assert "function toggleCustomerLeadForm" in app_js assert "function customerLeadSourceMeta" in app_js assert "function applyOperatorViewFromHash" in app_js assert "window.addEventListener('hashchange', handleOperatorHashChange);" in app_js def test_operator_ui_contains_telegram_workspace_page(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert 'id="telegramView"' in index_html assert 'data-operator-view="telegram"' in index_html assert 'data-operator-view-link="telegram"' not in index_html assert 'data-debug-channel="telegram"' in index_html assert 'href="#telegram-page"' in index_html assert 'id="loadTelegramThreadsBtn"' in index_html assert 'id="telegramThreadsList"' in index_html assert 'id="telegramMessagesTimeline"' in index_html assert 'id="telegramHeaderSearchBtn"' in index_html assert 'id="telegramMessageSearchBar"' in index_html assert 'id="telegramMessageSearch"' in index_html assert 'id="telegramMessageSearchMeta"' in index_html assert 'id="telegramMessageSearchClearBtn"' in index_html assert 'id="telegramReplyText"' in index_html assert 'id="telegramSendReplyBtn"' in index_html assert 'id="telegramClaimThreadBtn"' in index_html assert 'id="telegramCloseThreadBtn"' in index_html assert 'id="telegramEscalateThreadBtn"' in index_html assert "api('telegram', 'integrations/telegram/threads')" in app_js assert "integrations/telegram/threads/${encodeURIComponent(thread.thread_id)}/messages" in app_js assert "function renderTelegramWorkspace" in app_js assert "function toggleTelegramMessageSearch" in app_js assert "function telegramVisibleMessages" in app_js assert "function highlightTelegramMessageText" in app_js assert "function claimTelegramThread" in app_js assert "function sendTelegramReply" in app_js def test_operator_ui_contains_unified_messenger_mvp(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") styles_css = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "styles.css").read_text(encoding="utf-8") assert 'data-operator-view-link="messages"' in index_html assert 'href="#messages"' in index_html assert 'id="messagesView"' in index_html assert 'data-operator-view="messages"' in index_html assert 'id="messengerConversationList"' in index_html assert 'id="messengerMessagesTimeline"' in index_html assert 'id="messengerReplyText"' in index_html assert 'id="messengerSendReplyBtn"' in index_html assert 'id="messengerClaimBtn"' in index_html assert 'id="messengerCloseBtn"' in index_html assert 'id="messengerOpenProfileBtn"' in index_html assert 'id="messengerContextAiSummary"' in index_html assert 'Telegram' in index_html assert "messenger: {" in app_js assert "function messengerConversationFromTelegramThread(thread)" in app_js assert "function buildMessengerConversations()" in app_js assert "function renderMessengerWorkspace()" in app_js assert "function claimMessengerConversation()" in app_js assert "function sendMessengerReply()" in app_js assert "function closeMessengerConversation()" in app_js assert "api('telegram', 'integrations/telegram/threads')" in app_js assert "integrations/telegram/threads/${encodeURIComponent(conversation.thread_id)}/messages" in app_js assert "integrations/telegram/threads/${encodeURIComponent(conversation.thread_id)}/claim" in app_js assert "integrations/telegram/threads/${encodeURIComponent(conversation.thread_id)}/close" in app_js assert "setInterval(() => {" in app_js assert "}, 5000);" in app_js assert "[hidden] {" in styles_css assert "display: none !important;" in styles_css assert ".telegram-composer-shell textarea {" in styles_css assert ".messenger-composer-shell textarea {" in styles_css assert "align-content: start;" in styles_css def test_operator_ui_contains_live_call_control_block(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert 'id="liveCalls"' in index_html assert 'id="callsView"' in index_html assert 'data-operator-view="calls"' in index_html assert 'data-operator-view-link="calls"' in index_html assert 'href="#calls"' in index_html assert 'data-operator-view="voice-debug"' not in index_html assert 'data-operator-view-link="voice-debug"' not in index_html assert 'href="#voice-debug"' not in index_html assert 'id="loadLiveCallsBtn"' in index_html assert 'id="loadLiveCallActionsBtn"' in index_html assert 'id="liveCallIdSelect"' in index_html assert 'id="claimLiveCallBtn"' not in index_html assert 'id="hangupLiveCallBtn"' not in index_html assert 'id="blindTransferLiveCallBtn"' not in index_html assert "api('asterisk-bridge', 'asterisk/live-calls')" in app_js assert "api('asterisk-bridge', 'asterisk/recent-calls')" in app_js assert "asterisk/live-calls/${encodeURIComponent(callId)}/claim" in app_js assert "asterisk/live-calls/${encodeURIComponent(callId)}/hangup" in app_js assert "blind-transfer" in app_js assert "Принять в работу" in index_html assert "Только что завершённые" in app_js assert "function terminalActionLabel" in app_js assert "function fallbackTelephonyStatus" not in app_js assert "raw === 'calls' || raw === 'calls-page' || raw === 'voice-debug' || raw === 'voice-debug-page'" in app_js assert "window.history.replaceState(null, '', '#calls');" in app_js assert 'data-operator-anchor-link="liveCalls"' not in index_html assert 'data-operator-anchor-link="interactions"' not in index_html assert 'data-operator-anchor-link="integrations"' not in index_html def test_operator_ui_optimistically_updates_live_call_state_after_actions(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") assert "function applyClaimedLiveCall" in app_js assert "function moveLiveCallToRecent" in app_js assert "function refreshLiveCallsInBackground" in app_js assert "loadLiveCalls(false, { preserveStateOnError: true });" in app_js def test_operator_ui_contains_browser_softphone_popup_controls(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert 'id="browserSoftphone"' not in index_html assert 'id="browserPhoneStatusBtn"' in index_html assert 'id="browserPhoneSettingsPanel"' in index_html assert 'id="browserPhoneCallOverlay"' in index_html assert 'id="browserPhoneAiSummary"' in index_html assert 'id="browserPhoneConnectBtn"' in index_html assert 'id="browserPhoneAnswerBtn"' in index_html assert 'id="browserPhoneCallClaimBtn"' in index_html assert 'id="browserPhoneCallTransferBtn"' in index_html assert 'id="browserPhoneCallHangupBtn"' in index_html assert '/operator/assets/sip-0.21.2.min.js' in index_html assert "asterisk/browser-softphone/config" in app_js assert "function connectBrowserSoftphone" in app_js assert "function answerBrowserSoftphoneCall" in app_js assert "function toggleBrowserPhoneSettings" in app_js assert "function hangupBrowserPopupCall" in app_js assert "function transferBrowserPopupCall" in app_js assert "function loadVoiceAiSummary" in app_js assert "function renderVoiceAiSummary" in app_js assert "function startBrowserPhoneRingtone" in app_js assert "function stopBrowserPhoneRingtone" in app_js def test_operator_ui_can_fix_customer_name_from_call_views(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") styles_css = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "styles.css").read_text(encoding="utf-8") assert 'id="liveCallNameEditor"' in index_html assert 'id="liveCallNameInput"' in index_html assert 'id="liveCallNameSaveBtn"' in index_html assert 'id="browserPhoneNameEditor"' in index_html assert 'id="browserPhoneNameInput"' in index_html assert 'id="browserPhoneNameSaveBtn"' in index_html assert 'id="browserPhoneEditNameBtn"' in index_html assert "function openLiveCallNameEditor" in app_js assert "function saveLiveCallCustomerName" in app_js assert "function saveCustomerProfileName" in app_js assert "function openCustomerProfileNameEditor" in app_js assert "function customerProfileVoiceNameContext" in app_js assert "data-customer-name-form" in app_js assert 'data-customer-action="edit-name"' in app_js assert "customer-profile-name-meta" in app_js assert "customer-profile-name-flash" in app_js assert "function applyCustomerNamePatchLocally" in app_js assert "function updateLiveCallNameEditorsUi" in app_js assert "function handleLiveCallTableClick" in app_js assert "api('customer', `customers/${encodeURIComponent(customerId)}`" in app_js assert "customer_name_source: 'manual'" in app_js assert ".live-call-name-editor {" in styles_css assert ".call-window-name-editor {" in styles_css assert ".live-call-card-actions {" in styles_css assert ".customer-profile-name-editor {" in styles_css assert ".customer-profile-name-row {" in styles_css assert ".customer-profile-name-meta {" in styles_css assert ".customer-profile-name-note {" in styles_css assert ".customer-profile-name-flash {" in styles_css def test_operator_ui_surfaces_voice_name_state_across_call_views(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") styles_css = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "styles.css").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert "function voiceCustomerNameStatusMeta" in app_js assert "function formatVoiceCustomerNameSource" in app_js assert "function formatVoiceStartLanguage" in app_js assert "function voiceCustomerDisplayName" in app_js assert "function voiceCustomerIncomingMeta" in app_js assert "function refreshVoiceSummaryDependentViews" in app_js assert "Источник имени" in app_js assert "Язык старта" in app_js assert "Имя подтверждено" in app_js assert "Имя нужно уточнить" in app_js assert "Без подтверждения" in app_js assert ".micro-badge.name-confirmed {" in styles_css assert ".micro-badge.name-followup {" in styles_css assert ".micro-badge.name-missing {" in styles_css assert "/operator/assets/styles.css?v=track25-messenger-polish1" in index_html assert "/operator/assets/app.js?v=track44-messenger-polish1" in index_html def test_operator_ui_hides_whatsapp_behind_feature_flag(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert 'data-debug-channel="whatsapp"' in index_html assert 'data-operator-view-link="whatsapp"' not in index_html assert 'data-feature-option="whatsapp"' in index_html assert 'data-operator-view="whatsapp" data-feature="whatsapp" hidden' in index_html assert "function loadOperatorConfig()" in app_js assert "const response = await fetch('/operator/config');" in app_js assert "state.features.whatsapp = Boolean(data?.features?.whatsapp);" in app_js assert "document.querySelectorAll('[data-feature=\"whatsapp\"]')" in app_js assert "document.querySelectorAll('[data-feature-option=\"whatsapp\"]')" in app_js assert "if (featureEnabled('whatsapp')) {" in app_js def test_operator_ui_browser_softphone_has_fast_answer_and_hangup_states(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") assert "function browserPhonePopupPhase" in app_js assert "function browserPhoneAutoClaimSnapshot" in app_js assert "function ensureBrowserPhoneLocalStream" in app_js assert "function prepareBrowserPhoneAnswerMedia" in app_js assert "state.browserPhone.callPhase = 'connecting';" in app_js assert "state.browserPhone.callPhase = 'ending';" in app_js assert "state.browserPhone.callPhase = 'error';" in app_js assert "state.browserPhone.autoClaimInFlight" in app_js assert "localMediaStream" in app_js assert "await terminateBrowserPhoneSessionLocally();" in app_js assert "bestEffortBrowserAutoClaimWithOptions" in app_js def test_operator_ui_keeps_email_channel_option_without_demo_block(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "operator" / "index.html").read_text(encoding="utf-8") assert '' in index_html assert 'id="sendEmailMessageBtn"' not in index_html assert "function sendEmailMessage()" not in app_js def test_supervisor_ui_route_exists(): client = TestClient(app) response = client.get("/supervisor") assert response.status_code == 200 assert 'id="profileName"' in response.text assert 'id="profileRole"' in response.text assert "supervisor/assets/app.js" in response.text def test_analyst_ui_route_exists(): client = TestClient(app) response = client.get("/analyst") assert response.status_code == 200 assert 'id="profileName"' in response.text assert 'id="profileRole"' in response.text assert "analyst/assets/app.js" in response.text def test_admin_ui_route_exists(): client = TestClient(app) response = client.get("/admin") assert response.status_code == 200 assert 'id="profileName"' in response.text assert 'id="profileRole"' in response.text assert "admin/assets/app.js" in response.text def test_dashboard_route_points_to_sales_ui(): client = TestClient(app) response = client.get("/dashboard") assert response.status_code == 200 assert "КонтурCC Продажи" not in response.text assert "KonturCC Продажи" in response.text assert "sales/assets/app.js" in response.text assert 'id="salesDashboard"' in response.text def test_supervisor_ui_uses_realtime_and_kpi_endpoints(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "supervisor" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "supervisor" / "index.html").read_text(encoding="utf-8") assert "api('supervisor', 'supervisor/realtime')" in app_js assert "api('reporting', `reports/kpi?queue_id=${queueId}`)" in app_js assert "api('recording', `recordings${suffix}`)" in app_js assert "api('recording', `recordings/import-from-voice-event/" in app_js assert "fetchBinary('recording'" in app_js assert 'id="loadRealtimeBtn"' in index_html assert 'id="saveQueueMetricsBtn"' in index_html assert 'id="recordings"' in index_html assert 'id="importRecordingBtn"' in index_html assert 'id="loadRecordingsBtn"' in index_html assert 'id="recordingPlayer"' in index_html def test_supervisor_ui_no_longer_renders_analytics_dashboard(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "supervisor" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "supervisor" / "index.html").read_text(encoding="utf-8") assert 'id="analyticsPanel"' not in index_html assert 'id="analyticsPreset"' not in index_html assert 'id="analyticsRefreshBtn"' not in index_html assert 'id="analyticsTrendChart"' not in index_html assert 'id="analyticsQueueTable"' not in index_html assert 'id="analyticsCoverage"' not in index_html assert '"#analyticsPanel"' not in index_html assert "$('analyticsRefreshBtn').addEventListener" not in app_js assert "$('analyticsResetBtn').addEventListener" not in app_js assert "$('analyticsTrendMetric').addEventListener" not in app_js assert "$('analyticsQueueTable').addEventListener" not in app_js assert "loadAnalyticsDashboard(true)" not in app_js def test_analyst_ui_contains_analytics_dashboard_contract(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "analyst" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "analyst" / "index.html").read_text(encoding="utf-8") mock_json = (Path(__file__).resolve().parents[1] / "ui" / "analyst" / "mock-analytics.json") assert 'id="analyticsPanel"' in index_html assert 'id="analyticsPreset"' in index_html assert 'id="analyticsFrom"' in index_html assert 'id="analyticsTo"' in index_html assert 'id="analyticsQueue"' in index_html assert 'id="analyticsChannel"' in index_html assert 'id="analyticsRefreshBtn"' in index_html assert 'id="analyticsResetBtn"' in index_html assert 'id="analyticsOverview"' in index_html assert 'id="voiceNameAnalyticsPanel"' in index_html assert 'id="voiceNameAnalyticsOverview"' in index_html assert 'id="voiceNameAnalyticsTrendMetric"' in index_html assert 'id="voiceNameAnalyticsTrendChart"' in index_html assert 'id="voiceNameAnalyticsFunnel"' in index_html assert 'id="voiceNameAnalyticsLanguageTable"' in index_html assert 'id="voiceNameAnalyticsQueueTable"' in index_html assert 'id="voiceNameAnalyticsHandoffTable"' in index_html assert 'id="voiceNameAnalyticsEmptyState"' in index_html assert 'id="analyticsTrendMetric"' in index_html assert 'id="analyticsTrendChart"' in index_html assert 'id="analyticsChannelTable"' in index_html assert 'id="analyticsQueueTable"' in index_html assert 'id="analyticsCoverage"' not in index_html assert 'id="analyticsEmptyState"' in index_html assert 'id="aiAnalyticsPanel"' in index_html assert 'id="aiAnalyticsOverview"' in index_html assert 'id="aiAnalyticsTrendMetric"' in index_html assert 'id="aiAnalyticsTrendChart"' in index_html assert 'id="aiAnalyticsChannelComparison"' in index_html assert 'id="aiAnalyticsCoverage"' in index_html assert 'id="aiAnalyticsEmptyState"' in index_html assert 'id="aiAnalyticsOutcomeTable"' in index_html assert 'id="aiAnalyticsReasonTable"' in index_html assert 'id="agentAnalyticsPanel"' in index_html assert 'id="agentAnalyticsOverview"' in index_html assert 'id="agentAnalyticsStateStrip"' in index_html assert 'id="agentAnalyticsTrendMetric"' in index_html assert 'id="agentAnalyticsTrendChart"' in index_html assert 'id="agentAnalyticsTeamTable"' in index_html assert 'id="agentAnalyticsShiftTable"' in index_html assert 'id="agentAnalyticsTable"' in index_html assert 'id="agentAnalyticsEmptyState"' in index_html assert 'id="analyticsNarrative"' in index_html assert 'id="analyticsCompareMode"' in index_html assert 'id="analyticsSavedViewSelect"' in index_html assert 'id="analyticsSavedViewName"' in index_html assert 'id="analyticsSaveViewBtn"' in index_html assert 'id="analyticsDeleteViewBtn"' in index_html assert 'id="analyticsExportBtn"' in index_html assert 'id="analyticsComparePanel"' in index_html assert 'id="analyticsDrilldownDrawer"' in index_html assert 'id="analyticsDrilldownCloseBtn"' in index_html assert 'id="analyticsDrilldownFilters"' in index_html assert 'id="analyticsDrilldownList"' in index_html assert 'id="analyticsDrilldownDetail"' in index_html assert 'id="analyticsDrilldownPrevBtn"' in index_html assert 'id="analyticsDrilldownNextBtn"' in index_html assert 'id="analyticsDrilldownPageMeta"' in index_html assert 'id="analyticsDrilldownSearch"' in index_html assert 'id="analyticsDrilldownStatus"' in index_html assert 'id="analyticsDrilldownSort"' in index_html assert 'id="analyticsDrilldownClearBtn"' in index_html assert 'id="analyticsDrilldownExportBtn"' in index_html assert 'role="dialog"' in index_html assert 'aria-modal="true"' in index_html assert 'id="analyticsOperations"' not in index_html assert 'id="loadRealtimeBtn"' not in index_html assert 'id="saveQueueMetricsBtn"' not in index_html assert "api('reporting', 'reports/coverage')" in app_js assert "api('reporting', `reports/agents/overview?" in app_js assert "api('reporting', `reports/agents/timeseries?" in app_js assert "api('reporting', `reports/drilldown?" in app_js assert "api('reporting', `reports/timeseries?" in app_js assert "api('reporting', 'reports/views')" in app_js assert "api('reporting', `reports/views/${encodeURIComponent(activeView.id)}`" in app_js assert "api('routing', 'queues')" in app_js assert "api('ai', `ai/analytics/overview?" in app_js assert "api('ai', `ai/analytics/timeseries?" in app_js assert "api('ai', `ai/analytics/voice-name-flow/overview?" in app_js assert "api('ai', `ai/analytics/voice-name-flow/timeseries?" in app_js assert "api('ai', `ai/analytics/drilldown?" in app_js assert "api('ai', `ai/analytics/sessions/${encodeURIComponent(sessionId)}`)" in app_js assert "api('interaction'," in app_js assert "interactions/drilldown?" in app_js assert "params.set('agent_id', normalized.agent_id);" in app_js assert "params.set('q', normalized.q);" in app_js assert "params.set('sort_by', normalized.sort_by || 'created_at');" in app_js assert "params.set('sort_dir', normalized.sort_dir || 'desc');" in app_js assert "interactions/${encodeURIComponent(interactionId)}/timeline" in app_js assert "api('supervisor', 'supervisor/realtime')" not in app_js assert "api('supervisor', 'supervisor/live-calls?limit=20')" not in app_js assert "function loadAnalyticsDashboard" in app_js assert "function renderAnalyticsNarrative" in app_js assert "function renderAnalyticsComparePanel" in app_js assert "function renderAnalyticsTrendChart" in app_js assert "function renderVoiceNameAnalyticsOverview" in app_js assert "function renderVoiceNameAnalyticsTrendChart" in app_js assert "function renderVoiceNameAnalyticsFunnel" in app_js assert "function renderVoiceNameAnalyticsLanguageTable" in app_js assert "function renderVoiceNameAnalyticsQueueTable" in app_js assert "function renderVoiceNameAnalyticsHandoffTable" in app_js assert "function loadVoiceNameAnalyticsTrend" in app_js assert "function voiceNameAnalyticsSupportedChannel" in app_js assert "function renderAiAnalyticsTrendChart" in app_js assert "function renderAiAnalyticsOverview" in app_js assert "function renderAiAnalyticsOutcomeTable" in app_js assert "function renderAiAnalyticsReasonTable" in app_js assert "function renderAgentAnalyticsOverview" in app_js assert "function renderAgentAnalyticsTable" in app_js assert "function renderAgentAnalyticsStateStrip" in app_js assert "function loadAgentAnalyticsTrend" in app_js assert "function renderAgentAnalyticsTrendChart" in app_js assert "function renderAgentAnalyticsTeamTable" in app_js assert "function renderAgentAnalyticsShiftTable" in app_js assert "function handleAgentAnalyticsClick" in app_js assert "function loadAiAnalyticsTrend" in app_js assert "function aiAnalyticsSupportedChannel" in app_js assert "function handleAiAnalyticsOverviewClick" in app_js assert "function syncAnalyticsUrlState" in app_js assert "function parseAnalyticsDeepLinkState" in app_js assert "function restoreAnalyticsDrilldownFromUrlState" in app_js assert "const ANALYTICS_MOCK_DATA_URL = '/analyst/assets/mock-analytics.json';" in app_js assert "await ensureAnalyticsMockDataLoaded();" in app_js assert mock_json.exists() assert '"voice_name_flow"' in mock_json.read_text(encoding="utf-8") assert "window.history.replaceState(null, '', nextUrl);" in app_js assert "url.searchParams.set('dd_mode', drilldown.mode);" in app_js assert "url.searchParams.set('dd_selected', drilldown.selectedId);" in app_js assert "const deepLinkState = parseAnalyticsDeepLinkState();" in app_js assert "state.analytics.activeViewId = deepLinkState.activeViewId || '';" in app_js assert "if (deepLinkState.drilldown) {" in app_js assert "if (state.drilldown.mode === 'ai') {" in app_js assert "return loadAiAnalyticsDrilldownPageState(options);" in app_js assert "renderAiAnalyticsDrilldownActive();" in app_js assert "function handleAiAnalyticsOutcomeClick" in app_js assert "function handleAiAnalyticsReasonClick" in app_js assert "human_touched_rate" in app_js assert "function renderAnalyticsDrilldown" in app_js assert "function fetchAnalyticsMetricDrilldown" in app_js assert "function analyticsMetricCanDrilldown" in app_js assert "function analyticsMetricCoverageHint" in app_js assert "drawer.classList.toggle('is-open', isOpen);" in app_js assert "backdrop.classList.toggle('is-open', isOpen);" in app_js assert "document.body.classList.toggle('analytics-drilldown-open', isOpen);" in app_js assert "function rememberAnalyticsDrilldownReturnFocus" in app_js assert "function restoreAnalyticsDrilldownReturnFocus" in app_js assert "function focusAnalyticsDrilldownPrimaryControl" in app_js assert "function handleAnalyticsDrilldownWindowKeydown" in app_js assert "window.addEventListener('keydown', handleAnalyticsDrilldownWindowKeydown);" in app_js assert "closeBtn.textContent = 'Закрыть · Esc';" in app_js assert "function maybeScrollAnalyticsDrilldownDetailIntoView" in app_js assert "function loadAnalyticsDrilldownPage" in app_js assert "function loadAnalyticsDrilldownDetails" in app_js assert "function openAnalyticsDrilldown" in app_js assert "function handleAnalyticsDrilldownSearchInput" in app_js assert "function handleAnalyticsDrilldownStatusChange" in app_js assert "function handleAnalyticsDrilldownSortChange" in app_js assert "function exportAnalyticsDrilldownCsv" in app_js assert "function sanitizeAnalyticsTimelineMetadata" in app_js assert "function handleAnalyticsOverviewClick" in app_js assert "function handleAnalyticsChannelClick" in app_js assert "function handleAnalyticsQueueClick" in app_js assert "function analyticsDrilldownSourceLabel(sourceType, sourceValue = '')" in app_js assert "if (sourceType === 'agent') {" in app_js assert 'data-analytics-drilldown-source' in app_js assert 'data-analytics-drilldown-metric' in app_js assert 'data-analytics-channel' in app_js assert 'data-analytics-queue-id' in app_js assert 'data-analytics-agent-id' in app_js assert "function saveAnalyticsView" in app_js assert "function exportAnalyticsCsv" in app_js assert "window.localStorage.getItem(analyticsViewsStorageKey())" not in app_js assert "downloadProxyFile(" in app_js assert "integrations/telegram/threads/" not in app_js assert "integrations/whatsapp/threads/" not in app_js assert "api('recording'" not in app_js assert "ai-summary" not in app_js assert "recordings/" not in app_js assert "renderAnalyticsCard('Обращения', 'total', current, previous, {" in app_js assert "renderAnalyticsCard('Обработано', 'answered', current, previous, {" in app_js assert "renderAnalyticsCard('SL', 'SL', current, previous, {" in app_js def test_supervisor_ui_restricts_analyst_access_to_analyst_shell(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "supervisor" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "supervisor" / "index.html").read_text(encoding="utf-8") assert "const allowed = ['admin', 'supervisor'];" in app_js assert "if (role === 'analyst') {" in app_js assert "return '/analyst';" in app_js assert 'data-shell="supervisor" data-roles="admin,supervisor"' in index_html assert 'data-shell="analyst"' in index_html def test_admin_ui_uses_users_and_queues_endpoints(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "admin" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "admin" / "index.html").read_text(encoding="utf-8") assert "api('auth', 'users')" in app_js assert "api('auth', `users/${encodeURIComponent(userId)}`)" in app_js assert "function deleteUser()" in app_js assert "method: 'PATCH'" in app_js assert "method: 'DELETE'" in app_js assert "api('routing', 'queues')" in app_js assert "api('routing', `queues/${encodeURIComponent(queueId)}/rules`" in app_js assert "function deleteQueue()" in app_js assert "api('routing', `queues/${encodeURIComponent(queueId)}`" in app_js assert "api('routing', `queues/${queueId}/route?channel=${channel}&priority=${priority}`" in app_js assert 'id="createUserBtn"' in index_html assert 'id="loadUserDetailBtn"' in index_html assert 'id="updateUserBtn"' in index_html assert 'id="deleteUserBtn"' in index_html assert 'id="createQueueBtn"' in index_html assert 'id="updateQueueBtn"' in index_html assert 'id="deleteQueueBtn"' in index_html def test_admin_ui_contains_ivr_block_and_endpoints(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "admin" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "admin" / "index.html").read_text(encoding="utf-8") assert "api('ivr', 'ivr/flows')" in app_js assert "api('ivr', 'ivr/sessions/start'" in app_js assert "function createIvrFlow()" in app_js assert "function startIvrSession()" in app_js assert "function sendIvrDigit()" in app_js assert "function previewIvrRoute()" in app_js assert "prompt_audio_key" in app_js assert "resolved_queue_code" in app_js assert 'id="ivr"' in index_html assert 'id="createIvrFlowBtn"' in index_html assert 'id="startIvrSessionBtn"' in index_html assert 'id="sendIvrDigitBtn"' in index_html assert 'id="previewIvrRouteBtn"' in index_html def test_admin_ui_contains_voice_name_collection_block_and_endpoints(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "admin" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "admin" / "index.html").read_text(encoding="utf-8") assert "api('ai', 'ai/voice/config/name-collection')" in app_js assert "function loadVoiceNameConfig()" in app_js assert "function saveVoiceNameConfig()" in app_js assert "function resetVoiceNameConfigForm()" in app_js assert "function serializeVoiceNameConfigForm()" in app_js assert 'id="voiceNameCollection"' in index_html assert 'id="loadVoiceNameConfigBtn"' in index_html assert 'id="saveVoiceNameConfigBtn"' in index_html assert 'id="resetVoiceNameConfigBtn"' in index_html assert 'id="voiceNameSummary"' in index_html assert 'id="voiceNameConfigOutput"' in index_html def test_admin_ui_contains_asterisk_bridge_diagnostics(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "admin" / "app.js").read_text(encoding="utf-8") index_html = (Path(__file__).resolve().parents[1] / "ui" / "admin" / "index.html").read_text(encoding="utf-8") assert "api('asterisk-bridge', 'asterisk/status')" in app_js assert "api('asterisk-bridge', `asterisk/events${suffix}`)" in app_js assert "api('asterisk-bridge', `asterisk/events/${encodeURIComponent(bridgeEventId)}/retry`" in app_js assert "function loadAsteriskStatus()" in app_js assert "function retryAsteriskEvent()" in app_js assert 'id="asterisk"' in index_html assert 'id="loadAsteriskStatusBtn"' in index_html assert 'id="loadAsteriskEventsBtn"' in index_html assert 'id="retryAsteriskEventBtn"' in index_html def test_login_ui_redirects_by_role(): app_js = (Path(__file__).resolve().parents[1] / "ui" / "login" / "app.js").read_text(encoding="utf-8") assert "function destinationForRole(role)" in app_js assert "if (role === 'analyst') {" in app_js assert "return '/analyst';" in app_js assert "window.location.href = destinationForRole(data.role);" in app_js assert "window.location.href = destinationForRole(state.role);" in app_js assert "$('sessionRole')" not in app_js def test_shells_update_profile_meta_from_session(): base = Path(__file__).resolve().parents[1] / "ui" operator_js = (base / "operator" / "app.js").read_text(encoding="utf-8") supervisor_js = (base / "supervisor" / "app.js").read_text(encoding="utf-8") analyst_js = (base / "analyst" / "app.js").read_text(encoding="utf-8") admin_js = (base / "admin" / "app.js").read_text(encoding="utf-8") assert "function updateProfileMeta()" in operator_js assert "full_name: state.fullName" in operator_js assert "function updateProfileMeta()" in supervisor_js assert "full_name: state.fullName" in supervisor_js assert "function updateProfileMeta()" in analyst_js assert "full_name: state.fullName" in analyst_js assert "function updateProfileMeta()" in admin_js assert "full_name: state.fullName" in admin_js