"""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 (9 identity colors after phase 92, task 01; the 8 semantic state colors after phase 93, task 01 — B3 revised): 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 17 palette 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", "grid_line", "brand", "brand_soft", "brand_ink", "ok_bg", "ok_ink", "err_bg", "err_ink", "err_line", "accent_bg", "accent_ink", "accent_line", } 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", "grid_line", "brand", "brand_soft", "brand_ink", "ok_bg", "ok_ink", "err_bg", "err_ink", "err_line", "accent_bg", "accent_ink", "accent_line"): 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_git_sources_include_hidden_column_contract() -> None: """Phase 105: every source row carries its hidden-folders flag — BOOLEAN, NOT NULL, server default ``false`` (a pre-phase-105 row reads ``False``, so every existing source imports byte-identically until the owner flips it — A4).""" sources = Base.metadata.tables["git_sources"] assert "include_hidden" in sources.c, ( "git_sources must have the include_hidden column (phase 105)" ) col = sources.c["include_hidden"] assert col.nullable is False, "git_sources.include_hidden must be NOT NULL" sd = col.server_default assert isinstance(sd, DefaultClause), "include_hidden needs a server default" assert isinstance(sd.arg, TextClause), ( "the server default must be the literal SQL text false" ) assert sd.arg.text == "false", "include_hidden server default must be false" def test_git_source_python_default_include_hidden_false() -> None: """A freshly constructed row (no DB) resolves the flag to ``False`` via the Python-side default (``default=False``) — the ORM INSERT-time default, so an ORM insert that omits the column inserts ``false`` rather than NULL (the server default ``false`` independently covers non-ORM inserts — A4).""" from app.models import GitSource row = GitSource(url="https://example.com/r.git", kind="git") col = row.__table__.c["include_hidden"] assert col.default is not None, ( "include_hidden needs a Python-side (INSERT-time) default" ) # A scalar default: the arg IS the value (not a callable). assert col.default.arg is False, "the default must resolve to False" 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)"