diff --git a/static/index.html b/static/index.html index 8f268382..1f55169a 100644 --- a/static/index.html +++ b/static/index.html @@ -17,7 +17,7 @@ - + diff --git a/tests/test_6808_appearance_bootstrap_no_fabricated_choice.py b/tests/test_6808_appearance_bootstrap_no_fabricated_choice.py new file mode 100644 index 00000000..fd34ef5a --- /dev/null +++ b/tests/test_6808_appearance_bootstrap_no_fabricated_choice.py @@ -0,0 +1,188 @@ +"""Regression checks for #6808: the boot script must not fabricate a theme choice. + +`syncSettings` in static/boot.js deliberately lets the server win on a first +visit, because (its own comment) "empty (new-browser) state is indistinguishable +from a user who chose the defaults". The inline bootstrap in static/index.html +used to defeat that: it resolved a theme with +`localStorage.getItem('hermes-theme')||'dark'` and then wrote the result back +unconditionally, so a brand-new browser stored `hermes-theme=dark` before any +request was made. syncSettings then saw an "explicit" value, ignored the +server's SETTINGS_DEFAULTS, and POSTed the fabricated value back — making the +server-side appearance default unreachable for deployments that change it. + +These cases pin the distinction the fix introduces, which nothing covered +before: ABSENT appearance state is not the same as EXPLICIT appearance state. + + - both keys absent -> no setItem at all (and the dark pre-paint is UNCHANGED) + - either key present -> both values are normalised and persisted + - legacy names -> migration still happens, exactly as before + +The third is the reason the guard is on the PAIR rather than per key. The write +is not pointless: it canonicalises legacy names (`solarized` -> dark+poseidon). +With `hermes-theme=solarized` and no `hermes-skin`, a per-key guard would never +write the derived skin and the mapping would be lost on the next load. +""" +import json +import re +import shutil +import subprocess +import tempfile +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parent.parent +INDEX_HTML = ROOT / "static" / "index.html" +NODE = shutil.which("node") + +pytestmark = pytest.mark.skipif( + NODE is None, + reason="node is required to execute the appearance bootstrap harness", +) + + +def _bootstrap_script() -> str: + """The inline appearance bootstrap, lifted out of index.html.""" + html = INDEX_HTML.read_text(encoding="utf-8") + for m in re.finditer(r"", html, re.S): + body = m.group(1) + if "hermes-theme" in body and "legacy" in body and "skins" in body: + return body + raise AssertionError("appearance bootstrap