Files
brain-of-reese/tests/integration/test_caching_revalidation.py
ducoterra d22d260b8b
Build and Push Containers / build-and-push-app (push) Successful in 5m43s
Build and Push Containers / build-and-push-db (push) Successful in 12s
phase: 91_admin_theme_tab
All verification is complete — this pass needed no code changes. Final report:

**Phase 91 — Admin Theme tab: final verification pass (all 6 tasks already in `complete/`)**

- Verified pre-paint theming end-to-end: `ui_settings` store + resolver, admin `GET/PUT /api/ui-settings`, `CachingMiddleware` inline-`<style id="bor-theme">` injection before `</head>` (incl. `/shared/<token>` prefix branch, unit-pinned), CSP sha256 exemption for the inline tag, Theme tab shell + `theme.js` editor, CSS-file theming fully retired.
- No defects found; zero changes made — working tree left exactly as the task executors left it.
- Tests: `uv run pytest --cov=app` → 1841 passed, 0 failed (TOTAL coverage **99%**; theming/ui_settings/caching all 100%); `uv run pytest tests/e2e/test_admin_theme_tab.py -v --no-cov` → **5 passed** in isolation.
- Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings.
- Criteria: (1) unset deployment byte-identical, no `#bor-theme` anywhere — ✓ (unit no-op test + E2E reset byte-compare); `rg "BOR_THEME|themes/"` → single hit is the permitted doc-history comment in `frontend/index.html`. (2) admin-only gate + 403s for anonymous and token users — ✓ (E2E test 3). (3) saved theme inline before `</head>` on every page incl. `/shared/<token>`, computed `--brand` on first paint for admin + anonymous — ✓ (E2E test 2 + unit). (4) reset → byte-identical; 5 contrast pairs warn <4.5:1, non-blocking — ✓ (E2E tests 4–5). (5) suite green, >90% coverage, lint clean — ✓. (6) commit deferred to harness per rules.
- Notable: `.agents/PLAN.md` is absent from the repo — the phase overview's Design section was used as the binding spec; no deviation resulted.
- Next pending phase: **none** — 91 is the last phase in `todo/`.
2026-09-09 17:22:24 -04:00

225 lines
9.4 KiB
Python

"""Integration: the phase-54 revalidation contract against the REAL app.
Phase 33's two cache-busting layers (``asset_version()`` +
``CachingMiddleware``) rewrite the known HTML pages to carry
``?v=<token>`` asset references — but the ``etag`` / ``last-modified``
validators Starlette publishes for a page describe the STATIC FILE, not
the rewritten body this process built from its own token. A conditional
GET that matched those validators used to 304 out of the rewrite: the
browser kept the HTML it already had, whose ``?v=`` pinned the PREVIOUS
commit's CSS/JS — cached ``immutable`` for a year. This suite pins the
fix end-to-end (real ``app.main:app``, real ``StaticFiles`` mount on the
real ``frontend/`` tree, real ``asset_version()`` token — no mocks):
* every known page (``HTML_PAGES``) AND the dynamic ``/shared/<token>``
page 200s on a conditional GET, always with the current
``?v=<token>`` body and no validators;
* ``/assets/*`` is untouched: ``immutable`` for a year, validators
intact, a conditional GET on the versioned URL still 304s (that 304
is safe — the URL itself carries the version);
* ``/api/*`` stays byte-identical: no ``cache-control`` injected, no
validators, conditional headers pass through (the SSE chat stream's
pass-through is pinned by ``test_chat_api.py`` — the regression run
below).
Requires: podman compose up -d db
"""
from __future__ import annotations
import os
import re
from collections.abc import Iterator
from pathlib import Path
import httpx
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.caching import (
ASSET_CACHE_CONTROL,
HTML_CACHE_CONTROL,
HTML_PAGES,
asset_version,
)
REPO = Path(__file__).resolve().parents[2]
FRONTEND = REPO / "frontend"
def _token_ref_re(token: str) -> re.Pattern[str]:
"""A local ``assets/…`` href/src reference that already carries
``?v=<token>`` (the middleware's rewrite output)."""
return re.compile(r'(?:src|href)="(?:/)?assets/[^"?#]*\?v=' + re.escape(token) + r'"')
def file_validators(page_file: Path) -> tuple[str, str]:
"""The etag / last-modified Starlette would stamp on the underlying
static file — exactly what a browser revalidates against.
``stat_result`` is passed up front: starlette 1.x's ``FileResponse``
defers the stat to ``__call__``, so without it the headers carry no
validators yet (same pattern as the unit suite's ``_file_validators``).
"""
from starlette.responses import FileResponse
headers = FileResponse(page_file, stat_result=os.stat(page_file)).headers
return headers["etag"], headers["last-modified"]
#: Phase 76 (task 01): the shell-served view paths — the URL is a
#: navbar view, the file on disk is the SHELL (the shell route in
#: app/main.py serves frontend/index.html for it). The etag
#: computation below must use the file that actually backs the
#: response, or the conditional-GET probe would carry a validator no
#: browser ever saw. Tasks 02/03 extended this as the remaining views
#: folded in (task 03 — History — completed the phase-76 set: all
#: four non-chat navbar views; phase 79 task 06 adds the sixth —
#: Tokens). The page CONTRACT itself is unchanged: the
#: phase-33 middleware wraps the whole app and lists the path in
#: HTML_PAGES, so the shell-route response is normalized exactly like
#: a static page (200, no-cache, ?v=, no validators — asserted by
#: _assert_page_contract below).
SHELL_BACKED_PAGES = {
"/tuning.html": "index.html", # phase 76 task 01
"/sources.html": "index.html", # phase 76 task 02
"/git-sources.html": "index.html", # phase 76 task 02
"/history.html": "index.html", # phase 76 task 03
"/tokens.html": "index.html", # phase 79 task 06
"/theme.html": "index.html", # phase 91 task 04
}
def _page_file(path: str) -> Path:
"""The static file backing a page path (``/`` → ``index.html``;
the shell-served view paths → the shell, ``SHELL_BACKED_PAGES``)."""
name = SHELL_BACKED_PAGES.get(path, path.lstrip("/") or "index.html")
file = FRONTEND / name
assert file.is_file(), f"missing page file for {path}: {file}"
return file
def _assert_page_contract(response: httpx.Response, token: str) -> None:
"""The phase-54 page contract on any known page: a full 200 with the
CURRENT process token on the asset refs, ``Cache-Control: no-cache``,
and NO validators (a page must never be revalidated against a
validator this process published)."""
assert response.status_code == 200, (
f"a page path must never 304 (got {response.status_code})"
)
assert response.headers["cache-control"] == HTML_CACHE_CONTROL
assert "etag" not in response.headers
assert "last-modified" not in response.headers
assert f"?v={token}" in response.text
assert _token_ref_re(token).search(response.text), (
"no local assets/ ref carries ?v=<current token>"
)
@pytest.fixture(autouse=True)
def clean_chats(db: Session) -> Iterator[None]:
"""``saved_chats`` is global state — the share test writes one row
(house pattern from ``test_chats_api.py``)."""
db.execute(text("TRUNCATE saved_chats"))
db.commit()
yield
db.execute(text("TRUNCATE saved_chats"))
db.commit()
def test_every_known_page_200s_on_conditional_get(client: TestClient) -> None:
"""THE phase-54 regression, real app: for EVERY known page, a plain
GET sets the contract, then a conditional GET carrying the static
FILE's etag (what a browser captured pre-fix) must still 200 with
the SAME rewritten body — pre-fix the loop failed on the very first
304, pinning the browser on the previous commit's immutable assets."""
token = asset_version()
for path in HTML_PAGES:
plain = client.get(path)
_assert_page_contract(plain, token)
etag, _ = file_validators(_page_file(path))
conditional = client.get(path, headers={"if-none-match": etag})
_assert_page_contract(conditional, token)
assert conditional.content == plain.content, (
f"{path}: the conditional 200 must serve the same rewritten body"
)
def test_dynamic_shared_page_200s_on_conditional_get(admin_client: TestClient) -> None:
"""The dynamic /shared/<token> page (phase 51) gets the same contract
via the real save+share flow: a conditional GET carrying the
``shared.html`` file's etag must still 200 with the rewritten body —
the route's ``FileResponse`` honours conditional headers, so without
the inbound strip this page 304'd too."""
token = asset_version()
created = admin_client.post(
"/api/chats",
json={
"messages": [
{"who": "user", "text": "How did I install gitlab?"},
{"who": "brain", "text": "You've got this!"},
],
"share": True,
},
)
assert created.status_code == 201
share_url = created.json()["share_url"]
plain = admin_client.get(share_url)
_assert_page_contract(plain, token)
etag, _ = file_validators(FRONTEND / "shared.html")
conditional = admin_client.get(share_url, headers={"if-none-match": etag})
_assert_page_contract(conditional, token)
assert conditional.content == plain.content
def test_assets_keep_immutable_validators_and_304(client: TestClient) -> None:
"""The inbound strip must NOT have widened to /assets/*: the versioned
asset URL keeps its validators and still 304s on a conditional GET —
that 304 is safe because the URL itself carries ?v=<token>."""
token = asset_version()
url = f"/assets/styles.css?v={token}"
plain = client.get(url)
assert plain.status_code == 200
assert plain.headers["cache-control"] == ASSET_CACHE_CONTROL
assert "etag" in plain.headers
assert "last-modified" in plain.headers
conditional = client.get(url, headers={"if-none-match": plain.headers["etag"]})
assert conditional.status_code == 304 # versioned-URL 304s stay safe
assert conditional.content == b""
assert conditional.headers["cache-control"] == ASSET_CACHE_CONTROL
def test_api_paths_get_no_cache_headers_and_untouched_stream(client: TestClient) -> None:
"""/api/* stays byte-identical: no cache-control injected, no
validators published, and conditional headers pass through to the
route untouched (the SSE chat stream's pass-through is pinned by
``tests/integration/test_chat_api.py`` — the regression run below)."""
plain = client.get("/api/health")
assert plain.status_code == 200
assert "cache-control" not in plain.headers
assert "etag" not in plain.headers
assert "last-modified" not in plain.headers
conditional = client.get("/api/health", headers={"if-none-match": "x"})
assert conditional.status_code == 200 # pass-through — the strip is page-scoped
assert conditional.json() == plain.json()
assert "cache-control" not in conditional.headers
def test_page_token_matches_process_token(client: TestClient) -> None:
"""The token embedded in the served page equals ``asset_version()`` —
the per-process ``functools.cache`` contract: one page load can never
mix two versions (phase 54, assumption 5)."""
token = asset_version()
response = client.get("/")
assert response.status_code == 200
match = re.search(r'styles\.css\?v=([^"]+)"', response.text)
assert match is not None, "styles.css ref not found in the served page"
assert match.group(1) == token