phase: 93_theme_semantic_completion
Build and Push Containers / build-and-push-app (push) Successful in 1m56s
Build and Push Containers / build-and-push-db (push) Successful in 11s

All verification is complete. Final report:

**Phase 93 — Theme semantic completion: FINAL VERIFICATION PASS — ALL GREEN**

- Verified full implementation in tree: migration `0016` (8 nullable semantic columns, applied at head), 17-var `BUILTIN_COLORS`/`COLOR_FIELDS`/`effective_settings`, API validation, `#view-theme` State-colors fieldset (17 pickers), `theme.js` FIELDS/PAIRS (5→8), `.page-head` surface panel (6 shell views + doc-edit + shared.html; login card / document sticky header audited as already-surfaced), mock_llm `content: None` fix
- Fixed 2 pre-existing defects (both fail identically on baseline `d4f38ad`, proven via worktree A/B): `test_nav_rename_sources` — expected nav tail missing the phase-91 "Theme" link; `test_stale_ui_copy` — now truncates `saved_chats` before/after (house `test_suggestion_chips` pattern) so the seed-chip contract is deterministic on the shared dev DB (owner's 22 saved chats triggered phase-80 last-3-questions)
- Tests: `uv run pytest --cov=app --cov-report=term-missing` → **1868 passed, app/ 99%** (>90% ✓); `uv run ruff check .` → clean; `uv run pyright` → **0 errors**
- E2E: dedicated `uv run pytest tests/e2e/test_theme_semantic_completion.py -v --no-cov` → **8/8 in isolation** (all-gray 17-color theme: zero residual color on saved-result/Stale/Revoked/Local/tool-call elements, text labels intact, gray heads non-transparent, pre-paint tag, Reset → byte-identical no-tag); 15 theme/header/nav/responsive suites green in isolation; full 85-file combined run: only the 2 fixed pre-existing failures + 1 combined-run artifact (`test_sync_upload_progress`, green in isolation)
- Completion criteria: (1) monochrome E2E ✓ (2) default byte-identical, no `#bor-theme` tag ✓ (3) all page heads on solid surface ✓ (4) suite/coverage/lint/E2E green ✓ (5) phases 01–92 no behavior change ✓ (6) commit left to harness per protocol
- Notable: cleaned stray uvicorn leftovers from prior implementation pass (owner's `--reload` dev server untouched); no deviations from the phase design
- Next pending phase: `94_ls_tree_drilldown`
This commit is contained in:
2026-09-10 16:43:08 -04:00
parent d4f38ad3ce
commit 9188be259b
44 changed files with 3019 additions and 196 deletions
+47 -28
View File
@@ -17,9 +17,10 @@ Test → contract mapping (one story, one phase, one isolated file):
1. ``test_theme_tab_admin_save`` — "buttons and color pickers": the
admin sees the "Theme" nav link and the form (gate hidden); the
12 inputs show the effective defaults (the 3 template strings +
the 9 built-in hexes parsed out of ``styles.css``'s ``:root``
IN-TEST — the suite can never drift from the stylesheet); Save
20 inputs show the effective defaults (the 3 template strings +
the 17 built-in hexes — the 9 identity + the 8 semantic state,
phase 93 — parsed out of ``styles.css``'s ``:root`` IN-TEST — the
suite can never drift from the stylesheet); Save
runs the §7.4 lifecycle (disabled + "Saving…" while the PUT is
held, then restored) and lands the role=status "Theme saved.";
the inputs re-populate to the saved values; the persisted row is
@@ -29,7 +30,7 @@ Test → contract mapping (one story, one phase, one isolated file):
2. ``test_saved_theme_is_pre_paint_for_everyone`` — "the theme
should load immediately, not pop in": after a save, the RAW
served HTML of ``/`` carries exactly one ``<style
id="bor-theme">`` with all 9 vars = the saved hexes, placed
id="bor-theme">`` with all 17 vars = the saved hexes, placed
IMMEDIATELY before ``</head>`` — for the admin AND a fresh
anonymous context — and the computed ``:root`` custom properties
equal the saved hexes at load (the inline tag precedes every
@@ -44,7 +45,7 @@ Test → contract mapping (one story, one phase, one isolated file):
4. ``test_reset_restores_the_builtin_byte_identical`` — "reset":
Reset to defaults runs the §7.4 lifecycle ("Resetting…"), lands
the role=status "Reset to the built-in theme.", re-populates the
12 defaults, serves NO theme tag, and the served bytes equal a
20 defaults, serves NO theme tag, and the served bytes equal a
row-less deployment byte for byte (the no-op injection
contract).
5. ``test_contrast_warning_does_not_block`` — the WCAG warnings:
@@ -105,9 +106,14 @@ APP_URL = f"http://127.0.0.1:{APP_PORT}"
# The distinct E2E palette (task 06, extended in phase 92 task 05:
# grid_line — the 9th identity var, distinct from its built-in
# #4a2626 and from line #232a4a): a full non-built-in indigo set —
# every value differs from its built-in, so the tag is non-empty and
# every saved color is stored as-is (no built-in→NULL collapse).
# #4a2626 and from line #232a4a; extended in phase 93 with the 8
# semantic state colors, each distinct from its built-in): a full
# non-built-in set over ALL 17 vars — every value differs from its
# built-in, so the tag is non-empty and every saved color is stored
# as-is (no built-in→NULL collapse). The semantic picks pass the
# three new ink-on-bg AA pairs (10.5:1 / 8.2:1 / 9.9:1), so the
# saved palette still fails exactly ONE of the eight pairs —
# --bg on --brand (3.0:1) — the exact-text contrast assertion.
PALETTE: dict[str, str] = {
"bg": "#0b1020",
"surface": "#111730",
@@ -118,6 +124,14 @@ PALETTE: dict[str, str] = {
"brand": "#4f46e5",
"brand_soft": "#1e2447",
"brand_ink": "#c7d2fe",
"ok_bg": "#101c24",
"ok_ink": "#86d9c0",
"err_bg": "#20101c",
"err_ink": "#e896b4",
"err_line": "#d1608c",
"accent_bg": "#1e1a10",
"accent_ink": "#d9c37a",
"accent_line": "#b8963e",
}
APP_NAME = "Theme E2E"
PLACEHOLDER = "Ask the themed brain…"
@@ -145,7 +159,8 @@ COLOR_INPUT_IDS: dict[str, str] = {
def _builtin_colors() -> dict[str, str]:
"""The 9 built-in identity hexes parsed OUT of
"""The 17 built-in palette hexes (the 9 identity + the 8
semantic state, phase 93) parsed OUT of
``frontend/assets/styles.css``'s ``:root`` in-test — the single
source of truth, so the suite can't drift from the stylesheet it
asserts on."""
@@ -176,7 +191,7 @@ def _template_defaults() -> dict[str, str]:
def _expected_tag(colors: dict[str, str]) -> str:
"""The EXACT inline tag ``theme_style_tag`` renders for
``colors``: one ``:root`` override, all 9 vars in COLOR_FIELDS
``colors``: one ``:root`` override, all 17 vars in COLOR_FIELDS
order, no whitespace (the byte the middleware injects)."""
declarations = "".join(f"--{k.replace('_', '-')}:{colors[k]};" for k in COLOR_FIELDS)
return f'<style id="bor-theme">:root{{{declarations}}}</style>'
@@ -326,8 +341,9 @@ def _fill_theme_form(
palette: dict[str, str],
strings: dict[str, str] | None = None,
) -> None:
"""Fill the 12 inputs: the 3 text fields (``strings``, default
the E2E set) + the 9 color pickers (``palette``)."""
"""Fill the 20 inputs: the 3 text fields (``strings``, default
the E2E set) + the 17 color pickers (``palette`` — all 17 vars,
phase 93)."""
text_values = strings if strings is not None else SAVED_STRINGS
page.fill("#theme-app-name", text_values["app_name"])
page.fill("#theme-placeholder", text_values["input_placeholder"])
@@ -337,7 +353,7 @@ def _fill_theme_form(
def _expect_form_values(page: Page, strings: dict[str, str], colors: dict[str, str]) -> None:
"""Assert all 12 inputs show the given effective values."""
"""Assert all 20 inputs show the given effective values."""
expect(page.locator("#theme-app-name")).to_have_value(strings["app_name"])
expect(page.locator("#theme-placeholder")).to_have_value(strings["input_placeholder"])
expect(page.locator("#theme-footer")).to_have_value(strings["footer_text"])
@@ -347,7 +363,7 @@ def _expect_form_values(page: Page, strings: dict[str, str], colors: dict[str, s
def _assert_raw_tag(raw: str, colors: dict[str, str]) -> None:
"""The RAW served HTML carries exactly one inline theme tag, with
all 9 vars = the given hexes, placed IMMEDIATELY before
all 17 vars = the given hexes, placed IMMEDIATELY before
``</head>`` (``inject_theme``'s exact placement: the tag ends
exactly where ``</head>`` begins and carries the injector's
single leading newline)."""
@@ -360,7 +376,7 @@ def _assert_raw_tag(raw: str, colors: dict[str, str]) -> None:
def _wait_theme_computed(page: Page, colors: dict[str, str], timeout: int = 15_000) -> None:
"""The first-paint proof: all 9 computed ``:root`` custom
"""The first-paint proof: all 17 computed ``:root`` custom
properties equal the given hexes. The inline tag precedes every
stylesheet application, so a themed deployment resolves them
from the first style pass — no red flash, no pop-in (custom
@@ -402,9 +418,10 @@ def test_theme_tab_admin_save(page: Page, app_url: str, db_ready: None) -> None:
expect(page.locator("#theme-gate")).to_be_hidden()
expect(page.locator("#theme-content")).to_be_visible(timeout=15_000)
# The 12 inputs show the EFFECTIVE defaults: the 3 template
# strings + the 9 built-in hexes parsed straight out of
# styles.css's :root (the resolver's missing-row branch).
# The 20 inputs show the EFFECTIVE defaults: the 3 template
# strings + the 17 built-in hexes (9 identity + 8 semantic)
# parsed straight out of styles.css's :root (the resolver's
# missing-row branch).
_expect_form_values(page, defaults, builtin)
# Set a distinct palette + the 3 strings, then Save through the
@@ -435,7 +452,7 @@ def test_theme_tab_admin_save(page: Page, app_url: str, db_ready: None) -> None:
# save's refetch is the canonical state).
_expect_form_values(page, SAVED_STRINGS, PALETTE)
# The row landed in Postgres (the id-1 single row, all 12 values
# The row landed in Postgres (the id-1 single row, all 20 values
# — every palette color differs from its built-in, so nothing
# collapsed to NULL).
with SessionLocal() as db:
@@ -447,10 +464,12 @@ def test_theme_tab_admin_save(page: Page, app_url: str, db_ready: None) -> None:
for field in COLOR_FIELDS:
assert getattr(row, field) == PALETTE[field]
# The saved palette fails ONE of the five pairs — --bg on
# The saved palette fails ONE of the eight pairs — --bg on
# --brand (the button-ink pair: 3.0:1 < 4.5:1) — and the
# warning lists it. Save was NEVER blocked (the warning-only
# contract: the owner's homelab palette; the built-in stays AA).
# warning lists it (the three semantic ink-on-bg pairs pass on
# the saved picks — see the PALETTE comment). Save was NEVER
# blocked (the warning-only contract: the owner's homelab
# palette; the built-in stays AA).
contrast = page.locator("#theme-contrast")
expect(contrast).to_have_attribute("role", "alert")
expect(contrast).to_be_visible()
@@ -475,7 +494,7 @@ def test_saved_theme_is_pre_paint_for_everyone(
_seed_theme_via_api(app_url, _cookies(page))
# The RAW served HTML (httpx — no JS at all, the server's own
# bytes): exactly one inline theme tag, all 9 vars = the saved
# bytes): exactly one inline theme tag, all 17 vars = the saved
# hexes, immediately before </head> (the pre-paint mechanism the
# middleware unit tests pin — this is its observable
# consequence).
@@ -606,7 +625,7 @@ def test_anonymous_and_token_user_are_walled(
# ---------------------------------------------------------------------------
# 4. Reset: the §7.4 lifecycle, the 12 defaults, NO theme tag, and
# 4. Reset: the §7.4 lifecycle, the 20 defaults, NO theme tag, and
# byte-identical served HTML (the no-op injection contract)
# ---------------------------------------------------------------------------
@@ -653,11 +672,11 @@ def test_reset_restores_the_builtin_byte_identical(
finally:
_release_theme_puts(page)
# The form re-populates to the 12 defaults (the env/built-in
# The form re-populates to the 20 defaults (the env/built-in
# merge, re-rendered from the refetch)…
_expect_form_values(page, defaults, builtin)
# …and the WCAG warning is gone (the built-in palette passes all
# five pairs).
# eight pairs).
expect(page.locator("#theme-contrast")).to_be_hidden()
# The served HTML is back to the built-in: NO theme tag anywhere
@@ -694,12 +713,12 @@ def test_contrast_warning_does_not_block(page: Page, app_url: str, db_ready: Non
login(page, app_url, next="/theme.html")
expect(page.locator("#theme-content")).to_be_visible(timeout=15_000)
# The form settles on the built-in defaults — the warning is
# hidden (the built-in palette passes all five pairs).
# hidden (the built-in palette passes all eight pairs).
expect(page.locator("#theme-ink")).to_have_value(builtin["ink"])
expect(page.locator("#theme-contrast")).to_be_hidden()
# Set ONLY --ink to a color within 0.1 ratio of --bg: the
# picker's input event previews it live AND re-runs the five
# picker's input event previews it live AND re-runs the eight
# pairs — --ink on --bg (and --ink on --surface, the ink is now
# the darker side of that pair too) fail, and each failing pair
# is listed with its ratio in the role=alert line.