test: read static assets as utf-8 so locale/render tests run on Windows

34 test files read static/*.js via read_text()/open() without an encoding,
so Python's cp1252 default on Windows choked on non-Latin1 bytes and errored
at collection (Ubuntu CI defaults to utf-8, so it never saw this). Add
explicit encoding="utf-8" to those reads — purely mechanical, no assertion
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mo7al876anyandClaude Fable 5 committed 2026-07-04 16:04:23 +03:00
1 parent 980e98dc7e
commit 5f0cc6c567
34 files changed
+145 -145

No files matched your search

+3 -3
View File
@@ -15,7 +15,7 @@ REPO = pathlib.Path(__file__).parent.parent
def read(rel):
return (REPO / rel).read_text()
return (REPO / rel).read_text(encoding="utf-8")
# ── Group A: /root workspace ──────────────────────────────────────────────────
@@ -80,7 +80,7 @@ class TestCronSkillCacheInvalidation:
def _panels_src(self):
return read("static/panels.js")
def test_cache_busted_on_form_open(self):
def test_cache_busted_on_form_open(self, encoding="utf-8"):
src = self._panels_src()
# toggleCronForm should set cache to null unconditionally
# openCronCreate() opens the task create form (renamed from toggleCronForm
@@ -94,7 +94,7 @@ class TestCronSkillCacheInvalidation:
"before fetching skills"
)
def test_cache_not_guarded_by_if_on_open(self):
def test_cache_not_guarded_by_if_on_open(self, encoding="utf-8"):
src = self._panels_src()
# openCronCreate must not gate the fetch behind an if(!_cronSkillsCache) guard.
m = re.search(
+1 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PANELS_JS = (ROOT / "static" / "panels.js").read_text()
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
def _function_body(name: str) -> str:
+16 -16
View File
@@ -7,7 +7,7 @@ WORKSPACE_JS = Path("static/workspace.js").read_text(encoding="utf-8")
def test_csv_extension_regex():
"""Verify _CSV_EXTS regex is defined."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert '_CSV_EXTS' in src, "Missing _CSV_EXTS regex"
assert '.csv' in src, "CSV regex should match .csv extension"
@@ -15,7 +15,7 @@ def test_csv_extension_regex():
def test_csv_fence_block_handler():
"""Verify fenced ```csv blocks are handled."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert "lang==='csv'" in src, "Missing csv language detection in fence handler"
assert 'csv-table' in src, "Missing csv-table class for fenced CSV rendering"
@@ -24,7 +24,7 @@ def test_csv_fence_block_handler():
def test_csv_fence_renders_table_structure():
"""Verify fenced CSV blocks produce proper table HTML."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
# Should have thead, tbody, th, td
assert '<thead>' in src, "CSV table should have <thead>"
@@ -37,7 +37,7 @@ def test_csv_fence_renders_table_structure():
def test_csv_fence_fallback_for_insufficient_rows():
"""Verify CSV with < 2 rows falls back to code block."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
fence_section = src[src.find("lang==='csv'"):src.find("lang==='csv'") + 800]
assert 'rows.length>=2' in fence_section, "Should check for at least 2 rows"
@@ -48,7 +48,7 @@ def test_csv_fence_fallback_for_insufficient_rows():
def test_csv_media_file_handler():
"""Verify MEDIA: CSV files trigger inline loading."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert 'csv-inline-load' in src, "Missing csv-inline-load class for MEDIA: CSV"
assert 'csv_loading' in src, "Missing csv_loading i18n key usage"
@@ -62,7 +62,7 @@ def test_csv_media_file_handler():
def test_loadCsvInline_function():
"""Verify loadCsvInline lazy-load function exists."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert 'function loadCsvInline' in src, "Missing loadCsvInline function"
assert 'function buildCsvTablePreview(path, text)' in src, "Missing shared CSV preview helper"
@@ -70,7 +70,7 @@ def test_loadCsvInline_function():
def test_csv_inline_max_size():
"""Verify CSV inline rendering has a size cap."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert 'const CSV_MAX_SIZE=256*1024' in src, "Should have CSV_MAX_SIZE constant"
helper_section = src[src.find('function buildCsvTablePreview'):src.find('function buildCsvTablePreview') + 2000]
@@ -79,7 +79,7 @@ def test_csv_inline_max_size():
def test_csv_auto_detect_separator():
"""Verify CSV handler auto-detects separator."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
csv_section = src[src.find('function buildCsvTablePreview'):src.find('function buildCsvTablePreview') + 2000]
assert 'separators' in csv_section, "Should have separator detection"
@@ -89,14 +89,14 @@ def test_csv_auto_detect_separator():
def test_csv_quote_stripping():
"""Verify CSV handler strips surrounding quotes from fields."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert "replace(/^[\"']|[\"']$/g,'')" in src, "Should strip quotes from CSV fields"
def test_csv_error_handling():
"""Verify CSV error and empty data handling."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
csv_section = src[src.find('function buildCsvTablePreview'):src.find('function loadCsvInline') + 1000]
assert 'csv_error' in csv_section, "Should use csv_error i18n on fetch failure"
@@ -110,7 +110,7 @@ def test_csv_error_handling():
def test_csv_loadCsvInline_called_after_render():
"""Verify loadCsvInline is called by the consolidated post-render pass."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
# Behavior assertion (not a brittle rAF-literal match): the post-render pass
# is scheduled a frame later, now routed through _postProcessWithAnchorSuppression
@@ -144,7 +144,7 @@ def test_csv_loadCsvInline_called_after_render():
def test_csv_line_ending_normalization():
"""Verify CSV handler normalizes line endings."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
csv_section = src[src.find('function buildCsvTablePreview'):src.find('function buildCsvTablePreview') + 2000]
assert '\\r\\n' in csv_section, "Should handle \\r\\n line endings"
@@ -153,7 +153,7 @@ def test_csv_line_ending_normalization():
def test_csv_i18n_keys():
"""Verify CSV i18n keys exist in all 7 locales."""
with open('static/i18n.js') as f:
with open('static/i18n.js', encoding="utf-8") as f:
src = f.read()
required_keys = ['csv_loading', 'csv_too_large', 'csv_no_data', 'csv_error']
for key in required_keys:
@@ -163,7 +163,7 @@ def test_csv_i18n_keys():
def test_csv_css_classes():
"""Verify CSV table CSS classes are defined."""
with open('static/style.css') as f:
with open('static/style.css', encoding="utf-8") as f:
src = f.read()
required_classes = ['csv-table-wrap', 'csv-table', 'csv-table th', 'csv-table td']
for cls in required_classes:
@@ -174,7 +174,7 @@ def test_csv_css_classes():
def test_csv_not_matched_by_image_exts():
"""Verify .csv is NOT in _IMAGE_EXTS."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
match = re.search(r"const _IMAGE_EXTS=/([^/]+)/i", src)
assert match
@@ -187,7 +187,7 @@ def test_csv_preview_preserves_edit_flow():
workspace Edit affordance that .csv had when it fell through to the code
preview. csv mode must be editable, the preview must cache raw content for
the textarea, and a save must re-render the table (not markdown)."""
with open('static/workspace.js') as f:
with open('static/workspace.js', encoding="utf-8") as f:
src = f.read()
# csv mode is editable
assert "_previewCurrentMode==='csv'" in src, "csv mode should be editable / handled in workspace edit flow"
+17 -17
View File
@@ -4,7 +4,7 @@ import re
def test_excalidraw_extension_regex():
"""Verify _EXCALIDRAW_EXTS regex is defined."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert '_EXCALIDRAW_EXTS' in src, "Missing _EXCALIDRAW_EXTS regex"
assert '.excalidraw' in src, "Excalidraw regex should match .excalidraw"
@@ -12,7 +12,7 @@ def test_excalidraw_extension_regex():
def test_excalidraw_media_handler():
"""Verify MEDIA: .excalidraw files trigger inline loading."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert 'excalidraw-inline-load' in src, "Missing excalidraw-inline-load class"
assert 'excalidraw_loading' in src, "Missing excalidraw_loading i18n key usage"
@@ -20,14 +20,14 @@ def test_excalidraw_media_handler():
def test_loadExcalidrawInline_function():
"""Verify loadExcalidrawInline lazy-load function exists."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert 'function loadExcalidrawInline' in src, "Missing loadExcalidrawInline function"
def test_excalidraw_json_validation():
"""Verify Excalidraw handler validates JSON format."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
func = src[src.find('function loadExcalidrawInline'):src.find('function loadExcalidrawInline') + 2000]
assert 'JSON.parse' in func, "Should parse JSON"
@@ -37,7 +37,7 @@ def test_excalidraw_json_validation():
def test_excalidraw_size_cap():
"""Verify Excalidraw inline rendering has a size cap."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
func = src[src.find('function loadExcalidrawInline'):src.find('function loadExcalidrawInline') + 2000]
assert 'EXCALIDRAW_MAX_SIZE' in func, "Should have EXCALIDRAW_MAX_SIZE constant"
@@ -46,7 +46,7 @@ def test_excalidraw_size_cap():
def test_excalidraw_error_handling():
"""Verify Excalidraw error handling."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
func = src[src.find('function loadExcalidrawInline'):src.find('function loadExcalidrawInline') + 3500]
assert 'excalidraw_error' in func, "Should use excalidraw_error i18n on fetch failure"
@@ -54,7 +54,7 @@ def test_excalidraw_error_handling():
def test_excalidraw_svg_renderer_exists():
"""Verify SVG renderer for Excalidraw elements exists."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert 'function _renderExcalidrawCanvases()' in src, "Missing _renderExcalidrawCanvases function"
start = src.find('function _renderExcalidrawCanvases()')
@@ -66,7 +66,7 @@ def test_excalidraw_svg_renderer_exists():
def test_excalidraw_renders_element_types():
"""Verify SVG renderer handles common Excalidraw element types."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
start = src.find('function _renderExcalidrawCanvases()')
end = src.find('// ── PDF inline preview', start)
@@ -78,7 +78,7 @@ def test_excalidraw_renders_element_types():
def test_excalidraw_arrow_marker():
"""Verify SVG renderer includes arrow marker definition."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
start = src.find('function _renderExcalidrawCanvases()')
end = src.find('// ── PDF inline preview', start)
@@ -89,7 +89,7 @@ def test_excalidraw_arrow_marker():
def test_excalidraw_bounds_calculation():
"""Verify SVG renderer calculates viewBox from element bounds."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
start = src.find('function _renderExcalidrawCanvases()')
end = src.find('// ── PDF inline preview', start)
@@ -101,7 +101,7 @@ def test_excalidraw_bounds_calculation():
def test_excalidraw_empty_elements():
"""Verify empty diagrams show a message."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
start = src.find('function _renderExcalidrawCanvases()')
end = src.find('// ── PDF inline preview', start)
@@ -112,7 +112,7 @@ def test_excalidraw_empty_elements():
def test_excalidraw_download_link():
"""Verify Excalidraw embed includes download link."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
func = src[src.find('function loadExcalidrawInline'):src.find('function loadExcalidrawInline') + 2000]
assert 'excalidraw-open-link' in func, "Should include open/download link"
@@ -121,7 +121,7 @@ def test_excalidraw_download_link():
def test_excalidraw_called_after_render():
"""Verify loadExcalidrawInline is called by the consolidated post-render pass."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
# Behavior assertion (#5338): post-render is now scheduled through
# _postProcessWithAnchorSuppression (which still calls postProcessRenderedMessages).
@@ -139,7 +139,7 @@ def test_excalidraw_called_after_render():
def test_excalidraw_embed_wrap_structure():
"""Verify Excalidraw embed uses proper container structure."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert 'excalidraw-embed-wrap' in src, "Missing excalidraw-embed-wrap container"
assert 'excalidraw-canvas' in src, "Missing excalidraw-canvas div"
@@ -148,7 +148,7 @@ def test_excalidraw_embed_wrap_structure():
def test_excalidraw_i18n_keys():
"""Verify Excalidraw i18n keys exist in all 7 locales."""
with open('static/i18n.js') as f:
with open('static/i18n.js', encoding="utf-8") as f:
src = f.read()
required_keys = [
'excalidraw_loading', 'excalidraw_too_large', 'excalidraw_invalid',
@@ -162,7 +162,7 @@ def test_excalidraw_i18n_keys():
def test_excalidraw_css_classes():
"""Verify Excalidraw CSS classes are defined."""
with open('static/style.css') as f:
with open('static/style.css', encoding="utf-8") as f:
src = f.read()
required_classes = [
'excalidraw-embed-wrap', 'excalidraw-canvas', 'excalidraw-svg',
@@ -189,7 +189,7 @@ def test_excalidraw_css_classes():
# be coerced via Number()/isFinite gates so they cannot carry strings.
def _excalidraw_render_block():
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
start = src.find('function _renderExcalidrawCanvases')
assert start != -1, '_renderExcalidrawCanvases not found'
+2 -2
View File
@@ -3,12 +3,12 @@ import re
def _src(name: str) -> str:
with open(f"static/{name}") as f:
with open(f"static/{name}", encoding="utf-8") as f:
return f.read()
def _py_src() -> str:
with open("api/helpers.py") as f:
with open("api/helpers.py", encoding="utf-8") as f:
return f.read()
+1 -1
View File
@@ -36,7 +36,7 @@ import pytest
from tests.test_sprint16 import render_md # Python mirror of renderMd()
UI_JS = (Path(__file__).resolve().parent.parent / "static" / "ui.js").read_text()
UI_JS = (Path(__file__).resolve().parent.parent / "static" / "ui.js").read_text(encoding="utf-8")
def _strip_pre_blocks(out: str) -> str:
@@ -20,7 +20,7 @@ import re
def _src(name: str) -> str:
with open(f"static/{name}") as f:
with open(f"static/{name}", encoding="utf-8") as f:
return f.read()
+1 -1
View File
@@ -3,7 +3,7 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
UI_JS = (ROOT / "static" / "ui.js").read_text()
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
def _body_between(src: str, start: str, end: str) -> str:
+2 -2
View File
@@ -1,8 +1,8 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
UI_JS = (ROOT / "static" / "ui.js").read_text()
STYLE_CSS = (ROOT / "static" / "style.css").read_text()
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
def test_error_toast_default_duration_is_substantially_longer_than_info_toasts():
+3 -3
View File
@@ -11,9 +11,9 @@ from tests._pytest_port import BASE, TEST_STATE_DIR
ROOT = pathlib.Path(__file__).resolve().parent.parent
ROUTES_PY = (ROOT / "api" / "routes.py").read_text()
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text()
STYLE_CSS = (ROOT / "static" / "style.css").read_text()
ROUTES_PY = (ROOT / "api" / "routes.py").read_text(encoding="utf-8")
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
def post(path, body=None):
@@ -2,7 +2,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
UI_JS = (ROOT / "static" / "ui.js").read_text()
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
def test_temporary_configured_model_option_carries_provider_badge():
+1 -1
View File
@@ -5,7 +5,7 @@ import subprocess
def _src(name: str) -> str:
with open(f"static/{name}") as f:
with open(f"static/{name}", encoding="utf-8") as f:
return f.read()
+1 -1
View File
@@ -2,7 +2,7 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text()
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
def _function_body(src: str, name: str) -> str:
+3 -3
View File
@@ -52,7 +52,7 @@ def test_check_repo_still_returns_dict_when_dot_git_exists(tmp_path):
def test_format_update_target_status_excludes_no_git():
"""_formatUpdateTargetStatus returns null for no_git so it doesn't show as update available."""
ui_path = Path(__file__).resolve().parent.parent / 'static' / 'ui.js'
content = ui_path.read_text()
content = ui_path.read_text(encoding="utf-8")
assert '_formatUpdateTargetStatus' in content
assert 'info.no_git' in content
@@ -67,7 +67,7 @@ def test_panels_has_no_git_branch():
target is no_git (which hid the indicator in mixed installs).
"""
panels_path = Path(__file__).resolve().parent.parent / 'static' / 'panels.js'
content = panels_path.read_text()
content = panels_path.read_text(encoding="utf-8")
# Check for the no_git conditional in the update consumer
assert 'no_git' in content
@@ -82,7 +82,7 @@ def test_panels_has_no_git_branch():
def test_i18n_no_git_key_all_locales():
"""settings_update_no_git key appears in all 14 locale blocks in static/i18n.js."""
i18n_path = Path(__file__).resolve().parent.parent / 'static' / 'i18n.js'
content = i18n_path.read_text()
content = i18n_path.read_text(encoding="utf-8")
# Count occurrences of the new key (should be exactly 13, one per locale)
count = content.count("settings_update_no_git")
+1 -1
View File
@@ -19,7 +19,7 @@ import re
import html as _html
REPO_ROOT = pathlib.Path(__file__).parent.parent
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text()
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
# ── Helpers ──────────────────────────────────────────────────────────────────
+2 -2
View File
@@ -21,8 +21,8 @@ import re
import html as _html
REPO_ROOT = pathlib.Path(__file__).parent.parent
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text()
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text()
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
# ── Helpers ───────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -14,7 +14,7 @@ import pathlib
import re
REPO_ROOT = pathlib.Path(__file__).parent.parent
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text()
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
# ── Source-level check ────────────────────────────────────────────────────────
@@ -3,7 +3,7 @@
from pathlib import Path
MESSAGES_JS = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text()
MESSAGES_JS = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text(encoding="utf-8")
def test_messages_js_defines_active_session_viewed_helper():
@@ -3,8 +3,8 @@
from pathlib import Path
SESSIONS_JS = (Path(__file__).resolve().parent.parent / "static" / "sessions.js").read_text()
STYLE_CSS = (Path(__file__).resolve().parent.parent / "static" / "style.css").read_text()
SESSIONS_JS = (Path(__file__).resolve().parent.parent / "static" / "sessions.js").read_text(encoding="utf-8")
STYLE_CSS = (Path(__file__).resolve().parent.parent / "static" / "style.css").read_text(encoding="utf-8")
def test_pinned_indicator_renders_inside_title_row():
@@ -133,7 +133,7 @@ def test_plain_mouse_hover_does_not_mark_session_row_dragging():
def test_sidebar_uses_local_inflight_state_for_immediate_spinner():
messages_js = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text()
messages_js = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text(encoding="utf-8")
assert "function _isSessionLocallyStreaming(s)" in SESSIONS_JS
assert "isActive && Boolean(S.busy)" in SESSIONS_JS
@@ -158,7 +158,7 @@ def test_date_group_caret_expanded_down_collapsed_right():
def test_apperror_path_calls_render_session_list():
"""apperror handler must call renderSessionList() to clear the streaming indicator
immediately rather than waiting for the 5s streaming poll interval."""
messages_js = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text()
messages_js = (Path(__file__).resolve().parent.parent / "static" / "messages.js").read_text(encoding="utf-8")
apperror_idx = messages_js.find("source.addEventListener('apperror'")
assert apperror_idx != -1, "apperror handler not found in messages.js"
warning_idx = messages_js.find("source.addEventListener('warning'", apperror_idx)
+4 -4
View File
@@ -14,10 +14,10 @@ import pytest
ROOT = Path(__file__).resolve().parents[1]
INDEX_HTML = (ROOT / "static" / "index.html").read_text()
PANELS_JS = (ROOT / "static" / "panels.js").read_text()
STYLE_CSS = (ROOT / "static" / "style.css").read_text()
I18N_JS = (ROOT / "static" / "i18n.js").read_text()
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")
def test_kanban_header_exposes_accessible_view_toggle():
@@ -9,9 +9,9 @@ next ``loadSession()`` before ``syncTopbar()`` projects server metadata.
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BOOT_JS = (ROOT / "static" / "boot.js").read_text()
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text()
UI_JS = (ROOT / "static" / "ui.js").read_text()
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
def _body_between(src: str, start: str, end: str) -> str:
+3 -3
View File
@@ -10,12 +10,12 @@ import pytest
def _read_js(name):
with open(os.path.join('static', name)) as f:
with open(os.path.join('static', name), encoding="utf-8") as f:
return f.read()
def _read_css():
with open(os.path.join('static', 'style.css')) as f:
with open(os.path.join('static', 'style.css'), encoding="utf-8") as f:
return f.read()
@@ -316,7 +316,7 @@ class TestI18nKeys:
HTML_KEYS = ['html_loading', 'html_too_large', 'html_error', 'html_open_full', 'html_sandbox_label']
def _find_locale_block(self, locale):
with open('static/i18n.js') as f:
with open('static/i18n.js', encoding="utf-8") as f:
content = f.read()
start = content.find(f"'{locale}':")
if start < 0:
+3 -3
View File
@@ -4,9 +4,9 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MESSAGES_SRC = (ROOT / "static" / "messages.js").read_text()
SESSIONS_SRC = (ROOT / "static" / "sessions.js").read_text()
UI_SRC = (ROOT / "static" / "ui.js").read_text()
MESSAGES_SRC = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
SESSIONS_SRC = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
UI_SRC = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
def _function_body(src: str, signature: str) -> str:
+1 -1
View File
@@ -6,7 +6,7 @@ import queue
ROOT = Path(__file__).resolve().parents[1]
ROUTES_SRC = (ROOT / "api" / "routes.py").read_text()
ROUTES_SRC = (ROOT / "api" / "routes.py").read_text(encoding="utf-8")
def test_stream_status_exposes_replay_summary():
+20 -20
View File
@@ -4,7 +4,7 @@ import re
def test_batch_select_state_variables():
"""Verify batch select state variables are declared."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
assert '_sessionSelectMode' in src, "Missing _sessionSelectMode variable"
assert '_selectedSessions' in src, "Missing _selectedSessions variable"
@@ -13,7 +13,7 @@ def test_batch_select_state_variables():
def test_batch_select_functions_exist():
"""Verify all batch select functions are defined."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
required_funcs = [
'toggleSessionSelectMode',
@@ -31,7 +31,7 @@ def test_batch_select_functions_exist():
def test_batch_select_checkbox_rendering():
"""Verify checkbox is rendered when in select mode."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
assert 'session-select-cb' in src, "Missing session-select-cb class"
assert 'session-select-cb-wrapper' in src, "Missing session-select-cb-wrapper class"
@@ -40,7 +40,7 @@ def test_batch_select_checkbox_rendering():
def test_batch_select_intercepts_navigation():
"""Verify select mode intercepts session navigation."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
assert "_sessionSelectMode" in src
# Should have early return when in select mode
@@ -50,7 +50,7 @@ def test_batch_select_intercepts_navigation():
def test_batch_checkbox_sets_selection_without_row_double_toggle():
"""Clicking the checkbox itself must not also trigger row-level toggling."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
assert 'function setSessionSelected(sid, selected)' in src, \
"Checkbox changes should set explicit state instead of toggling blindly"
@@ -64,7 +64,7 @@ def test_batch_checkbox_sets_selection_without_row_double_toggle():
def test_batch_select_escape_handler():
"""Verify Escape key exits select mode."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
assert "e.key==='Escape'&&_sessionSelectMode" in src, \
"Should have Escape key handler for select mode"
@@ -72,7 +72,7 @@ def test_batch_select_escape_handler():
def test_batch_select_toggle_button():
"""Verify select mode toggle button is rendered."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
assert 'session-select-toggle' in src, "Missing session-select-toggle class"
assert 'toggleSessionSelectMode' in src, "Missing toggleSessionSelectMode call"
@@ -80,7 +80,7 @@ def test_batch_select_toggle_button():
def test_batch_select_bar_element():
"""Verify batch action bar DOM element is created."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
assert 'batchActionBar' in src, "Missing batchActionBar element"
assert 'batch-action-bar' in src, "Missing batch-action-bar CSS class"
@@ -89,7 +89,7 @@ def test_batch_select_bar_element():
def test_batch_action_bar_overrides_css_hidden_state():
"""Selected sessions must make the fixed action bar visible."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
assert "if(count>0){_renderBatchActionBar();}" in src, \
"Updating selected count must render action buttons, not just reveal an empty bar"
@@ -107,9 +107,9 @@ def test_batch_action_bar_overrides_css_hidden_state():
def test_batch_action_bar_is_sidebar_inline_not_global_footer():
"""Batch actions should appear in the session list, not over the composer."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
js = f.read()
with open('static/style.css') as f:
with open('static/style.css', encoding="utf-8") as f:
css = f.read()
assert "list.appendChild(batchBar)" in js, \
"Batch action bar should be rendered inside the session list"
@@ -123,9 +123,9 @@ def test_batch_action_bar_is_sidebar_inline_not_global_footer():
def test_batch_project_picker_is_anchored_to_batch_actions():
"""Batch move project picker should open inside the sidebar action bar."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
js = f.read()
with open('static/style.css') as f:
with open('static/style.css', encoding="utf-8") as f:
css = f.read()
assert "const bar=$('batchActionBar');if(!bar)return;" in js, \
"Batch project picker should anchor to the batch action bar"
@@ -143,7 +143,7 @@ def test_batch_project_picker_is_anchored_to_batch_actions():
def test_streaming_zero_message_sessions_stay_visible_after_reload():
"""In-flight sessions may have zero saved messages during reload recovery."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
assert "_isSessionEffectivelyStreaming(s)" in src, \
"Streaming sessions must bypass the zero-message sidebar filter"
@@ -155,7 +155,7 @@ def test_streaming_zero_message_sessions_stay_visible_after_reload():
def test_boot_does_not_drop_zero_message_inflight_session():
"""Reloading /session/<id> during a running turn must keep the session open."""
with open('static/boot.js') as f:
with open('static/boot.js', encoding="utf-8") as f:
src = f.read()
assert "const _restoredInFlight = S.session && (" in src, \
"Boot must detect restored in-flight sessions before ephemeral cleanup"
@@ -169,7 +169,7 @@ def test_boot_does_not_drop_zero_message_inflight_session():
def test_batch_select_i18n_keys():
"""Verify all batch select i18n keys exist in all locales."""
with open('static/i18n.js') as f:
with open('static/i18n.js', encoding="utf-8") as f:
src = f.read()
required_keys = [
'session_select_mode',
@@ -202,7 +202,7 @@ def test_batch_select_i18n_keys():
def test_i18n_string_placeholder_interpolation_supported():
"""String-valued translations with {0} placeholders should interpolate args."""
with open('static/i18n.js') as f:
with open('static/i18n.js', encoding="utf-8") as f:
src = f.read()
assert "String(val).replace(/\\{(\\d+)\\}/g" in src, \
"t() must interpolate {0}-style placeholders for string-valued translations"
@@ -212,7 +212,7 @@ def test_i18n_string_placeholder_interpolation_supported():
def test_batch_select_css_exists():
"""Verify batch select CSS classes are defined."""
with open('static/style.css') as f:
with open('static/style.css', encoding="utf-8") as f:
src = f.read()
required_classes = [
'session-select-toggle',
@@ -233,7 +233,7 @@ def test_batch_select_css_exists():
def test_batch_select_mode_flags():
"""Verify select mode properly toggles state."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
# toggleSessionSelectMode should flip the flag
assert '_sessionSelectMode=!_sessionSelectMode' in src, \
@@ -247,7 +247,7 @@ def test_batch_select_mode_flags():
def test_batch_delete_uses_confirm_dialog():
"""Verify batch delete shows confirmation dialog."""
with open('static/sessions.js') as f:
with open('static/sessions.js', encoding="utf-8") as f:
src = f.read()
# The delete handler should call showConfirmDialog with batch message
assert "session_batch_delete_confirm" in src, \
+2 -2
View File
@@ -7,8 +7,8 @@ and the streaming render path (messages.js _streamDisplay logic).
import pathlib
REPO_ROOT = pathlib.Path(__file__).parent.parent
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text()
MSG_JS = (REPO_ROOT / "static" / "messages.js").read_text()
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
MSG_JS = (REPO_ROOT / "static" / "messages.js").read_text(encoding="utf-8")
# ── ui.js: static render path ────────────────────────────────────────────────
+3 -3
View File
@@ -16,8 +16,8 @@ from unittest.mock import patch
import api.onboarding as mod
REPO_ROOT = pathlib.Path(__file__).parent.parent
I18N_JS = (REPO_ROOT / "static" / "i18n.js").read_text()
ONBOARDING_JS = (REPO_ROOT / "static" / "onboarding.js").read_text()
I18N_JS = (REPO_ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
ONBOARDING_JS = (REPO_ROOT / "static" / "onboarding.js").read_text(encoding="utf-8")
# ── Backend: _build_setup_catalog ──────────────────────────────────────────
@@ -152,7 +152,7 @@ class TestOAuthOnboardingJs(unittest.TestCase):
def test_style_css_has_oauth_card_rules(self):
"""style.css must contain the .onboarding-oauth-card rules."""
css = (REPO_ROOT / "static" / "style.css").read_text()
css = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
self.assertIn("onboarding-oauth-card", css)
self.assertIn("onboarding-oauth-ready", css)
self.assertIn("onboarding-oauth-pending", css)
+7 -7
View File
@@ -18,9 +18,9 @@ if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
REPO_ROOT = _REPO_ROOT
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text()
SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text()
PANELS_JS = (REPO_ROOT / "static" / "panels.js").read_text()
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
PANELS_JS = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8")
try:
from api import config as _api_config
@@ -101,7 +101,7 @@ class TestGatewaySessionNullModel(unittest.TestCase):
"""api/models.py must not use `or 'unknown'` for the model field
so that a NULL model in state.db is returned as None (falsy) to
the frontend rather than the truthy string 'unknown'."""
models_src = (REPO_ROOT / "api" / "models.py").read_text()
models_src = (REPO_ROOT / "api" / "models.py").read_text(encoding="utf-8")
# Ensure the old fallback pattern is gone
self.assertNotIn(
"'model': row['model'] or 'unknown'",
@@ -113,7 +113,7 @@ class TestGatewaySessionNullModel(unittest.TestCase):
def test_gateway_watcher_null_model_returns_none_not_unknown(self):
"""api/gateway_watcher.py must not use `or 'unknown'` for the model
field so that a NULL model in state.db is returned as None (falsy)."""
gw_src = (REPO_ROOT / "api" / "gateway_watcher.py").read_text()
gw_src = (REPO_ROOT / "api" / "gateway_watcher.py").read_text(encoding="utf-8")
self.assertNotIn(
"'model': row['model'] or 'unknown'",
gw_src,
@@ -124,8 +124,8 @@ class TestGatewaySessionNullModel(unittest.TestCase):
def test_gateway_session_model_uses_none_fallback(self):
"""Both source files must use `row['model'] or None` (explicit None
fallback) for the model field assignment."""
models_src = (REPO_ROOT / "api" / "models.py").read_text()
gw_src = (REPO_ROOT / "api" / "gateway_watcher.py").read_text()
models_src = (REPO_ROOT / "api" / "models.py").read_text(encoding="utf-8")
gw_src = (REPO_ROOT / "api" / "gateway_watcher.py").read_text(encoding="utf-8")
self.assertIn(
"'model': row['model'] or None,",
models_src,
+4 -4
View File
@@ -12,10 +12,10 @@ import re
import unittest
REPO_ROOT = pathlib.Path(__file__).parent.parent
CSS = (REPO_ROOT / "static" / "style.css").read_text()
HTML = (REPO_ROOT / "static" / "index.html").read_text()
MESSAGES_JS = (REPO_ROOT / "static" / "messages.js").read_text()
STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text()
CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
HTML = (REPO_ROOT / "static" / "index.html").read_text(encoding="utf-8")
MESSAGES_JS = (REPO_ROOT / "static" / "messages.js").read_text(encoding="utf-8")
STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text(encoding="utf-8")
# ── streaming.py: title auto-generation condition ─────────────────────────
+6 -6
View File
@@ -19,7 +19,7 @@ import unittest
from unittest import mock
REPO_ROOT = pathlib.Path(__file__).parent.parent
STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text()
STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text(encoding="utf-8")
# ── Shared helpers for sprint-42 additional tests ────────────────────────────
@@ -717,7 +717,7 @@ def test_cleanTitle_is_let_not_const():
# ── Sprint 42 additional tests: thinking panel persistence (#427) ────────
def test_streaming_persists_reasoning_in_session():
"""streaming.py must accumulate reasoning and patch assistant messages."""
src = (REPO / 'api' / 'streaming.py').read_text()
src = (REPO / 'api' / 'streaming.py').read_text(encoding="utf-8")
# #3587: per-message reasoning segments replaced the flat _reasoning_text accumulator
assert "_reasoning_segments" in src, \
@@ -752,7 +752,7 @@ def test_streaming_persists_reasoning_in_session():
def test_done_handler_patches_reasoning_field():
"""messages.js done SSE handler must patch reasoningText onto the last assistant message."""
src = (REPO / 'static' / 'messages.js').read_text()
src = (REPO / 'static' / 'messages.js').read_text(encoding="utf-8")
# The persistence comment must be present inside the done handler
assert "Persist reasoning trace for Worklog Thinking Cards" in src, \
@@ -778,7 +778,7 @@ def test_done_handler_patches_reasoning_field():
def test_rendermessages_keeps_reasoning_metadata_out_of_worklog_display():
"""ui.js renderMessages must not promote provider reasoning metadata into Worklog prose."""
src = (REPO / 'static' / 'ui.js').read_text()
src = (REPO / 'static' / 'ui.js').read_text(encoding="utf-8")
sig_fn = src.split("function _messageHasReasoningPayload(m)", 1)[1].split("function", 1)[0]
assert 'm.reasoning' in sig_fn, \
@@ -801,7 +801,7 @@ def test_streaming_restores_prior_reasoning_metadata_after_followup():
history before saving the session, including reinserting dropped
reasoning-only assistant segments.
"""
src = (REPO / 'api' / 'streaming.py').read_text()
src = (REPO / 'api' / 'streaming.py').read_text(encoding="utf-8")
assert "def _restore_reasoning_metadata(" in src, \
"streaming.py must define a helper to restore prior reasoning metadata"
assert "_next_context_messages" in src and "s.context_messages" in src, \
@@ -814,7 +814,7 @@ def test_streaming_restores_prior_reasoning_metadata_after_followup():
def test_routes_restores_prior_reasoning_metadata_after_followup():
"""The non-streaming route path must preserve prior reasoning metadata too."""
src = (REPO / 'api' / 'routes.py').read_text()
src = (REPO / 'api' / 'routes.py').read_text(encoding="utf-8")
assert "_restore_reasoning_metadata" in src, \
"routes.py must import reasoning metadata restoration helper"
assert "_next_context_messages" in src and "s.context_messages" in src, \
+10 -10
View File
@@ -18,16 +18,16 @@ import sys
import unittest
REPO_ROOT = pathlib.Path(__file__).parent.parent
GATEWAY_WATCHER_PY = (REPO_ROOT / "api" / "gateway_watcher.py").read_text()
CONFIG_PY = (REPO_ROOT / "api" / "config.py").read_text()
BOOTSTRAP_PY = (REPO_ROOT / "bootstrap.py").read_text()
SERVER_PY = (REPO_ROOT / "server.py").read_text()
ROUTES_PY = (REPO_ROOT / "api" / "routes.py").read_text()
AUTH_PY = (REPO_ROOT / "api" / "auth.py").read_text()
PROFILES_PY = (REPO_ROOT / "api" / "profiles.py").read_text()
STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text()
WORKSPACE_PY = (REPO_ROOT / "api" / "workspace.py").read_text()
STATE_SYNC_PY = (REPO_ROOT / "api" / "state_sync.py").read_text()
GATEWAY_WATCHER_PY = (REPO_ROOT / "api" / "gateway_watcher.py").read_text(encoding="utf-8")
CONFIG_PY = (REPO_ROOT / "api" / "config.py").read_text(encoding="utf-8")
BOOTSTRAP_PY = (REPO_ROOT / "bootstrap.py").read_text(encoding="utf-8")
SERVER_PY = (REPO_ROOT / "server.py").read_text(encoding="utf-8")
ROUTES_PY = (REPO_ROOT / "api" / "routes.py").read_text(encoding="utf-8")
AUTH_PY = (REPO_ROOT / "api" / "auth.py").read_text(encoding="utf-8")
PROFILES_PY = (REPO_ROOT / "api" / "profiles.py").read_text(encoding="utf-8")
STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text(encoding="utf-8")
WORKSPACE_PY = (REPO_ROOT / "api" / "workspace.py").read_text(encoding="utf-8")
STATE_SYNC_PY = (REPO_ROOT / "api" / "state_sync.py").read_text(encoding="utf-8")
# ── B324: MD5 usedforsecurity=False ─────────────────────────────────────────
+1 -1
View File
@@ -15,7 +15,7 @@ REPO = pathlib.Path(__file__).parent.parent
def read(rel):
return (REPO / rel).read_text()
return (REPO / rel).read_text(encoding="utf-8")
# ── Bug #702 — XML tool-call leak on DeepSeek ────────────────────────────────
+14 -14
View File
@@ -4,7 +4,7 @@ import re
def test_media_extension_regexes_exist():
"""Verify SVG/audio/video extension regexes are defined."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert '_SVG_EXTS' in src, "Missing _SVG_EXTS regex"
assert '_AUDIO_EXTS' in src, "Missing _AUDIO_EXTS regex"
@@ -19,7 +19,7 @@ def test_media_extension_regexes_exist():
def test_svg_rendered_before_image_catch_all():
"""Verify SVG handler for URLs runs before the catch-all image handler."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
# Find positions of SVG vs image catch-all in the URL section
svg_url_match = src.find("SVG URLs")
@@ -33,7 +33,7 @@ def test_svg_rendered_before_image_catch_all():
def test_local_svg_inline_rendering():
"""Verify local SVG files render as inline image."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert "msg-media-svg" in src, "Missing msg-media-svg CSS class for SVG rendering"
# Should have at least 2 SVG handlers (URL + local)
@@ -43,7 +43,7 @@ def test_local_svg_inline_rendering():
def test_local_audio_inline_rendering():
"""Verify local audio files render as inline player."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert "msg-media-audio" in src, "Missing msg-media-audio CSS class"
assert "<audio controls" in src, "Should render <audio> element with controls"
@@ -53,7 +53,7 @@ def test_local_audio_inline_rendering():
def test_local_video_inline_rendering():
"""Verify local video files render as inline player."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert "msg-media-video" in src, "Missing msg-media-video CSS class"
assert "<video controls" in src, "Should render <video> element with controls"
@@ -63,7 +63,7 @@ def test_local_video_inline_rendering():
def test_url_svg_audio_video_handlers():
"""Verify HTTPS URLs for SVG/audio/video get inline rendering."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
# SVG URLs should be handled via _SVG_EXTS test on urlPath
url_svg = "_SVG_EXTS.test(urlPath)" in src or ("_SVG_EXTS.test" in src and "urlPath" in src)
@@ -77,7 +77,7 @@ def test_url_svg_audio_video_handlers():
def test_attachment_svg_audio_video():
"""Verify file attachments for SVG/audio/video get inline previews."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert "attach-thumb--svg" in src, "Missing attach-thumb--svg for SVG thumbnails"
assert "attach-chip--audio" in src, "Missing attach-chip--audio"
@@ -87,7 +87,7 @@ def test_attachment_svg_audio_video():
def test_attachment_blob_url_cleanup():
"""Verify audio/video attachment chips create blob URLs."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
# SVG and media attachments should use createObjectURL
assert "URL.createObjectURL(f)" in src, "Should create blob URLs for attachments"
@@ -95,21 +95,21 @@ def test_attachment_blob_url_cleanup():
def test_preload_metadata():
"""Verify audio/video elements use preload='metadata' for performance."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert 'preload="metadata"' in src, "Audio/video should use preload='metadata'"
def test_media_label_class():
"""Verify media label class exists for type identification."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
assert "msg-media-label" in src, "Missing msg-media-label class"
def test_i18n_keys():
"""Verify media rendering i18n keys exist in all locales."""
with open('static/i18n.js') as f:
with open('static/i18n.js', encoding="utf-8") as f:
src = f.read()
required_keys = [
'media_audio_label',
@@ -123,7 +123,7 @@ def test_i18n_keys():
def test_css_classes_exist():
"""Verify all media CSS classes are defined."""
with open('static/style.css') as f:
with open('static/style.css', encoding="utf-8") as f:
src = f.read()
required_classes = [
'msg-media-svg',
@@ -141,7 +141,7 @@ def test_css_classes_exist():
def test_svg_not_matched_by_image_exts():
"""Verify .svg is NOT in _IMAGE_EXTS (SVG has its own handler)."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
# Extract the _IMAGE_EXTS regex
match = re.search(r"const _IMAGE_EXTS=/([^/]+)/i", src)
@@ -152,7 +152,7 @@ def test_svg_not_matched_by_image_exts():
def test_audio_video_not_matched_by_image_exts():
"""Verify audio/video extensions are NOT in _IMAGE_EXTS."""
with open('static/ui.js') as f:
with open('static/ui.js', encoding="utf-8") as f:
src = f.read()
match = re.search(r"const _IMAGE_EXTS=/([^/]+)/i", src)
assert match
+2 -2
View File
@@ -1,7 +1,7 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
UI_JS = (ROOT / "static" / "ui.js").read_text()
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
def test_topbar_uses_session_total_for_lazy_loaded_transcripts():
@@ -28,7 +28,7 @@ def test_sync_topbar_does_not_count_only_loaded_tail_messages():
assert "document.title='● '+document.title;" in block
assert "const pendingPrefix=(typeof activeSessionHasPendingPromptAttention==='function'&&activeSessionHasPendingPromptAttention())?'● ':'';" not in block
sessions_js = (ROOT / "static" / "sessions.js").read_text()
sessions_js = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
fn = sessions_js[
sessions_js.index("async function _ensureMessagesLoaded") :
sessions_js.index("function _messageComparableText", sessions_js.index("async function _ensureMessagesLoaded"))