"""Unit: the built-in identity palette + the effective-settings resolver (phase 91, task 01). Covers ``app/core/theming.py`` — the single source of the built-in identity palette (re-homed from the retired phase-62 CSS-file themes' authoring guide before task 03 deleted it) and the DB-over-env / DB-over-built-in resolver shared by ``/api/ui-settings`` and ``/api/config``: * ``BUILTIN_COLORS`` — the DRIFT GUARD: the 8 built-ins must equal the values parsed straight out of ``frontend/assets/styles.css``'s ``:root`` block, so the Python palette and the stylesheet can never silently diverge; * ``theme_style_tag`` — the byte-identical contract (all built-in → ``""``) and the exact tag shape (all 8 variables, ``COLOR_FIELDS`` order, lowercased hex); * ``effective_settings`` — missing row → env strings + built-ins; a DB row's set columns win; an empty-string DB string falls back to env (the resolver treats "" as unset, B1). House DB-test pattern (the ``test_tokens`` precedent): the real compose Postgres, skipped with clear instructions when the stack is not up. """ from __future__ import annotations import base64 import hashlib import re from pathlib import Path from typing import Any from sqlalchemy import select from sqlalchemy.orm import Session from app.config import Settings from app.core import theming from app.models import UiSettings REPO_ROOT = Path(__file__).resolve().parents[2] STYLES_CSS = REPO_ROOT / "frontend" / "assets" / "styles.css" def _delete_row() -> Any: from sqlalchemy import delete return delete(UiSettings).where(UiSettings.id == 1) def _root_declarations() -> dict[str, str]: """The ``--name: value`` declarations of styles.css's (first) ``:root`` block, comments stripped, in file order.""" css = STYLES_CSS.read_text(encoding="utf-8") match = re.search(r":root\s*\{", css) assert match is not None, "styles.css must have a :root block" block = css[match.end() : css.index("}", match.end())] block = re.sub(r"/\*.*?\*/", "", block, flags=re.S) return dict(re.findall(r"(--[a-z-]+)\s*:\s*([^;]+);", block)) def test_builtin_colors_match_styles_css_root() -> None: """The drift guard: every built-in equals the stylesheet's ``:root`` value for the same variable (and ``BUILTIN_COLORS`` names exactly the 8 identity variables — no more, no fewer).""" decls = _root_declarations() builtin_names = set(theming.BUILTIN_COLORS) assert builtin_names == { "bg", "surface", "ink", "ink_soft", "line", "brand", "brand_soft", "brand_ink", }, f"BUILTIN_COLORS must name exactly the 8 identity variables, got {sorted(builtin_names)}" for name, value in theming.BUILTIN_COLORS.items(): css_name = f"--{name.replace('_', '-')}" assert css_name in decls, f"styles.css :root is missing {css_name}" assert decls[css_name].strip() == value, ( f"{css_name} drifted: BUILTIN_COLORS has {value!r}, " f"styles.css has {decls[css_name].strip()!r}" ) def test_color_fields_are_the_eight_keys_in_readme_order() -> None: """``COLOR_FIELDS`` is the 8 keys in the themes-README order — the order the resolver, the API, and the tag renderer all rely on.""" assert theming.COLOR_FIELDS == ( "bg", "surface", "ink", "ink_soft", "line", "brand", "brand_soft", "brand_ink", ) assert theming.STRING_FIELDS == ("app_name", "input_placeholder", "footer_text") def _env_settings() -> Settings: """An explicit env source (the resolver's optional ``settings`` parameter) — deterministic values, independent of the local ``.env`` (the ``/api/config`` env pins in test_api.py own the env-file behaviour; this unit module only needs stable fallbacks).""" return Settings( app_name="Env Name", input_placeholder="Env placeholder…", footer_text="Env footer", ) # ---------- effective_settings (real Postgres — house DB-test pattern) ---------- def test_effective_missing_row_is_env_strings_plus_builtins(db: Session) -> None: """A missing row (GET creates nothing) means "defaults": the env strings + the built-in palette, all 11 keys.""" row = db.execute(select(UiSettings).where(UiSettings.id == 1)).scalars().first() assert row is None, "the test starts from a row-missing state" effective = theming.effective_settings(db, _env_settings()) assert set(effective) == set(theming.STRING_FIELDS) | set(theming.COLOR_FIELDS) assert effective["app_name"] == "Env Name" assert effective["input_placeholder"] == "Env placeholder…" assert effective["footer_text"] == "Env footer" assert {k: effective[k] for k in theming.COLOR_FIELDS} == theming.BUILTIN_COLORS def test_effective_db_row_wins_column_by_column(db: Session) -> None: """Set columns win, unset columns fall back — per column, so a partial row (only ``bg`` set) mixes the DB color with the built-ins and the env strings.""" db.add(UiSettings(id=1, bg="#111111", app_name="DB Name")) db.commit() try: effective = theming.effective_settings(db, _env_settings()) assert effective["bg"] == "#111111" # DB wins assert effective["app_name"] == "DB Name" # DB wins # Unset columns: env strings + the built-in colors. assert effective["input_placeholder"] == "Env placeholder…" assert effective["footer_text"] == "Env footer" for key in theming.COLOR_FIELDS: if key != "bg": assert effective[key] == theming.BUILTIN_COLORS[key] finally: db.execute(_delete_row()) db.commit() def test_effective_empty_string_db_string_falls_back_to_env(db: Session) -> None: """B1: an EMPTY string in the DB is unset — the resolver falls back to the env value (a hand-edited row with '' can't blank the UI). Colors: ``None`` → the built-in (an empty color is impossible through the API — the hex validator — the resolver's not-None rule covers the hand-edited edge by returning whatever the row holds).""" db.add(UiSettings(id=1, app_name="")) db.commit() try: effective = theming.effective_settings(db, _env_settings()) assert effective["app_name"] == "Env Name" # "" → env fallback assert effective["footer_text"] == "Env footer" assert effective["brand"] == theming.BUILTIN_COLORS["brand"] # None → built-in finally: db.execute(_delete_row()) db.commit() def test_effective_without_explicit_settings_uses_get_settings(db: Session) -> None: """``settings=None`` (the design's call shape) resolves the env fallback from the cached :func:`app.config.get_settings` — the values it reports must be real ``str``s for all 11 keys.""" from app.config import get_settings effective = theming.effective_settings(db) assert set(effective) == set(theming.STRING_FIELDS) | set(theming.COLOR_FIELDS) assert effective["app_name"] == get_settings().app_name for field in theming.STRING_FIELDS: assert isinstance(effective[field], str) and effective[field] assert all(re.fullmatch(r"#[0-9a-f]{6}", effective[k]) for k in theming.COLOR_FIELDS) assert {k: effective[k] for k in theming.COLOR_FIELDS} == theming.BUILTIN_COLORS # ---------- theme_style_tag (pure) ---------- def test_theme_style_tag_all_builtins_is_empty_string() -> None: """The byte-identical contract: an unset (or "defaults saved") deployment serves NO tag — exactly the pre-phase-91 HTML.""" assert theming.theme_style_tag(dict(theming.BUILTIN_COLORS)) == "" # The tag is PURE string-equality: uppercase hex is NOT the built-in # (the API's lowercasing-before-store is what makes stored hex # canonical — pin the renderer's own contract here). colors = {k: v.upper() for k, v in theming.BUILTIN_COLORS.items()} assert theming.theme_style_tag(colors) != "" def test_theme_style_tag_one_changed_carries_all_eight_in_order() -> None: """A single non-built-in color still emits ALL 8 variables, in ``COLOR_FIELDS`` order, with the exact tag shape (no whitespace).""" colors = dict(theming.BUILTIN_COLORS) colors["brand"] = "#818cf8" tag = theming.theme_style_tag(colors) assert tag == ( '" ) # The changed value lands under the dashed CSS name… assert "--brand:#818cf8;" in tag # …and the underscored field (ink_soft) renders as --ink-soft. assert "--ink-soft:#b8a8a8;" in tag assert "--ink_soft" not in tag def test_theme_style_tag_multiple_changed() -> None: """Two changed colors: both values present, the rest built-in, order unchanged (the tag is a complete :root override — the page never mixes a partial palette).""" colors = dict(theming.BUILTIN_COLORS) colors["bg"] = "#0a0e1a" colors["brand_ink"] = "#c7d2fe" tag = theming.theme_style_tag(colors) assert tag.startswith('") # The order of the 8 dashed names is the COLOR_FIELDS order. names = re.findall(r"--([a-z-]+):", tag) assert names == [k.replace("_", "-") for k in theming.COLOR_FIELDS] # ---------- inject_theme (pure — task 02's injection helper) ---------- _HEAD_HTML = "
b
" def test_inject_theme_empty_tag_is_identity() -> None: """``tag == ""`` (the unset / "defaults saved" deployment — what ``theme_style_tag" returns for all-built-in colors) → the html is returned EXACTLY as passed in, byte for byte (B4's no-op contract).""" assert theming.inject_theme(_HEAD_HTML, "") == _HEAD_HTML # Whitespace is NOT an empty tag — a real tag is always inserted. assert theming.inject_theme(_HEAD_HTML, " ") != _HEAD_HTML def test_inject_theme_missing_head_is_identity() -> None: """No ```` occurrence → unchanged (nothing to anchor to); the empty string (no ```` either) is identity too.""" tag = '' html = "no head" assert theming.inject_theme(html, tag) == html assert theming.inject_theme("", tag) == "" def test_inject_theme_exact_placement_before_first_head_close() -> None: """The tag lands with a leading newline immediately BEFORE the first ```` — nothing between the tag and the close, nothing moved after it.""" tag = '' html = "