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:
1 parent
980e98dc7e
commit
5f0cc6c567
34 files changed
+145
-145
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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, \
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ─────────────────────────
|
||||
|
||||
@@ -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
@@ -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 ─────────────────────────────────────────
|
||||
|
||||
@@ -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 ────────────────────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in new issue
Block a user