release: v0.50.241 Batch release of 4 PRs: - #1290 (@nickgiulioni1) — Inline audio/video media editor with playback speed controls and HTTP byte-range streaming. PDF/media previews in workspace file browser. Composer tray inline players for audio/video. (Rebased from #1232.) - #1287 (@renatomott) — Configured model badges (Primary / Fallback N) in the model picker, carried through to the composer chip. Persists through on-disk model cache. - #1289 (@franksong2702) — Appearance autosave for theme/skin/font-size in Settings; inline Saving / Saved / Failed status. Font size now persists to config.yaml. Refs #1003. - #1294 (@franksong2702) — Normalize agent session source metadata (raw_source / session_source / source_label) through /api/sessions and gateway watcher SSE snapshots. Existing source_tag / is_cli_session fields preserved. Refs #1013. Tests: 3254 passed, 2 skipped, 3 xpassed (was 3199 before this release). Independently reviewed and approved by nesquena (commit d1738f6).
139 lines
5.2 KiB
Python
139 lines
5.2 KiB
Python
"""Regression checks for Issue #1003: Appearance settings autosave.
|
|
|
|
Focus:
|
|
- Theme/Skin/Font size should autosave immediately and only show inline status.
|
|
- Appearance changes should not participate in the global unsaved-changes guard.
|
|
- Full-save flow should still include font_size.
|
|
- /api/settings should accept appearance-only payloads and preserve untouched fields.
|
|
"""
|
|
import json
|
|
import re
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
from tests._pytest_port import BASE
|
|
|
|
|
|
BOOT_JS = (Path(__file__).parent.parent / "static" / "boot.js").read_text(encoding="utf-8")
|
|
PANELS_JS = (Path(__file__).parent.parent / "static" / "panels.js").read_text(encoding="utf-8")
|
|
INDEX_HTML = (Path(__file__).parent.parent / "static" / "index.html").read_text(encoding="utf-8")
|
|
I18N_JS = (Path(__file__).parent.parent / "static" / "i18n.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def _function_block(src: str, name: str) -> str:
|
|
marker = re.search(rf"(^|\n)(?:async\s+)?function\s+{re.escape(name)}\(", src)
|
|
assert marker is not None, f"{name}() not found"
|
|
start = marker.start()
|
|
next_marker = re.search(r"\n(?:function\s+\w+\(|async\s+function\s+\w+\()", src[start + 1 :])
|
|
if next_marker:
|
|
end = start + 1 + next_marker.start()
|
|
else:
|
|
end = len(src)
|
|
return src[start:end]
|
|
|
|
|
|
def _post(path, body):
|
|
data = json.dumps(body).encode("utf-8")
|
|
req = urllib.request.Request(
|
|
BASE + path,
|
|
data=data,
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=10) as r:
|
|
return json.loads(r.read()), r.status
|
|
except urllib.error.HTTPError as e:
|
|
payload = e.read()
|
|
return json.loads(payload or b"{}"), e.code
|
|
|
|
|
|
def _get(path):
|
|
with urllib.request.urlopen(BASE + path, timeout=10) as r:
|
|
return json.loads(r.read()), r.status
|
|
|
|
|
|
def test_appearance_pickers_schedule_autosave_and_do_not_mark_dirty():
|
|
for fn in ("_pickTheme", "_pickSkin", "_pickFontSize"):
|
|
block = _function_block(BOOT_JS, fn)
|
|
assert "_scheduleAppearanceAutosave()" in block, (
|
|
f"{fn}() should invoke _scheduleAppearanceAutosave()"
|
|
)
|
|
assert "_markSettingsDirty()" not in block, (
|
|
f"{fn}() should not call _markSettingsDirty()"
|
|
)
|
|
|
|
|
|
def test_appearance_revert_preview_no_longer_rolls_back_theme_skin_font():
|
|
block = _function_block(PANELS_JS, "_revertSettingsPreview")
|
|
assert "hermes-theme" not in block
|
|
assert "hermes-skin" not in block
|
|
assert "hermes-font-size" not in block
|
|
assert "_markSettingsDirty()" not in block
|
|
|
|
|
|
def test_appearance_autosave_payload_is_theme_skin_font_only():
|
|
block = _function_block(PANELS_JS, "_appearancePayloadFromUi")
|
|
assert "theme:" in block
|
|
assert "skin:" in block
|
|
assert "font_size:" in block
|
|
assert "language:" not in block
|
|
assert "workspace" not in block
|
|
assert "show_token_usage" not in block
|
|
status_block = _function_block(PANELS_JS, "_setAppearanceAutosaveStatus")
|
|
assert "settings_autosave_saving" in status_block
|
|
assert "settings_autosave_saved" in status_block
|
|
assert "settings_autosave_failed" in status_block
|
|
assert "settings_autosave_retry" in status_block
|
|
|
|
|
|
def test_appearance_autosave_status_line_and_i18n_keys_exist():
|
|
assert 'id="settingsAppearanceAutosaveStatus"' in INDEX_HTML
|
|
required_keys = [
|
|
"settings_autosave_saving",
|
|
"settings_autosave_saved",
|
|
"settings_autosave_failed",
|
|
"settings_autosave_retry",
|
|
]
|
|
for key in required_keys:
|
|
assert I18N_JS.count(f"{key}:") >= 8, (
|
|
f"{key} must be defined in all LOCALES blocks (found {I18N_JS.count(f'{key}:')})"
|
|
)
|
|
|
|
|
|
def test_full_save_settings_still_includes_font_size():
|
|
block = _function_block(PANELS_JS, "saveSettings")
|
|
compact = block.replace(" ", "")
|
|
assert "body.theme=theme;" in compact
|
|
assert "body.skin=skin;" in compact
|
|
assert "body.font_size=fontSize;" in compact
|
|
|
|
|
|
def test_settings_api_accepts_appearance_only_payload_without_overwriting_other_fields():
|
|
original, status = _get("/api/settings")
|
|
assert status == 200
|
|
snapshot = {
|
|
"theme": original.get("theme"),
|
|
"skin": original.get("skin"),
|
|
"font_size": original.get("font_size", "default"),
|
|
"show_token_usage": original.get("show_token_usage"),
|
|
"show_cli_sessions": original.get("show_cli_sessions"),
|
|
"check_for_updates": original.get("check_for_updates"),
|
|
}
|
|
try:
|
|
d, status = _post("/api/settings", {"theme": "system", "skin": "charizard", "font_size": "large"})
|
|
assert status == 200
|
|
assert d.get("theme") == "system"
|
|
assert d.get("skin") == "charizard"
|
|
assert d.get("font_size") == "large"
|
|
reloaded, _ = _get("/api/settings")
|
|
assert reloaded.get("show_token_usage") == snapshot["show_token_usage"]
|
|
assert reloaded.get("show_cli_sessions") == snapshot["show_cli_sessions"]
|
|
assert reloaded.get("check_for_updates") == snapshot["check_for_updates"]
|
|
finally:
|
|
_post("/api/settings", {
|
|
"theme": snapshot["theme"],
|
|
"skin": snapshot["skin"],
|
|
"font_size": snapshot["font_size"],
|
|
})
|