HERMES-GIELIE/tests/test_extensions_settings_panel.py
2026-06-21 07:10:05 +00:00

154 lines
6.5 KiB
Python

"""Regression tests for the read-only Settings → Extensions diagnostics panel."""
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")
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 _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')" 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_is_read_only_and_warns_about_trust_model():
pane_start = INDEX_HTML.index('id="settingsPaneExtensions"')
pane_end = INDEX_HTML.index('id="settingsPaneSystem"', pane_start)
pane = INDEX_HTML[pane_start:pane_end]
assert "Read-only diagnostics" in pane
assert "Extensions run in the WebUI browser origin" in pane
assert "Only load trusted local extension directories" 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 "toggle" not in pane.lower()
assert "install" not in pane.lower()
assert "marketplace" not in pane.lower()
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_only():
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_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 "script_urls" in render_block
assert "stylesheet_urls" 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 "esc((item&&item.code)||'unknown_warning')" in warning_block
assert "esc((item&&item.source)||'unknown')" in warning_block
assert "Rejected" not in render_block # rejected values must never be rendered directly
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
def test_extensions_tab_i18n_key_exists_for_all_locales():
blocks = _locale_blocks()
assert len(blocks) == _locale_count()
missing = [name for name, block in blocks.items() if "settings_tab_extensions" not in block]
assert not missing, f"Locale(s) missing settings_tab_extensions: {missing}"
def test_extensions_docs_mentions_settings_panel_without_mutation_claims():
diagnostics_section = DOCS_EXTENSIONS[DOCS_EXTENSIONS.index("## Diagnostics"):]
assert "Settings → Extensions" in diagnostics_section
assert "enable, disable, install, or mutate extensions" in diagnostics_section
assert "`/api/extensions/status`" in diagnostics_section
assert "do **not** return" in diagnostics_section