"""Regression tests for the Settings → Extensions diagnostics and toggles.""" from pathlib import Path import re ROOT = Path(__file__).parent.parent INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8") PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8") STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8") I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8") DOCS_EXTENSIONS = (ROOT / "docs" / "EXTENSIONS.md").read_text(encoding="utf-8") ROUTES_PY = (ROOT / "api" / "routes.py").read_text(encoding="utf-8") def _function_block(name: str, *, extra: int = 2200) -> str: start = PANELS_JS.find(f"function {name}") assert start >= 0, f"{name} not found" return PANELS_JS[start:start + extra] def _between(start_marker: str, end_marker: str) -> str: start = PANELS_JS.find(start_marker) assert start >= 0, f"{start_marker} not found" end = PANELS_JS.find(end_marker, start) assert end >= 0, f"{end_marker} not found after {start_marker}" return PANELS_JS[start:end] def _locale_count() -> int: return len(re.findall(r"^ (?:[A-Za-z_][A-Za-z0-9_]*|'[^']+'):\s*\{", I18N_JS, re.MULTILINE)) def _locale_blocks() -> dict[str, str]: matches = list(re.finditer(r"^ ((?:[A-Za-z_][A-Za-z0-9_]*|'[^']+')):\s*\{", I18N_JS, re.MULTILINE)) blocks = {} for idx, match in enumerate(matches): end = matches[idx + 1].start() if idx + 1 < len(matches) else len(I18N_JS) blocks[match.group(1)] = I18N_JS[match.start():end] return blocks def _locale_string(block: str, key: str) -> str: match = re.search(rf"\b{re.escape(key)}:\s*([\"'])(.*?)\1", block, re.DOTALL) assert match, f"{key} not found in locale block" return match.group(2) def _contains_post_method(block: str) -> bool: """Return True when a JS block contains a method: 'POST' style mutation.""" return bool(re.search(r"\bmethod\s*:\s*([\"'`])POST\1", block)) def test_settings_sidebar_has_extensions_section_and_pane(): assert 'data-settings-section="extensions"' in INDEX_HTML assert "switchSettingsSection('extensions',{fromSidebarItem:true})" in INDEX_HTML assert 'id="settingsPaneExtensions"' in INDEX_HTML assert 'id="extensionsDiagnostics"' in INDEX_HTML assert 'id="extensionsCopyDiagnosticsBtn"' in INDEX_HTML assert 'data-i18n="settings_tab_extensions"' in INDEX_HTML def test_extensions_panel_warns_about_trust_model_and_stays_install_free(): pane_start = INDEX_HTML.index('id="settingsPaneExtensions"') pane_end = INDEX_HTML.index('id="settingsPaneSystem"', pane_start) pane = INDEX_HTML[pane_start:pane_end] assert "Extensions run in the WebUI browser origin" in pane assert "Settings and extension-owned storage are browser-local and not for secrets" in pane assert "Only load trusted local extension directories" in pane assert "takes effect after reload" in pane assert "copyExtensionsDiagnostics()" in pane assert "saveSettings(" not in pane assert "api('/api/settings'" not in pane assert "type=\"checkbox\"" not in pane assert "marketplace" not in pane.lower() assert "static/extension_settings.js" in INDEX_HTML assert INDEX_HTML.index("static/extension_settings.js") < INDEX_HTML.index("static/panels.js") def test_switch_settings_section_supports_extensions_lazy_load(): switch_block = _function_block("switchSettingsSection", extra=2300) assert "name==='extensions'" in switch_block assert "plugins:'Plugins',extensions:'Extensions'" in switch_block assert "'plugins','extensions','system'" in switch_block.replace("\n", "") assert "if(section==='extensions') loadExtensionsPanel();" in switch_block def test_settings_search_knows_extensions_pane(): build_block = _function_block("_buildSettingsIndex", extra=2600) filter_block = _function_block("filterSettings", extra=1700) resolve_block = _function_block("_resolveSettingsField", extra=1400) assert "loadExtensionsPanel()" in build_block assert "settingsPaneExtensions: 'extensions'" in build_block assert "extensions: t('settings_tab_extensions') || 'Extensions'" in filter_block assert "extensions: 'settingsPaneExtensions'" in resolve_block def test_extensions_panel_fetches_status_endpoint_without_mutating_settings(): load_block = _function_block("loadExtensionsPanel", extra=900) assert "api('/api/extensions/status')" in load_block assert "api('/api/settings'" not in load_block assert not _contains_post_method(load_block) assert "extensions-error" in load_block def test_extensions_do_not_add_generic_backend_settings_write_route(): assert "/api/extensions/settings" not in ROUTES_PY assert "/api/extensions/storage" not in ROUTES_PY assert "set_extension_settings" not in ROUTES_PY assert "write_extension_settings" not in ROUTES_PY def test_extensions_diagnostics_tab_refreshes_runtime_status(): tab_block = _function_block("switchExtensionsTab", extra=900) assert "if(tab==='diagnostics') loadExtensionsPanel({preserveExisting:true});" in tab_block assert "if(tab==='gallery'&&!_extensionsGalleryLoaded) loadExtensionsGallery();" in tab_block def test_extensions_panel_renders_sanitized_status_payload(): render_block = _between("function _renderExtensionsPanel", "async function loadExtensionsPanel") warning_block = _function_block("_extensionWarningList", extra=900) asset_block = _function_block("_extensionAssetList", extra=500) assert "extension_dir_configured" in render_block assert "extension_dir_valid" in render_block assert "manifest.status" in render_block assert "manifest.entry_count" in render_block assert "manifest.script_count" in render_block assert "manifest.stylesheet_count" in render_block assert "manifest.sidecar_count" in render_block assert "script_urls" in render_block assert "stylesheet_urls" in render_block assert "data&&data.sidecars" in render_block assert "data&&data.extensions" in render_block assert "counts,'manifest_extensions'" in render_block assert "counts,'user_disabled'" in render_block assert "_extensionInstalledList(extensions,!!(data&&data.extension_dir_configured))" in render_block assert "_extensionSidecarCard(sidecars)" in render_block assert "data&&data.warnings" in render_block assert "esc(url)" in asset_block assert "esc(manifest.status||'unknown')" in render_block assert "const rawCode=(item&&item.code)||'unknown_warning'" in warning_block assert "const code=esc(rawCode)" in warning_block assert "esc((item&&item.source)||'unknown')" in warning_block assert "extension_state_unknown_ids" in warning_block assert "Some saved disabled-extension overrides no longer match the current manifest" in warning_block assert "Rejected" not in render_block # rejected values must never be rendered directly def test_extensions_panel_renders_loopback_sidecar_monitor_safely(): runtime_block = _between("function _extensionRuntimeStatusValue", "function _extensionSidecarCard") sidecar_block = _between("function _extensionSidecarCard", "function _setExtensionSidecarHealth") runtime_setter_block = _between("function _setExtensionSidecarRuntime", "async function _checkExtensionSidecarHealth") monitor_block = _between("async function _checkExtensionSidecarHealth", "function _renderExtensionsPanel") render_block = _between("function _renderExtensionsPanel", "async function loadExtensionsPanel") load_block = _between("async function loadExtensionsPanel", "async function copyExtensionsDiagnostics") load_catch_block = load_block[load_block.index("}catch(e){"):] assert "Loopback sidecars" in sidecar_block assert "No loopback sidecars declared." in sidecar_block assert "esc(title)" in sidecar_block assert "esc(meta)" in sidecar_block assert "esc(origin)" in sidecar_block assert "esc(healthPath)" in sidecar_block assert "esc(healthUrl)" in sidecar_block assert "sidecar&&sidecar.proxy" in sidecar_block assert "proxy.available===true" in sidecar_block assert "proxy.consented===true" in sidecar_block assert "proxy.consent_required===true" in sidecar_block assert "proxy.origin_changed===true" in sidecar_block assert "Proxy path" in sidecar_block assert "data-extension-sidecar-proxy-id" in sidecar_block assert "data-extension-sidecar-proxy-approved" in sidecar_block assert 'data-sidecar-runtime-index="${index}"' in sidecar_block assert "fetch(healthUrl,{credentials:'omit',cache:'no-store'" in monitor_block assert "function _monitorExtensionSidecars(sidecars,seq)" in monitor_block assert "const seq=_extensionsSidecarMonitorSeq" not in monitor_block assert "_monitorExtensionSidecars(sidecars,seq)" in render_block assert "function _renderExtensionsPanel(data,seq)" in render_block assert "_bindExtensionSidecarProxyButtons(target)" in render_block assert "const seq=++_extensionsSidecarMonitorSeq" in load_block assert "opts&&opts.preserveExisting&&target.innerHTML.trim()" in load_block # A failed refresh must NOT be preserved as "existing content": the Loading/ # error placeholders are excluded so a fetch error always renders the error # instead of leaving the panel stuck on "Loading extension diagnostics…". assert "!target.querySelector('.extensions-loading,.extensions-error')" in load_block assert "if(!preserveExisting) target.innerHTML" in load_block assert "loadExtensionsPanel({preserveExisting:true})" in load_block assert "if(seq!==_extensionsSidecarMonitorSeq) return;" in load_block assert "if(seq!==_extensionsSidecarMonitorSeq) return;" in load_catch_block assert "if(preserveExisting&&target.innerHTML.trim()) return;" in load_catch_block assert "_renderExtensionsPanel(data,seq)" in load_block assert "res.ok" in monitor_block assert "res.text" not in monitor_block assert "body=await res.json()" in monitor_block assert "_setExtensionSidecarRuntime(index,body&&typeof body==='object'?body.runtime:null)" in monitor_block assert "String(value??'').trim()" in runtime_block assert r"/^\d+(?:\.\d+)?$/.test(text)" in runtime_block assert "seconds>now+300" in runtime_block assert "runtime.sidecar" in runtime_block assert "runtime.native_host" in runtime_block assert "runtime.bridge" in runtime_block assert "runtime.last_seen_at" in runtime_block assert "runtime.webui_origin" in runtime_block assert "el.innerHTML=details" in runtime_setter_block assert "api('/api/settings'" not in monitor_block assert "api('/extensions/" not in monitor_block assert "sidecar/*" not in monitor_block assert not _contains_post_method(monitor_block) def test_extensions_panel_sidecar_proxy_consent_uses_dedicated_endpoint(): bind_block = _between("function _bindExtensionSidecarProxyButtons", "async function handleExtensionToggle") consent_block = _between("async function handleExtensionSidecarProxyConsent", "function _readExtensionSettingsForm") assert "handleExtensionSidecarProxyConsent" in bind_block assert "data-extension-sidecar-proxy-id" in bind_block assert "api('/api/extensions/sidecar-proxy-consent',{method:'POST',body:JSON.stringify({id,approved})})" in consent_block assert "Extension sidecar proxy approved." in consent_block assert "Extension sidecar proxy consent revoked." in consent_block assert "Failed to update extension sidecar proxy consent" in consent_block assert "api('/api/extensions/toggle'" not in consent_block def test_extensions_panel_toggle_uses_dedicated_endpoint_without_settings_or_install(): installed_block = _between("function _extensionInstalledList", "function _extensionSidecarHealthBadge") toggle_block = _between("async function handleExtensionToggle", "async function loadExtensionsPanel") assert "data-extension-toggle-id" in installed_block assert "data-extension-next-enabled" in installed_block assert "No extension directory is configured." in installed_block assert "No manifest extensions are installed in the configured bundle." in installed_block assert "extensionDirConfigured" in installed_block assert "Manifest-disabled entries cannot be enabled from WebUI." in installed_block assert "api('/api/extensions/toggle',{method:'POST',body:JSON.stringify({id,enabled})})" in toggle_block assert "Reload WebUI to apply changes" in toggle_block combined = installed_block + toggle_block assert "api('/api/settings'" not in combined assert ">Install<" not in combined assert "Install extension" not in combined assert "Uninstall" not in combined assert "marketplace" not in combined.lower() def test_extensions_installed_settings_route_through_shared_accessor(): installed_block = _between("function _extensionInstalledList", "function _extensionSidecarHealthBadge") settings_block = _between("function _configureExtensionSettingsFromStatus", "function _extensionInstalledList") bind_block = _between("function _bindExtensionSettingsButtons", "async function loadExtensionsPanel") gallery_block = _between("function _renderExtensionsGallery", "function _bindExtensionGalleryButtons") assert "entry&&entry.storage_owned" in settings_block assert "window.HermesExtensionSettings.settingsForExtension(id)" in settings_block assert "settingsApi&&settingsApi.schema" in settings_block assert "settingsApi||!settingsApi.trusted" in settings_block assert "data-extension-settings-save" in settings_block assert "data-extension-settings-reset" in settings_block assert "data-extension-storage-clear" in settings_block assert "Browser-local extension settings" in settings_block assert "Do not store secrets here" in settings_block assert "Reload WebUI after enabling or installing this extension to edit browser-local settings." in settings_block assert "_extensionSettingsControls(entry)" in installed_block assert "window.HermesExtensionSettings.settingsForExtension(id).reset()" in bind_block assert "window.HermesExtensionSettings.storageForExtension(id).clear()" in bind_block assert "api('/api/extensions/status')" not in bind_block assert "api('/api/settings'" not in bind_block assert "localStorage" not in bind_block assert "_extensionInstalledList(statusData&&statusData.extensions" in gallery_block assert "_bindExtensionSettingsButtons(installedEl)" in gallery_block def test_extensions_gallery_renders_post_install_guidance(): url_block = _between("function _extensionSafeHttpUrl", "function _extensionPostInstallNote") gallery_block = _between("function _extensionPostInstallNote", "async function loadExtensionsGallery") render_block = _between("function _renderExtensionsGallery", "function _bindExtensionGalleryButtons") install_block = _between("async function handleExtensionInstall", "async function handleExtensionUninstall") assert "/^https?:\\/\\//i.test(raw)" in url_block assert "url.username||url.password" in url_block assert "entry.post_install" in gallery_block assert "post&&post.docs_url" in gallery_block assert "sidecar_start_required" in gallery_block assert "native_host_start_required" in gallery_block assert "requires_local_app" in gallery_block assert "local_app_label" in gallery_block assert "t('ext_gallery_local_component_required')" in gallery_block assert "t('ext_gallery_local_app_label')" in gallery_block assert "t('ext_gallery_required_suffix',localAppLabel)" in gallery_block assert "t('ext_gallery_sidecar_required')" in gallery_block assert "t('ext_gallery_native_host_required')" in gallery_block assert "t('ext_gallery_open_setup_guide')" in gallery_block assert "t(isInstalled?'ext_gallery_next_step':'ext_gallery_after_install')" in gallery_block assert "target=\"_blank\"" in gallery_block assert "rel=\"noopener noreferrer\"" in gallery_block assert "extension-gallery-next-step" in gallery_block assert "esc(summary)" in gallery_block assert "esc(docsUrl)" in gallery_block assert "esc(item)" in gallery_block assert "_extensionPostInstallNote(entry,isInstalled)" in render_block assert "t('ext_gallery_install_restart_required')" in install_block assert "t('ext_gallery_install_followup')" in install_block assert "t('ext_gallery_install_ok')" in install_block assert "webui_restart_required" in install_block def test_extensions_gallery_links_sources_and_humanizes_permissions(): helper_block = _between("function _extensionRegistrySourceUrl", "function _extensionPostInstallNote") render_block = _between("function _renderExtensionsGallery", "function _bindExtensionGalleryButtons") assert "entry.homepage" in helper_block assert "entry.repository_url" in helper_block assert "entry.entry_path||entry.runtime_manifest_path" in helper_block assert "hermes-webui/hermes-webui-extensions/tree/main" in helper_block assert "encodeURIComponent" in helper_block assert "extension-gallery-source-link" in helper_block assert "target=\"_blank\"" in helper_block assert "rel=\"noopener noreferrer\"" in helper_block assert "t('ext_gallery_permissions_empty')" in helper_block assert "webui_api" in helper_block assert "sidecar_commands" in helper_block assert "dom.mutates_core_views" in helper_block assert "storage.shared_webui_keys" in helper_block assert "loopback_sidecar" in helper_block assert "native_host" in helper_block assert "network_external" in helper_block assert "extension-gallery-permission-row" in helper_block assert "_extensionSourceLink(entry)" in render_block assert "_extensionPermissionSummary(perms)" in render_block assert "JSON.stringify(perms" not in render_block assert "
" not in render_block


def test_copy_extensions_diagnostics_copies_current_sanitized_payload():
    copy_block = _between("function copyExtensionsDiagnostics", "// ── Plugins panel")

    assert "JSON.stringify(_extensionsStatusData,null,2)" in copy_block
    assert "navigator.clipboard.writeText(text)" in copy_block
    assert "api('/api/settings'" not in copy_block
    assert "api('/api/extensions'" not in copy_block
    assert not _contains_post_method(copy_block)


def test_extensions_styles_are_scoped_to_extensions_panel():
    assert ".extensions-diagnostics" in STYLE_CSS
    assert ".extensions-trust-note" in STYLE_CSS
    assert ".extension-summary-grid" in STYLE_CSS
    assert ".extension-warning-list" in STYLE_CSS
    assert ".extension-url-list" in STYLE_CSS
    assert ".extension-installed-list" in STYLE_CSS
    assert ".extension-toggle-btn" in STYLE_CSS
    assert ".extension-settings-box" in STYLE_CSS
    assert ".extension-settings-actions" in STYLE_CSS
    assert ".extension-sidecar-list" in STYLE_CSS
    assert ".extension-sidecar-runtime" in STYLE_CSS
    assert ".extension-sidecar-status-badge" in STYLE_CSS
    assert ".extension-gallery-source-link" in STYLE_CSS
    assert ".extension-gallery-next-step" in STYLE_CSS
    assert ".extension-gallery-next-link" in STYLE_CSS
    assert ".extension-gallery-permission-list" in STYLE_CSS
    assert ".extension-gallery-permission-row" in STYLE_CSS


def test_extensions_i18n_keys_exist_for_all_locales():
    blocks = _locale_blocks()

    assert len(blocks) == _locale_count()
    required_keys = [
        "settings_tab_extensions",
        "ext_gallery_next_step",
        "ext_gallery_after_install",
        "ext_gallery_permissions_empty",
        "ext_gallery_local_component_required",
        "ext_gallery_local_app_label",
        "ext_gallery_required_suffix",
        "ext_gallery_sidecar_required",
        "ext_gallery_native_host_required",
        "ext_gallery_open_setup_guide",
        "ext_gallery_install_restart_required",
        "ext_gallery_install_followup",
    ]
    missing = {
        name: [key for key in required_keys if key not in block]
        for name, block in blocks.items()
    }
    missing = {name: keys for name, keys in missing.items() if keys}
    assert not missing, f"Locale(s) missing extension i18n key(s): {missing}"


def test_extensions_post_install_i18n_is_localized_outside_english():
    blocks = _locale_blocks()
    english = blocks["en"]
    post_install_keys = [
        "ext_gallery_next_step",
        "ext_gallery_after_install",
        "ext_gallery_local_component_required",
        "ext_gallery_local_app_label",
        "ext_gallery_required_suffix",
        "ext_gallery_sidecar_required",
        "ext_gallery_native_host_required",
        "ext_gallery_open_setup_guide",
        "ext_gallery_install_restart_required",
        "ext_gallery_install_followup",
    ]
    english_values = {key: _locale_string(english, key) for key in post_install_keys}
    untranslated = {
        name: [
            key
            for key in post_install_keys
            if _locale_string(block, key) == english_values[key]
        ]
        for name, block in blocks.items()
        if name != "en"
    }
    untranslated = {name: keys for name, keys in untranslated.items() if keys}

    assert not untranslated, f"Locale(s) keep English post-install guidance: {untranslated}"
    for name, block in blocks.items():
        assert "{0}" in _locale_string(block, "ext_gallery_required_suffix"), (
            f"{name} must preserve the local-app label placeholder"
        )


def test_extensions_i18n_does_not_include_replacement_characters():
    assert "\ufffd" not in I18N_JS


def test_extensions_docs_mentions_settings_panel_without_install_or_proxy_claims():
    diagnostics_section = DOCS_EXTENSIONS[DOCS_EXTENSIONS.index("## Diagnostics"):]

    assert "Settings → Extensions" in diagnostics_section
    assert "`POST /api/extensions/toggle`" in diagnostics_section
    assert "WebUI-managed override" in diagnostics_section
    assert "does not edit extension" in diagnostics_section
    assert "manifests" in diagnostics_section
    assert "fetch new extension assets" in diagnostics_section
    assert "uninstall files" in diagnostics_section
    assert "GET /api/extensions/status" in diagnostics_section
    assert "`POST /api/extensions/sidecar-proxy-consent`" in diagnostics_section
    assert "sanitized loopback sidecars" in diagnostics_section
    assert "credentials: 'omit'" in diagnostics_section
    assert "fixed per-extension sidecar path" in diagnostics_section
    assert "WebUI strips `Cookie`, `Authorization`, and CSRF headers" in diagnostics_section
    assert "does not create arbitrary extension-owned backend routes" in diagnostics_section
    assert "optional top-level `runtime` object" in diagnostics_section
    assert "allowlisted scalar fields" in diagnostics_section
    assert "browser-local controls" in diagnostics_section
    assert "`window.HermesExtensionSettings`" in DOCS_EXTENSIONS
    assert "does not store extension settings or expose a generic settings write route" in DOCS_EXTENSIONS
    assert "Settings persist only non-default overrides" in DOCS_EXTENSIONS
    assert "do **not**" in diagnostics_section
    assert "return `HERMES_WEBUI_EXTENSION_DIR`" in diagnostics_section
    assert "override state-file path" in diagnostics_section