phase: 177_themed_favicon
Everything is verified. Final report: ## Phase 177 — Themed favicon: completion pass (all tasks pre-done, verified end-to-end) **Verified (no code fixes needed — implementation matched the design D1–D4 exactly):** - `theming.favicon_svg()` pure renderer + golden byte-identical pin; `GET/HEAD /favicon.svg` route (no-cache, `image/svg+xml`, built-in 200 fallback on DB blip, routes-first before static mount); 5 templates → bare `href="/favicon.svg"`, static file deleted (Containerfile cp removed); Theme-tab `data:` live preview with 4 restore points; JS↔server mirror pins. **Tests / lint / coverage (all green):** - `uv run pytest --cov=app --cov-report=term-missing` → 4161 passed, `app/` 99% (>90% gate) - `uv run ruff check .` → clean; `uv run pyright` → 0 errors - `uv run pytest tests/e2e/test_themed_favicon.py -v --no-cov` (isolated) → 6 passed - `test_theme_save_and_coverage.py` (92) + `test_theme_semantic_completion.py` (93), isolated → 3 + 8 passed **Completion criteria:** all met — default golden bytes/headers; themed roles + `#bor-theme` non-regression; DB-failure 200 fallback; 5 bare links + `/assets/favicon.svg` 404 + browser rendering; live-preview data: repaint + Save/Reset/re-show restore; gates green; E2E suites green in isolation. Commit: harness commits atomically (overrides AGENTS rule 8 — no git commands run). **Documentation:** README.md verified consistent in full (the phase's favicon paragraph is accurate; all commands, env vars, routes, ports, scripts cross-checked) — no changes needed. AGENTS.md verified consistent — no changes needed. **Notable:** `image-1.png` (owner's TODO screenshot, referenced by phase 176's overview) sits untracked at the repo root — pre-existing, explicitly out of scope per the phase-176 report; left untouched. **Next pending phase:** none — 177 was the last; TODO.md is cleared.
This commit is contained in:
@@ -29,12 +29,21 @@ DB-over-built-in resolver shared by ``/api/ui-settings`` and
|
||||
ellipsis. The :root 17-variable set itself stays pinned by
|
||||
``test_builtin_colors_match_styles_css_root`` (UNCHANGED — this
|
||||
phase adds no palette variable, B3).
|
||||
* Phase 177 (task 01): ``favicon_svg`` — the pure palette→artwork
|
||||
renderer, byte-identical for the built-in palette (the golden
|
||||
constant ``FAVICON_GOLDEN`` holds the pre-phase static file's
|
||||
``<svg>`` element — the file is deleted in task 03, so the
|
||||
constant is the durable contract); the three color roles per A1
|
||||
(hexagon fill → ``surface``, hexagon stroke + center dot →
|
||||
``brand``, spokes → ``brand_ink``); the 8 semantic state colors
|
||||
are NOT in the artwork (a favicon has no state meaning).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -572,3 +581,107 @@ def test_cell_clip_ellipsis_pin() -> None:
|
||||
f".cell-clip white-space drifted: {rule.get('white-space')!r} — "
|
||||
f"the ellipsis needs the nowrap"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 177 (task 01): favicon_svg — the pure palette→artwork renderer,
|
||||
# byte-identical for the built-in palette (A1). The GOLDEN constant is
|
||||
# the durable contract: it holds the pre-phase static file's ``<svg>``
|
||||
# element (the file's comment header is file packaging, not art, and is
|
||||
# NOT part of the contract; the file itself is deleted in task 03).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: The pre-phase ``frontend/assets/favicon.svg`` ``<svg>`` element —
|
||||
#: hexagon + center dot + 4 spokes, ``viewBox 0 0 64 64``, lowercase
|
||||
#: hex, original attribute order. ``favicon_svg(BUILTIN_COLORS)`` must
|
||||
#: equal this EXACTLY (341 bytes, single line, no comment header).
|
||||
FAVICON_GOLDEN = (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">'
|
||||
'<path d="M32 4 55 18v28L32 60 9 46V18Z" fill="#1a0f0f" '
|
||||
'stroke="#f43f5e" stroke-width="4" stroke-linejoin="round"/>'
|
||||
'<circle cx="32" cy="32" r="6.5" fill="#f43f5e"/>'
|
||||
'<path d="M32 25.5V16M32 48v-9.5M25.5 32H16M48 32h-9.5" '
|
||||
'stroke="#fca5a5" stroke-width="3" stroke-linecap="round"/>'
|
||||
"</svg>"
|
||||
)
|
||||
|
||||
|
||||
def _mask_color_attrs(svg_text: str) -> str:
|
||||
"""The document with its EXACTLY FOUR color attributes (the
|
||||
``fill="`` / ``stroke="`` values) masked — the rest (geometry,
|
||||
``d`` paths, circle ``cx/cy/r``, stroke widths, ``linejoin`` /
|
||||
``linecap``, attribute order) is then comparable byte for byte.
|
||||
(``stroke-width`` / ``stroke-linejoin`` / ``stroke-linecap`` never
|
||||
match — they carry ``-`` before the ``="``.)"""
|
||||
return re.sub(r'(fill|stroke)="#[0-9a-f]{6}"', r'\1="COLOR"', svg_text)
|
||||
|
||||
|
||||
def test_favicon_svg_builtins_is_byte_identical_to_the_static_art() -> None:
|
||||
"""The byte-identical contract (D1): for the built-in palette the
|
||||
output is EXACTLY the static file's ``<svg>`` element — lowercase
|
||||
hex, same attribute order — pinned by the golden constant (the
|
||||
durable contract; the static file is deleted in task 03, so the
|
||||
constant, not the file, keeps the bytes alive)."""
|
||||
out = theming.favicon_svg(dict(theming.BUILTIN_COLORS))
|
||||
assert out == FAVICON_GOLDEN
|
||||
# Single-line element, no comment header (the file's comment is
|
||||
# file packaging, not art), exact extent.
|
||||
assert "\n" not in out
|
||||
assert out.startswith('<svg xmlns="http://www.w3.org/2000/svg"')
|
||||
assert out.endswith("</svg>")
|
||||
# The three built-in role values land in the golden's roles.
|
||||
assert 'fill="#1a0f0f"' in out # surface
|
||||
assert out.count('"#f43f5e"') == 2 # brand (hexagon stroke + circle)
|
||||
assert 'stroke="#fca5a5"' in out # brand_ink
|
||||
|
||||
|
||||
def test_favicon_svg_themed_carries_the_three_roles() -> None:
|
||||
"""A palette with distinct ``surface``/``brand``/``brand_ink``
|
||||
(plus the other 14 built-ins): the three hexes appear in their
|
||||
exact attribute roles — asserted by PARSING the SVG attributes
|
||||
(not by substring) — and none of the other 14 built-in hexes
|
||||
appears anywhere in the output."""
|
||||
colors = dict(theming.BUILTIN_COLORS)
|
||||
colors["surface"] = "#102030"
|
||||
colors["brand"] = "#4f46e5"
|
||||
colors["brand_ink"] = "#a5b4fc"
|
||||
out = theming.favicon_svg(colors)
|
||||
ns = "{http://www.w3.org/2000/svg}"
|
||||
root = ET.fromstring(out)
|
||||
assert root.tag == f"{ns}svg"
|
||||
assert root.get("viewBox") == "0 0 64 64"
|
||||
hexagon, circle, spokes = list(root)
|
||||
# Hexagon: fill ← surface, stroke ← brand (geometry unchanged).
|
||||
assert hexagon.tag == f"{ns}path"
|
||||
assert hexagon.get("d") == "M32 4 55 18v28L32 60 9 46V18Z"
|
||||
assert hexagon.get("fill") == "#102030"
|
||||
assert hexagon.get("stroke") == "#4f46e5"
|
||||
# Center dot: fill ← brand.
|
||||
assert circle.tag == f"{ns}circle"
|
||||
assert circle.get("fill") == "#4f46e5"
|
||||
# Spokes: stroke ← brand_ink.
|
||||
assert spokes.tag == f"{ns}path"
|
||||
assert spokes.get("stroke") == "#a5b4fc"
|
||||
# None of the other 14 built-in hexes appears in the output.
|
||||
for key, value in theming.BUILTIN_COLORS.items():
|
||||
if key not in ("surface", "brand", "brand_ink"):
|
||||
assert value not in out, (
|
||||
f"built-in {key} value {value} must not appear in a themed favicon"
|
||||
)
|
||||
|
||||
|
||||
def test_favicon_svg_geometry_is_invariant() -> None:
|
||||
"""Geometry invariance: with a themed palette, everything EXCEPT
|
||||
the four color attributes is byte-equal to the built-in output
|
||||
(viewBox, both path ``d``s, circle ``cx/cy/r``, stroke widths,
|
||||
``linejoin``/``linecap``, attribute order — the mask leaves only
|
||||
the four ``fill="…"`` / ``stroke="…"`` values to differ)."""
|
||||
colors = dict(theming.BUILTIN_COLORS)
|
||||
colors["surface"] = "#102030"
|
||||
colors["brand"] = "#4f46e5"
|
||||
colors["brand_ink"] = "#a5b4fc"
|
||||
themed = theming.favicon_svg(colors)
|
||||
builtin = theming.favicon_svg(dict(theming.BUILTIN_COLORS))
|
||||
assert _mask_color_attrs(themed) == _mask_color_attrs(builtin)
|
||||
# Sanity: the mask masked exactly the four color attributes.
|
||||
assert _mask_color_attrs(builtin).count('"COLOR"') == 4
|
||||
|
||||
Reference in New Issue
Block a user