"""Regression tests for the Phase 2 `ResumeChunk` ORM model. Two defects made the model unusable and are pinned here: * ``embedding`` was annotated ``Mapped[list[float] | None]`` with no Core type, so importing ``app.models`` raised ``MappedAnnotationError`` and the whole application failed to start. * ``is_parent`` was created by the migration but never declared on the model, so every query filtering on it (dense retrieval does) hit a column the ORM did not know about. These cases construct objects in memory only — no database is required. """ from __future__ import annotations import uuid from pgvector.sqlalchemy import Vector from app.models import ResumeChunk def _chunk(**overrides: object) -> ResumeChunk: """Build a `ResumeChunk` with valid defaults, overriding named fields. Args: **overrides: Column values to replace in the baseline row. Returns: An unsaved `ResumeChunk` instance. """ fields: dict[str, object] = { "tenant_id": uuid.uuid4(), "resume_version_id": uuid.uuid4(), "document_id": uuid.uuid4(), "chunk_index": 0, "section": "experience", "content": "Senior Backend Engineer — Python, Kubernetes, PostgreSQL", "page_from": 0, "page_to": 0, "start_char": 0, "end_char": 56, "token_count": 8, "embedding_model": "BAAI/bge-m3", "embedding_version": "1", "is_parent": False, } fields.update(overrides) return ResumeChunk(**fields) def test_resume_chunk_embedding_column_uses_pgvector_type() -> None: """`embedding` must map to `vector(1024)`, not an unresolvable Python type.""" column = ResumeChunk.__table__.columns["embedding"] assert isinstance(column.type, Vector) assert column.type.dim == 1024 assert column.nullable is True def test_resume_chunk_declares_is_parent_matching_the_migration() -> None: """`is_parent` must exist on the model, non-nullable, defaulting to false.""" column = ResumeChunk.__table__.columns["is_parent"] assert column.nullable is False assert _chunk().is_parent is False def test_resume_chunk_accepts_an_embedding_vector() -> None: """A 1024-dimension vector round-trips onto the instance unchanged.""" embedding = [0.01] * 1024 chunk = _chunk(embedding=embedding) assert chunk.embedding == embedding def test_resume_chunk_embedding_defaults_to_none_before_backfill() -> None: """Chunks are insertable before embeddings exist, so the column stays optional.""" assert _chunk().embedding is None def test_resume_chunk_parent_and_child_are_distinguishable() -> None: """Parents carry `is_parent`; children point back through `parent_chunk_id`.""" parent_id = uuid.uuid4() parent = _chunk(id=parent_id, is_parent=True) child = _chunk(chunk_index=1, parent_chunk_id=parent_id, is_parent=False) assert parent.is_parent is True assert parent.parent_chunk_id is None assert child.is_parent is False assert child.parent_chunk_id == parent_id def test_resume_chunk_content_tsv_column_uses_tsvector_type() -> None: """`content_tsv` must be TSVECTOR to match the migration's generated column. The migration creates `content_tsv` as a PostgreSQL tsvector GENERATED column. If the model declares it as Text, `Base.metadata.create_all()` will build a TEXT column instead, and queries like `content_tsv @@ plainto_tsquery(...)` will fail at runtime. """ from sqlalchemy.dialects.postgresql import TSVECTOR column = ResumeChunk.__table__.columns["content_tsv"] assert isinstance(column.type, TSVECTOR), ( f"content_tsv type is {type(column.type).__name__}, not TSVECTOR — " "this breaks lexical search with the @@ operator" ) assert column.nullable is True