phase: 147_sync_changed_only
All defaults match. Verification complete — everything is green with no defects found. **Phase 147 final verification pass — report** - All 3 task files found in `complete/147_sync_changed_only/` → verification pass only; no code changes needed. - Verified: migration 0025 (`documents.file_hash` nullable String(64), no index) + model field; fast path at top of `_index_file` (streamed 1 MiB sha256 → row lookup → `_unchanged_doc`); stamping on all three branches' create/update + shared helper; pre-phase NULL-hash fallthrough; retriever/`content_hash` untouched. - **Tests:** `uv run pytest --cov=app --cov-report=term-missing` → 3438 passed, `app/` coverage **99%** (>90% ✓); `uv run pytest tests/e2e/test_sync_changed_only.py -v --no-cov` → 3 passed in isolation ✓; `uv run ruff check . && uv run pyright` → all checks passed, 0 errors ✓; prior-phase `tests/e2e/test_sync_button.py` → 3 passed (no regression) ✓. - **Completion criteria:** all 7 pass — (1) column + round-trip ✓ (2) unchanged text/image/garbage-PDF/pictorial-PDF cost zero LLM/extraction/vision, `summary.unchanged` counted, rows byte-still ✓ (3) changed file re-indexes + re-stamps; bytes-changed/content-same stamps only ✓ (4) backfill/D1/D4 + log wordings pinned verbatim ✓ (5) `content_hash`/retriever byte-identical ✓ (6) all gates green ✓ (7) no prior-phase behavior change ✓. - **Documentation:** full review of README.md (1,031 lines) and AGENTS.md against the code: every `.env.example`/code `BOR_*` var documented, all config defaults match `app/config.py`, quick-start/QA commands and referenced test files exist, `file://` URL + troubleshooting claims accurate; the phase's README "Updating the documents" edit is accurate. **Verified consistent — no changes needed.** - **Notable:** no live-infrastructure changes (dev Postgres container used for the standard test gates only). Working tree left uncommitted per harness protocol. - **Next pending phase:** `148_pdf_pages_in_sources`.
This commit is contained in:
@@ -128,6 +128,81 @@ def test_documents_unique_source_path() -> None:
|
||||
assert uq, "documents must be unique on (source, path) — the upsert key"
|
||||
|
||||
|
||||
def test_documents_file_hash_column_contract() -> None:
|
||||
"""Phase 147 (task 01): ``documents.file_hash`` — the sha256 of the
|
||||
RAW SOURCE FILE's bytes (the sync's change signal): VARCHAR(64),
|
||||
NULLABLE (NULL = "unknown" — pre-phase rows until the first
|
||||
post-upgrade sync stamps it), NO server default, and NO index
|
||||
(the fast path compares it in Python after the unique
|
||||
``(source, path)`` lookup — internal bookkeeping, never a query
|
||||
surface). ``content_hash`` (0001) stays untouched: NOT NULL and
|
||||
still indexed — the retriever's dedupe/caching identity."""
|
||||
from sqlalchemy import String as SAString
|
||||
|
||||
documents = Base.metadata.tables["documents"]
|
||||
col = documents.c["file_hash"]
|
||||
assert col.nullable is True, ("file_hash must be NULLABLE — NULL is the pre-phase state")
|
||||
assert isinstance(col.type, SAString), "file_hash must be String(64)"
|
||||
assert col.type.length == 64, "file_hash must be String(64) — a sha256 hex digest"
|
||||
assert col.server_default is None, (
|
||||
"file_hash must carry NO server default — NULL is the meaningful"
|
||||
" pre-phase state"
|
||||
)
|
||||
# No index: neither the inline flag (None = unspecified) nor any
|
||||
# table-level index may cover file_hash (unlike content_hash — a
|
||||
# query/exposure surface).
|
||||
assert not col.index, ("file_hash must be UNINDEXED — compared in Python")
|
||||
covering = [
|
||||
idx
|
||||
for idx in documents.indexes
|
||||
if any(c.name == "file_hash" for c in idx.columns)
|
||||
]
|
||||
assert not covering, "no table-level index may cover file_hash"
|
||||
uq_covering = [
|
||||
c
|
||||
for c in documents.constraints
|
||||
if isinstance(c, UniqueConstraint) and any(col.name == "file_hash" for col in c.columns)
|
||||
]
|
||||
assert not uq_covering, "no unique constraint may cover file_hash"
|
||||
|
||||
# content_hash is untouched: still NOT NULL and indexed.
|
||||
content = documents.c["content_hash"]
|
||||
assert content.nullable is False, ("content_hash must stay NOT NULL (0001)")
|
||||
assert content.index is True, "content_hash must stay INDEXED (0001)"
|
||||
content_covering = [
|
||||
idx for idx in documents.indexes if any(c.name == "content_hash" for c in idx.columns)
|
||||
]
|
||||
assert content_covering, "the content_hash index must survive phase 147"
|
||||
|
||||
|
||||
def test_document_file_hash_orm_defaults_to_none() -> None:
|
||||
"""A freshly constructed ``Document`` (no DB) exposes ``file_hash``:
|
||||
the omitted kwarg and explicit ``None`` read NULL (the pre-phase
|
||||
shape — NULL is backfilled with a real digest only by the first
|
||||
post-upgrade sync), and a set digest round-trips as-is (the real
|
||||
DB round-trip is pinned in ``test_migration_0025.py``)."""
|
||||
from app.models import Document
|
||||
|
||||
base = dict(
|
||||
source="s",
|
||||
path="p.md",
|
||||
full_path="/tmp/s/p.md",
|
||||
title="p",
|
||||
content="c",
|
||||
content_hash="1" * 64,
|
||||
)
|
||||
stamped = Document(**base, file_hash="a" * 64)
|
||||
assert stamped.file_hash == "a" * 64
|
||||
|
||||
explicit_null = Document(**base, file_hash=None)
|
||||
assert explicit_null.file_hash is None
|
||||
|
||||
omitted = Document(**base)
|
||||
assert omitted.file_hash is None, (
|
||||
"an omitted file_hash must read NULL (the pre-phase shape)"
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user