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