feat(web): customizable placeholder, footer text, and color theme via BOR_* env vars
BOR_INPUT_PLACEHOLDER / BOR_FOOTER_TEXT / BOR_THEME (+ the indigo.css example theme); authoring guide: frontend/assets/themes/README.md, docs: README 'Customizing the look'.
This commit is contained in:
@@ -397,3 +397,77 @@ def test_docs_branchs_garbage_ignored_when_repo_unset(
|
||||
s = _settings()
|
||||
assert s.docs_configured is False
|
||||
assert s.docs_branch == "bor docs.." # stored verbatim, never used
|
||||
|
||||
|
||||
# --- UI customization (phase 62, TODO L3) ---
|
||||
|
||||
|
||||
def test_ui_customization_defaults_are_the_phase_61_copy() -> None:
|
||||
"""UNSET => byte-identical to the phase-61 neutral UI: the locked
|
||||
phase-61 copy is the DEFAULT (composer placeholder + footer line),
|
||||
and an empty theme = the built-in dark-tech palette."""
|
||||
s = _settings()
|
||||
assert s.input_placeholder == "Ask me anything…"
|
||||
assert s.footer_text == "Powered by self-hosted models"
|
||||
assert s.theme == ""
|
||||
|
||||
|
||||
def test_ui_customization_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""The three settings honor their ``BOR_`` env vars
|
||||
(``BOR_INPUT_PLACEHOLDER`` / ``BOR_FOOTER_TEXT`` / ``BOR_THEME``);
|
||||
placeholder/footer accept any string (empty is legal — the brand
|
||||
layer then keeps the template default)."""
|
||||
monkeypatch.setenv("BOR_INPUT_PLACEHOLDER", "Ask the vault…")
|
||||
monkeypatch.setenv("BOR_FOOTER_TEXT", "Powered by my own models")
|
||||
monkeypatch.setenv("BOR_THEME", "indigo.css")
|
||||
s = _settings()
|
||||
assert s.input_placeholder == "Ask the vault…"
|
||||
assert s.footer_text == "Powered by my own models"
|
||||
assert s.theme == "indigo.css"
|
||||
monkeypatch.setenv("BOR_INPUT_PLACEHOLDER", "")
|
||||
assert _settings().input_placeholder == "" # empty stands
|
||||
|
||||
|
||||
def test_theme_validator_accepts_empty_and_bare_css_filename() -> None:
|
||||
"""Phase 62 (A5): empty = the built-in palette; a bare lowercase
|
||||
``.css`` filename (the ``indigo.css`` example) is the only
|
||||
non-empty shape — dashes/underscores/digits are legal tokens."""
|
||||
assert _settings().theme == "" # "" passes
|
||||
assert _settings(theme="indigo.css").theme == "indigo.css"
|
||||
assert _settings(theme="dark-2026_v2.css").theme == "dark-2026_v2.css"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("bad", "match"),
|
||||
[
|
||||
# uppercase — the shape is lowercase-only
|
||||
("Indigo.css", "Indigo.css"),
|
||||
# path escape — a theme is a filename, never a path
|
||||
("../evil.css", r"\.\./evil\.css"),
|
||||
("a/b.css", r"a/b\.css"),
|
||||
("/abs.css", r"got '/abs\.css'"),
|
||||
# a missing extension is not a theme file
|
||||
("indigo", r"got 'indigo'"), # must not match the example text
|
||||
],
|
||||
)
|
||||
def test_theme_validator_rejects_malformed_naming_the_value(
|
||||
bad: str,
|
||||
match: str,
|
||||
) -> None:
|
||||
"""A typo in ``BOR_THEME`` must kill startup, not silently 404 at
|
||||
runtime — the rejection names the offending value (the phase-56
|
||||
fail-loud house style) alongside the allowed shape."""
|
||||
with pytest.raises(ValidationError, match=match):
|
||||
_settings(theme=bad)
|
||||
|
||||
|
||||
def test_bor_theme_env_malformed_fails_startup_naming_value(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The startup path: a malformed ``BOR_THEME`` in the environment
|
||||
fails Settings construction loudly (the app builds its settings at
|
||||
import time, so this is a refused boot), naming the value — the
|
||||
E2E boots-check lands in task 05."""
|
||||
monkeypatch.setenv("BOR_THEME", "../evil.css")
|
||||
with pytest.raises(ValidationError, match=r"\.\./evil\.css"):
|
||||
_settings()
|
||||
|
||||
Reference in New Issue
Block a user