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/`.
146 lines
6.2 KiB
Python
146 lines
6.2 KiB
Python
"""Unit tests: SQLAlchemy models register the pgvector schema on the metadata.
|
|
|
|
Importing :mod:`app.models` is what Alembic's ``env.py`` and the runtime rely
|
|
on; these tests lock the table/column contract (PLAN §5) without a live DB.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pgvector.sqlalchemy import Vector
|
|
from sqlalchemy import TextClause, UniqueConstraint
|
|
from sqlalchemy.sql.schema import DefaultClause
|
|
|
|
import app.models # noqa: F401 (import registers all tables on Base.metadata)
|
|
from app.db import Base
|
|
|
|
|
|
def test_all_tables_registered() -> None:
|
|
tables = Base.metadata.tables
|
|
assert "documents" in tables
|
|
assert "chunks" in tables
|
|
assert "query_log" in tables
|
|
assert "ui_settings" in tables # phase 91: the single-row UI settings
|
|
|
|
|
|
def test_ui_settings_single_row_nullable_contract() -> None:
|
|
"""Phase 91: the single-row UI settings table — Integer PK ``id``
|
|
with the Python-side ``default=1`` (the row is always id 1), the 3
|
|
strings VARCHAR(300) and the 8 identity colors VARCHAR(7), ALL
|
|
nullable (NULL = default — B1: env value for the strings, the
|
|
built-in palette for the colors)."""
|
|
settings_table = Base.metadata.tables["ui_settings"]
|
|
assert set(settings_table.c.keys()) == {
|
|
"id", "app_name", "input_placeholder", "footer_text",
|
|
"bg", "surface", "ink", "ink_soft", "line",
|
|
"brand", "brand_soft", "brand_ink",
|
|
}
|
|
pk = settings_table.c["id"]
|
|
assert pk.primary_key is True, "ui_settings.id must be the PK"
|
|
assert pk.default is not None, "id needs the Python-side default=1"
|
|
for name in ("app_name", "input_placeholder", "footer_text"):
|
|
col = settings_table.c[name]
|
|
assert col.nullable is True, f"{name} must be NULL (env default)"
|
|
assert getattr(col.type, "length", None) == 300, f"{name} must be String(300)"
|
|
for name in ("bg", "surface", "ink", "ink_soft", "line",
|
|
"brand", "brand_soft", "brand_ink"):
|
|
col = settings_table.c[name]
|
|
assert col.nullable is True, f"{name} must be NULL (the built-in)"
|
|
assert getattr(col.type, "length", None) == 7, f"{name} must be String(7) — #rrggbb"
|
|
|
|
|
|
def test_chunks_embedding_is_vector_768() -> None:
|
|
chunks = Base.metadata.tables["chunks"]
|
|
col = chunks.c["embedding"]
|
|
assert isinstance(col.type, Vector)
|
|
assert col.type.dim == 768
|
|
# Embeddings are two-phase: inserted first, embedded later.
|
|
assert col.nullable is True
|
|
|
|
|
|
def test_chunks_reference_documents_cascade() -> None:
|
|
chunks = Base.metadata.tables["chunks"]
|
|
fkc = list(chunks.foreign_key_constraints)[0]
|
|
assert fkc.elements[0].column.table.name == "documents"
|
|
assert fkc.ondelete == "CASCADE"
|
|
|
|
|
|
def test_documents_unique_source_path() -> None:
|
|
documents = Base.metadata.tables["documents"]
|
|
uq = [
|
|
c
|
|
for c in documents.constraints
|
|
if isinstance(c, UniqueConstraint)
|
|
and {col.name for col in c.columns} == {"source", "path"}
|
|
]
|
|
assert uq, "documents must be unique on (source, path) — the upsert key"
|
|
|
|
|
|
def test_doc_drafts_token_is_unique_not_null() -> None:
|
|
"""Phase 59: the draft's URL credential — an unguessable uuid4,
|
|
UNIQUE + NOT NULL (no "un-drafted" state, unlike the NULLable
|
|
``saved_chats.share_token``)."""
|
|
drafts = Base.metadata.tables["doc_drafts"]
|
|
token = drafts.c["token"]
|
|
assert token.nullable is False, "doc_drafts.token must be NOT NULL"
|
|
uq = [
|
|
c
|
|
for c in drafts.constraints
|
|
if isinstance(c, UniqueConstraint)
|
|
and {col.name for col in c.columns} == {"token"}
|
|
]
|
|
assert uq, "doc_drafts must be unique on (token) — the URL credential"
|
|
|
|
|
|
def test_git_sources_ignore_paths_column_contract() -> None:
|
|
"""Phase 89: every source row carries its ignore list — JSONB, NOT
|
|
NULL, server default ``'[]'`` (a pre-phase-89 row reads ``[]``, so
|
|
every existing source imports exactly as before)."""
|
|
sources = Base.metadata.tables["git_sources"]
|
|
assert "ignore_paths" in sources.c, (
|
|
"git_sources must have the ignore_paths column (phase 89)"
|
|
)
|
|
col = sources.c["ignore_paths"]
|
|
assert col.nullable is False, "git_sources.ignore_paths must be NOT NULL"
|
|
sd = col.server_default
|
|
assert isinstance(sd, DefaultClause), "ignore_paths needs a server default"
|
|
assert isinstance(sd.arg, TextClause), (
|
|
"the server default must be the literal SQL text '[]'"
|
|
)
|
|
assert sd.arg.text == "'[]'", "ignore_paths server default must be '[]'"
|
|
|
|
|
|
def test_git_source_python_default_empty_list() -> None:
|
|
"""A freshly constructed row (no DB) resolves to an empty ignore
|
|
list via the Python-side default (``default=list``) — the ORM
|
|
INSERT-time default, so an ORM insert that omits the column inserts
|
|
``[]`` rather than NULL (the server default ``'[]'`` independently
|
|
covers non-ORM inserts)."""
|
|
from app.models import GitSource
|
|
|
|
row = GitSource(url="https://example.com/r.git", kind="git")
|
|
col = row.__table__.c["ignore_paths"]
|
|
assert col.default is not None, (
|
|
"ignore_paths needs a Python-side (INSERT-time) default"
|
|
)
|
|
# Invoked with the (unused) execution context at INSERT time — a
|
|
# freshly constructed row that omits the kwarg stores [] rather
|
|
# than NULL (the real-DB behaviour is pinned against the dev DB
|
|
# by the git-sources API integration tests).
|
|
assert col.default.arg(None) == [], "the default must resolve to []"
|
|
|
|
|
|
def test_doc_drafts_column_contract() -> None:
|
|
"""Phase 59: the editable triple (title/path/body) + status +
|
|
timestamps are NOT NULL; ``branch`` / ``commit_sha`` are NULL
|
|
until the push endpoint records them."""
|
|
drafts = Base.metadata.tables["doc_drafts"]
|
|
assert set(drafts.c.keys()) == {
|
|
"id", "token", "title", "path", "body", "status",
|
|
"branch", "commit_sha", "created_at", "updated_at",
|
|
}
|
|
for name in ("title", "path", "body", "status", "created_at", "updated_at"):
|
|
assert drafts.c[name].nullable is False, f"{name} must be NOT NULL"
|
|
for name in ("branch", "commit_sha"):
|
|
assert drafts.c[name].nullable is True, f"{name} must be NULL until pushed"
|
|
assert drafts.c["status"].default is not None, "status needs an ORM default (draft)"
|
|
assert drafts.c["token"].default is not None, "token needs an ORM default (uuid4)"
|