phase: 190_app_name_settings_fix
Build and Push Containers / build-and-push-app (push) Successful in 2m9s
Build and Push Containers / build-and-push-db (push) Successful in 15s

All gates re-verified green. Report:

- **Verified (all 4 tasks already complete; this was the final pass):** `effective_settings` resolves the 3 brand strings panel-row → tab-row → env (`applied_override_keys`, presence-not-value); `PUT /api/ui-settings` ignores string body fields; Theme-tab form is colors-only (17 pickers, no `#theme-app-name`/`#theme-placeholder`/`#theme-footer`); the ONE shared `applyBrandFromConfig` re-applies title/`.brand-text`/footer/placeholder after a panel save-reset and a tab save-reset; boot path byte-identical (single `/api/config`).
- **Completion criteria:**
  - strings order panel→tab→env, both branches — ✅ `tests/unit/test_theming.py::test_effective_app_name_prefers_settings_panel_row_over_ui_settings_row`, `..._fall_back_to_ui_settings_row_without_panel_row`, `..._per_field_panel_then_tab_then_env`, `..._ignore_panel_rows_when_the_overlay_is_off`
  - PUT ignores string fields + no 3 inputs in the tab — ✅ `tests/integration/test_ui_settings_api.py::test_put_ignores_string_fields_in_body` (+2) and the E2E `test_theme_tab_dom_is_colors_only`
  - live apply without reload + incident regression — ✅ `tests/e2e/test_app_name_settings_fix.py` tests 1–4
  - byte-identical boot, no double-apply — ✅ `tests/unit/test_brand_live.py` markers + E2E "exactly one /api/config"
  - suite green, coverage >90%, ruff+pyright clean — ✅
  - no behavior change in prior phases — ✅ (palette, phase-168 panel, env fallback suites all green)
  - atomic commit — **left to the harness** (pipeline rule overrides per-phase committing; no git write run)
- **Gates:** `uv run pytest --cov=app --cov-report=term-missing` → **4630 passed, TOTAL 6941 16 99%**, exit 0 · `uv run pytest tests/e2e/test_app_name_settings_fix.py -v --no-cov` → **6 passed** in isolation · regression E2E (isolated, one process each, all exit 0): `test_smoke` `test_configurable_brand` `test_admin_theme_tab` `test_theme_save_and_coverage` `test_theme_semantic_completion` `test_themed_favicon` `test_dark_tech_theme` `test_admin_settings_panel` `test_admin_settings_layout` `test_settings_mobile_value_width` · `uv run ruff check .` → All checks passed · `uv run pyright` → 0 errors.
- **Documentation:** README — fixed "saving one repaints *every* open page" (only the open document repaints; other pages on next load) and a broken paragraph wrap in *Customizing the look*; AGENTS.md rule 16 — `test_chunks_hnsw_recall.py` now described accurately (index paths forced with `enable_seqscan`/`indexonlyscan`/`bitmapscan` off, unforced plan asserted only on a pristine relation); `tests/unit/test_theme_frontend.py` — two stale section headers ("20 form fields"/"20 inputs" → 17 color). No AGENTS.md settings-overlay/resolution-order clause exists, so per the phase none was added. Full env-var cross-check (`BOR_*` in `app/config.py` vs README) → 0 gaps; 0 missing file references.
- **Notable:** the working tree also carries this phase's flake hardening of the phase-188 `tests/integration/test_chunks_hnsw_recall.py` (verified legit and green). Live infra untouched; shared DB left clean (0 `app_settings`, 0 `ui_settings`). **Flagged, not changed:** `settings_overlay.applied_override_keys` lets a `ProgrammingError` (missing `app_settings`) propagate from public `GET /api/config`, where `load_overrides` deliberately fails soft — only reachable on an un-migrated (0027) DB, outside this phase's pinned A1 semantics, so left for the owner to rule on rather than silently deviating.
- **Next pending phase:** `191_source_name_labels_tags`.
This commit is contained in:
2026-10-18 02:59:19 -04:00
parent 47456a44df
commit c6ffa050a6
59 changed files with 4104 additions and 556 deletions
+219 -3
View File
@@ -29,6 +29,20 @@ 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 190 (tasks 01 + 02): the brand-string OWNERSHIP pins (A1, owner
confirmed 2026-10-18) — the Settings-panel row (``app_settings``,
phase 168) is the single writable owner of the three strings, so the
resolution order is panel row → ``ui_settings`` (Theme-tab) row →
env. Task 01 pinned the order as a strict ``xfail`` (the baked-in tab
row SHADOWED the panel — the TODO.md L3 incident); task 02 flipped
``effective_settings`` and removed the marker, so the ownership pin
(``test_effective_app_name_prefers_settings_panel_row_over_ui_settings_row``)
is GREEN here. Beside it: the no-panel-row fallback pin (the tab row
still applies — the A1 fallback a legacy baked-in row lands in, green
before AND after the flip), the PER-FIELD pin (a panel row for one
string never touches the other two), and the kill-switch pin
(``BOR_SETTINGS_DB=0`` → no row is in effect, so the tab row keeps
its phase-91 fallback).
* 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
@@ -47,12 +61,13 @@ import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any
from sqlalchemy import select
import pytest
from sqlalchemy import select, text, update
from sqlalchemy.orm import Session
from app.config import Settings
from app.config import Settings, invalidate_settings
from app.core import theming
from app.models import UiSettings
from app.models import AppSetting, UiSettings
REPO_ROOT = Path(__file__).resolve().parents[2]
STYLES_CSS = REPO_ROOT / "frontend" / "assets" / "styles.css"
@@ -72,6 +87,16 @@ def _start_row_missing(db: Session) -> None:
db.commit()
def _clear_panel_rows(db: Session) -> None:
"""Phase 190: the ``app_settings`` rows (the Settings-panel surface,
phase 168) are global state exactly like the single ``ui_settings``
row — wipe them so the ownership pins start AND end with an empty
panel (a surviving row would shadow every later suite's strings
once the overlay is enabled)."""
db.execute(text("DELETE FROM app_settings"))
db.commit()
def _root_declarations() -> dict[str, str]:
"""The ``--name: value`` declarations of styles.css's (first)
``:root`` block, comments stripped, in file order."""
@@ -133,6 +158,38 @@ def _env_settings() -> Settings:
)
def _panel_settings() -> Settings:
"""The OVERLAY-RESOLVED ``Settings`` (phase 190): the object
:func:`app.config.get_settings` hands every consumer when the
Settings panel holds ``app_name`` / ``input_placeholder`` /
``footer_text`` rows — the DB rows layered over the env base
(``_env_settings`` standing for that base). The ownership pin
pairs this explicit object with REAL committed ``app_settings``
rows so the contract holds whichever side of the overlay the
resolver consults — the panel row is the source, never the bare
env value.
"""
return Settings(
app_name="Panel Name",
input_placeholder="Panel placeholder…",
footer_text="Panel footer",
)
def _seed_panel_rows(db: Session) -> None:
"""The three Settings-panel rows, committed (production writes them
through ``PUT /api/admin/settings/{key}`` — the resolver only ever
sees the table, so seeding it directly IS the panel state)."""
db.add_all(
[
AppSetting(key="app_name", value="Panel Name"),
AppSetting(key="input_placeholder", value="Panel placeholder…"),
AppSetting(key="footer_text", value="Panel footer"),
]
)
db.commit()
# ---------- effective_settings (real Postgres — house DB-test pattern) ----------
@@ -193,6 +250,165 @@ def test_effective_empty_string_db_string_falls_back_to_env(db: Session) -> None
db.commit()
# ---------- phase 190 (task 01): the brand-string ownership (A1) ----------
def test_effective_app_name_prefers_settings_panel_row_over_ui_settings_row(
db: Session, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A1 (owner-confirmed 2026-10-18): the Settings panel is the SINGLE
writable owner of the three brand strings, so an ``app_settings``
row beats a conflicting ``ui_settings`` (Theme-tab) row for EVERY
one of them — the resolution order is panel row → tab row → env.
GREEN since task 02 flipped the order (phase 190 task 01 pinned it
as a strict ``xfail`` on the defect: ``effective_settings`` used to
return the ``ui_settings`` value whenever it was a non-empty string,
so a Theme-tab save BAKED the then-effective name into the row and
every later Settings-panel change was SHADOWED — the exact
``TODO.md`` L3 incident, "changing the App name in settings broke,
it no longer updates the name").
"""
_start_row_missing(db)
_clear_panel_rows(db)
_seed_panel_rows(db)
db.add(
UiSettings(
id=1,
app_name="Tab Name",
input_placeholder="Tab placeholder…",
footer_text="Tab footer",
)
)
db.commit()
monkeypatch.setenv("BOR_SETTINGS_DB", "1") # the overlay is the panel's mechanism
try:
effective = theming.effective_settings(db, _panel_settings())
assert effective["app_name"] == "Panel Name" # pre-fix: "Tab Name" (the shadow)
assert effective["input_placeholder"] == "Panel placeholder…"
assert effective["footer_text"] == "Panel footer"
# The ownership flip is STRINGS-only — the tab row keeps the
# colors half untouched (the 17-color palette is unchanged).
assert effective["bg"] == theming.BUILTIN_COLORS["bg"]
assert effective["brand"] == theming.BUILTIN_COLORS["brand"]
finally:
_clear_panel_rows(db)
db.execute(_delete_row())
db.commit()
invalidate_settings()
def test_effective_strings_fall_back_to_ui_settings_row_without_panel_row(
db: Session, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The A1 FALLBACK (no data migration): a ``ui_settings`` row set and
NO Settings-panel row means the tab value still applies (a legacy
baked-in row keeps rendering what it holds until the panel
overrides it) — the env value stays the LAST resort.
GREEN pre-phase AND post-phase: the pin that protects the fallback
branch of the new order. An implementation that lets the plain env
``Settings`` value beat the tab row (instead of consulting the
actual ``app_settings`` rows for a real override) fails here.
"""
_start_row_missing(db)
_clear_panel_rows(db)
db.add(
UiSettings(
id=1,
app_name="Tab Name",
input_placeholder="Tab placeholder…",
footer_text="Tab footer",
)
)
db.commit()
monkeypatch.setenv("BOR_SETTINGS_DB", "1") # overlay live, panel table simply empty
try:
effective = theming.effective_settings(db, _env_settings())
assert effective["app_name"] == "Tab Name"
assert effective["input_placeholder"] == "Tab placeholder…"
assert effective["footer_text"] == "Tab footer"
finally:
db.execute(_delete_row())
db.commit()
invalidate_settings()
def test_effective_strings_resolve_per_field_panel_then_tab_then_env(
db: Session, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The order is PER FIELD, not per row (phase 190 task 02): one
Settings-panel row owns exactly its own key, so ``app_name`` reads
the panel while ``input_placeholder`` falls to the tab row and
``footer_text`` — absent from BOTH rows — falls to the env value.
The panel row that equals the env value still wins, too: presence,
never a value compare (a compare is how the shadow would sneak
back — an owner who sets the panel name to what the env already
says would see a legacy tab bake instead)."""
_start_row_missing(db)
_clear_panel_rows(db)
db.add(AppSetting(key="app_name", value="Panel Name"))
db.add(UiSettings(id=1, app_name="Tab Name", input_placeholder="Tab placeholder…"))
db.commit()
monkeypatch.setenv("BOR_SETTINGS_DB", "1")
env = _env_settings()
try:
effective = theming.effective_settings(
db,
Settings(
app_name="Panel Name",
input_placeholder=env.input_placeholder,
footer_text=env.footer_text,
),
)
assert effective["app_name"] == "Panel Name" # the panel row (tab row ignored)
assert effective["input_placeholder"] == "Tab placeholder…" # no panel row → tab
assert effective["footer_text"] == env.footer_text # neither row → env
# A panel row that EQUALS the env value still owns the field —
# the tab row must not resurrect itself on a value tie.
db.execute(
update(AppSetting).where(AppSetting.key == "app_name").values(
value=env.app_name
)
)
db.commit()
tied = theming.effective_settings(db, Settings(app_name=env.app_name))
assert tied["app_name"] == env.app_name
assert tied["input_placeholder"] == "Tab placeholder…"
finally:
_clear_panel_rows(db)
db.execute(_delete_row())
db.commit()
invalidate_settings()
def test_effective_strings_ignore_panel_rows_when_the_overlay_is_off(
db: Session, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The ``BOR_SETTINGS_DB`` kill switch (phase 168 D6) governs the
ownership too: with the overlay off NO row is in effect (so
``settings.app_name`` is the plain env value), and the Theme-tab row
keeps its phase-91 fallback rather than being shadowed by a row the
resolver never applies."""
_start_row_missing(db)
_clear_panel_rows(db)
_seed_panel_rows(db)
db.add(UiSettings(id=1, app_name="Tab Name"))
db.commit()
monkeypatch.setenv("BOR_SETTINGS_DB", "0")
try:
effective = theming.effective_settings(db, _env_settings())
assert effective["app_name"] == "Tab Name" # no override in effect → the tab row
assert effective["input_placeholder"] == _env_settings().input_placeholder
assert effective["footer_text"] == _env_settings().footer_text
finally:
monkeypatch.setenv("BOR_SETTINGS_DB", "1")
_clear_panel_rows(db)
db.execute(_delete_row())
db.commit()
invalidate_settings()
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