Files
brain-of-reese/tests/unit/test_importer_unchanged_fast_path.py
T
ducoterra 6ba46e735a
Build and Push Containers / build-and-push-app (push) Successful in 2m3s
Build and Push Containers / build-and-push-db (push) Successful in 21s
phase: 163_pdf_strip_additive_selfheal
All gates verified. Final report:

**Phase 163 — final verification pass (all 5 tasks already complete; verification-only run)**

- Verified D1–D4 in code: `page_strip_mode` column + migration 0027, `DocContent` absent-never-null rule keyed off `page_count`; `_persist_pdf_pages` render → atomic tmp+`os.replace` writes → stale-tail-last + `strip_class` stamp; `_regate_pdf_strip` on both unchanged paths (mirror gate, heal, repair, fail-soft, one INFO line only when acting); viewer mode branch + Text/Pages toggle (Text default, `aria-pressed`, no innerHTML, lazy imgs).
- `uv run pytest --cov=app --cov-report=term-missing` → **3796 tests, exit 0, coverage 99%** (>90%)
- `uv run pytest tests/e2e/test_pdf_strip_additive_selfheal.py -v --no-cov` → **5 passed** in isolation
- `test_pdf_pages_in_sources + test_pictorial_pdf + test_garbage_pdf` → 10 passed; `test_document_viewer(_context) + test_image_documents` → 20 passed
- `uv run ruff check .` clean; `uv run pyright` → 0 errors; alembic **0027 downgrade→upgrade CLI round-trip clean** (+ integration pins)
- Completion criteria: additive view w/ toggle ✅ · primary DOM byte-identity ✅ · write-first order/mid-write-failure/atomic pins ✅ · heal + repair E2E ✅ · zero vision/LLM on fast path (mock-captured) ✅ · non-qualifying payload byte-identity ✅ · docs pass ✅
- **Documentation**: README.md + AGENTS.md reviewed in full — the phase's updates are accurate; all config-table defaults cross-checked against `app/config.py` (no drift), every page route, script, and E2E command referenced exists, every `.env.example` var documented — **verified consistent, no changes needed**.
- Notable: live `Letumdei` repair is owner-runtime post-deploy per D5 (next production Sync self-heals; no live changes made); the modified `.agents/screenshots/doc_modal_scrollbar_1280.png` is an E2E-regenerated artifact left for the pipeline commit.
- **Next pending phase:** `164_chat_error_honesty`
2026-10-07 13:57:30 -04:00

1154 lines
49 KiB
Python

"""Phase 147, task 02 — the unchanged fast path (unit matrix).
The owner's TODO L4: "Local sources, like PDFs, are synced every time
even if they haven't changed. This results in really long sync times
for PDFs that are mostly pictoral." The fix: the sha256 of the RAW
source file's bytes (:func:`app.rag.importer._file_hash` — streamed in
1 MiB chunks) is computed ONCE at the top of ``_index_file``, BEFORE
the image/text/PDF dispatch, and compared against
``documents.file_hash`` (migration 0025). A match returns through the
shared unchanged path (``_unchanged_doc``) with ZERO llm calls (embed
+ chat, both counted through the fakes), ZERO PDF extraction, and
ZERO vision work — while the preserved unchanged semantics (the
phase-118 summary-NULL backfill, the phase-106 D1 manual-date lock,
the D4 date refresh, the counts, the log wordings) survive verbatim.
Pinned here (the load-bearing pins are the ZERO-llm-call assertions on
the second sync):
* the fast path fires for all three document classes (text, image,
garbage-layer PDF, pictorial PDF) and returns before any branch code;
* a changed file (any byte) re-indexes fully and re-stamps the hash;
* a bytes-changed/content-same file (a PDF metadata edit) re-runs the
branch's pipeline but lands unchanged (no content change) and
stamps the new hash — the next sync is fast;
* a pre-phase row (``file_hash`` NULL) runs the branch's content-digest
path (today's behavior) and gets STAMPED — the next sync is fast;
* the D1 manual-date lock and the D4 date refresh (may go older,
counts ``dates_updated``) hold on the fast path;
* the phase-118 backfill on the fast path runs from the STORED
``doc.content`` (one ``is_summary`` chunk, no content re-embed);
* the streamed hash is transparent (a >1 MiB file hashes identically
to ``hashlib.sha256(bytes).hexdigest()``).
Phase 163 (task 03) extends this file with the PDF strip RE-GATE the
unchanged paths now run (``_regate_pdf_strip``) — the pre-phase "the
copies persist" assumption is what broke in production (a row claiming
``page_count = 36`` over ZERO files, every page 404ing forever): the
re-gate re-derives the strip from the current bytes + settings (ZERO
vision/LLM calls), heals missing copies, re-stamps a changed class,
clears a strip that no longer qualifies (kill switch included), stays
fail-soft, logs only when it ACTS, and costs a non-strip file zero PDF
parsing (spy-pinned).
The E2E surface (the idempotent second sync through the real UI +
mock LLM) is pinned in ``tests/e2e/test_sync_changed_only.py``
(task 03).
"""
from __future__ import annotations
import asyncio
import hashlib
import logging
import os
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any
import pymupdf as fitz
import pytest
from sqlalchemy import select
from app.config import Settings
from app.models import Document
from app.rag import importer
from app.rag.importer import _file_hash, import_sources
from tests.fakes import FakeEmbedder
from tests.unit.test_importer import (
_build_garbage_pdf,
_PageVisionLLM,
_pdf_settings,
_png_bytes,
_VisionLLM,
)
from tests.unit.test_pictorial_import import (
_build_dense_pdf,
_build_ikea_like_pdf,
_PictorialLLM,
_settings,
)
#: mtime granularity tolerance (the ``test_importer_dates.py``
#: convention).
_TOL = timedelta(milliseconds=50)
OLD_2021 = datetime(2021, 6, 1, 12, 0, 0, tzinfo=UTC)
def _utime(path: Path, when: datetime) -> None:
ts = when.timestamp()
os.utime(path, (ts, ts))
def _doc(db, source: str, rel: str) -> Document:
doc = db.scalar(select(Document).where(Document.source == source, Document.path == rel))
assert doc is not None, f"no documents row for ({source!r}, {rel!r})"
return doc
def _cleanup_source(db, source: str) -> None:
for doc in db.scalars(select(Document).where(Document.source == source)).all():
db.delete(doc)
db.commit()
# ---------- the streamed hash -------------------------------------------------
def test_file_hash_streamed_matches_direct_hash(tmp_path: Path) -> None:
"""The chunking is transparent: a >1 MiB file (1.5 MiB here —
spans multiple 1 MiB reads) hashes identically to
``hashlib.sha256(bytes).hexdigest()``, an empty file hashes the
empty byte string, and the result is a 64-char lowercase hex
digest (the ``documents.file_hash`` column width)."""
block = hashlib.sha256(b"seed-147").digest() # 32 B
data = block * 49152 # 1.5 MiB — well past one 1 MiB chunk
big = tmp_path / "big.bin"
big.write_bytes(data)
expected = hashlib.sha256(data).hexdigest()
assert len(expected) == 64 and expected == expected.lower()
assert _file_hash(big) == expected
empty = tmp_path / "empty.bin"
empty.write_bytes(b"")
assert _file_hash(empty) == hashlib.sha256(b"").hexdigest()
# ---------- the fast path: zero llm on the second sync ------------------------
def test_text_unchanged_second_sync_costs_zero_llm(db, tmp_path: Path) -> None:
"""Text class: index a file, capture the llm call count (embed +
chat both counted); re-run → ``unchanged == 1``, ZERO new llm
calls, ``files == 1``, the doc's ``indexed_at``/``content_hash``
untouched, and ``file_hash`` stamped after run 1 (equal to
``_file_hash`` of the raw bytes)."""
root = tmp_path / "fasttext"
root.mkdir()
path = root / "note.md"
path.write_text("# Note\n\nmarkdown body\n")
llm = FakeEmbedder()
try:
first = asyncio.run(import_sources([root], llm, session=db))
assert (first.added, first.unchanged, first.files) == (1, 0, 1)
doc = _doc(db, root.name, "note.md")
# Stamped after run 1 — the raw-bytes digest.
assert doc.file_hash == _file_hash(path)
embeds1, chats1 = len(llm.calls), len(llm.chat_calls)
indexed1, digest1 = doc.indexed_at, doc.content_hash
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged, second.files) == (0, 0, 1, 1)
# THE pin: zero new llm calls of any kind.
assert len(llm.calls) == embeds1, "fast path: no re-embed"
assert len(llm.chat_calls) == chats1, "fast path: no lite/vision call"
assert second.changed_paths == [] # no KB change recorded
db.expire_all()
doc2 = _doc(db, root.name, "note.md")
assert doc2.indexed_at == indexed1 # the re-index did not happen
assert doc2.content_hash == digest1
assert doc2.file_hash == _file_hash(path)
finally:
_cleanup_source(db, root.name)
def test_garbage_pdf_unchanged_second_sync_costs_zero_vision(db, tmp_path: Path) -> None:
"""Garbage-layer PDF class (the owner's core complaint): a no-text
layer's PDF indexed through the page-screenshot fallback (two
vision calls); re-run → ``unchanged == 1``, ZERO new llm calls —
the fallback vision did NOT re-fire (the pre-phase "vision
re-issued before the unchanged check can skip them" cost is
gone), the gate never re-fires, and the content row is
byte-identical."""
root = tmp_path / "fastgarbage"
root.mkdir()
path = root / "garbage.pdf"
_build_garbage_pdf(path)
llm = _PageVisionLLM()
llm.settings = _pdf_settings(tmp_path)
try:
first = asyncio.run(import_sources([root], llm, session=db))
assert (first.added, first.pdfs_screenshotted) == (1, 1)
assert llm.vision_calls == 2 # one per page — the first index
doc = _doc(db, root.name, "garbage.pdf")
assert doc.file_hash == _file_hash(path)
embeds1, chats1 = len(llm.calls), len(llm.chat_calls)
content1, digest1, indexed1 = doc.content, doc.content_hash, doc.indexed_at
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
assert second.pdfs_screenshotted == 0 # the gate never re-fired
# THE pin: the fallback vision did NOT re-fire.
assert llm.vision_calls == 2, "fast path: no re-issued vision call"
assert len(llm.chat_calls) == chats1
assert len(llm.calls) == embeds1, "fast path: no re-embed"
db.expire_all()
doc2 = _doc(db, root.name, "garbage.pdf")
assert doc2.content == content1 # content row byte-identical
assert doc2.content_hash == digest1
assert doc2.indexed_at == indexed1
finally:
_cleanup_source(db, root.name)
def test_pictorial_pdf_unchanged_second_sync_costs_zero_vision(
db, tmp_path: Path
) -> None:
"""Pictorial PDF class (the IKEA assembly-instruction class,
TODO L5): a sparse-page PDF indexed with the pictorial pass ON
(six vision calls); re-run → ``unchanged == 1``, ZERO new llm
calls — the pictorial pass did NOT re-fire (no re-render, no
vision), and the content is byte-stable."""
root = tmp_path / "fastikea"
root.mkdir()
path = root / "ikea.pdf"
_build_ikea_like_pdf(path)
llm = _PictorialLLM()
llm.settings = _settings(tmp_path) # pdf_pictorial ON (code default)
try:
first = asyncio.run(import_sources([root], llm, session=db))
assert (first.added, first.pdfs_pictorial, first.pdf_pictorial_pages) == (1, 1, 6)
assert llm.pictorial_calls == 6 # one per candidate — the first index
doc = _doc(db, root.name, "ikea.pdf")
assert doc.file_hash == _file_hash(path)
embeds1, chats1 = len(llm.calls), len(llm.chat_calls)
content1, digest1 = doc.content, doc.content_hash
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
assert second.pdfs_pictorial == 0 # the pass did not re-fire
# THE pin: the pictorial vision did NOT re-fire.
assert llm.pictorial_calls == 6, "fast path: no re-issued vision call"
assert len(llm.chat_calls) == chats1
assert len(llm.calls) == embeds1, "fast path: no re-embed"
db.expire_all()
doc2 = _doc(db, root.name, "ikea.pdf")
assert doc2.content == content1 # byte-stable content
assert doc2.content_hash == digest1
finally:
_cleanup_source(db, root.name)
def test_image_unchanged_second_sync_costs_zero_llm(db, tmp_path: Path) -> None:
"""Image class: a standalone image indexed through the vision
describe (one chat call); re-run → ``unchanged == 1``, ZERO new
llm calls — the describe did NOT re-fire, no re-embed, and the
row (content + ``indexed_at``) is untouched."""
root = tmp_path / "fastimg"
root.mkdir()
path = root / "pic.png"
path.write_bytes(_png_bytes(4, 4, (0.2, 0.4, 0.6)))
llm = _VisionLLM()
llm.settings = Settings(
_env_file=None, # pyright: ignore[reportCallIssue] (kwarg exists at runtime)
images=True,
image_dir=str(tmp_path / "images"),
)
try:
first = asyncio.run(import_sources([root], llm, session=db))
assert (first.added, first.images_failed) == (1, 0)
doc = _doc(db, root.name, "pic.png")
assert doc.file_hash == _file_hash(path)
assert doc.content == _VisionLLM.DESCRIPTION
embeds1, chats1 = len(llm.calls), len(llm.chat_calls)
indexed1 = doc.indexed_at
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
# THE pin: no re-describe, no re-embed.
assert len(llm.chat_calls) == chats1, "fast path: no re-describe"
assert len(llm.calls) == embeds1, "fast path: no re-embed"
db.expire_all()
doc2 = _doc(db, root.name, "pic.png")
assert doc2.content == _VisionLLM.DESCRIPTION
assert doc2.indexed_at == indexed1
finally:
_cleanup_source(db, root.name)
# ---------- changed files: full re-index + re-stamp ---------------------------
def test_changed_file_reindexes_and_re_stamps(db, tmp_path: Path) -> None:
"""A changed file (one byte flipped in a text file) re-indexes
FULLY — the llm calls fire again (embed + summary) — and
``file_hash`` is RE-STAMPED with the new raw-bytes digest;
``content_hash`` moves with the content."""
root = tmp_path / "fastchange"
root.mkdir()
path = root / "note.md"
path.write_text("# Note\n\nv1\n")
llm = FakeEmbedder()
try:
first = asyncio.run(import_sources([root], llm, session=db))
assert first.added == 1
doc = _doc(db, root.name, "note.md")
hash1, content_digest1 = doc.file_hash, doc.content_hash
assert hash1 == _file_hash(path)
embeds1, chats1 = len(llm.calls), len(llm.chat_calls)
path.write_text("# Note\n\nv2\n") # the bytes changed
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 1, 0)
# The full pipeline re-fired.
assert len(llm.calls) > embeds1, "a changed file re-embeds"
assert len(llm.chat_calls) > chats1, "a changed file re-summarizes"
db.expire_all()
doc2 = _doc(db, root.name, "note.md")
assert doc2.file_hash == _file_hash(path) # re-stamped
assert doc2.file_hash != hash1
assert doc2.content_hash != content_digest1
finally:
_cleanup_source(db, root.name)
def test_pdf_bytes_changed_content_same_stamps_without_content_change(
db, tmp_path: Path
) -> None:
"""The bytes-changed/content-same edge (a PDF metadata edit with
identical extracted text): the fast path does NOT fire (the raw
bytes moved), the branch re-runs its pipeline (the fallback's
vision re-issues — the honest cost of a byte change), but the
CONTENT digest matches — the row is UNCHANGED (no content change,
no re-embed), and the shared helper STAMPS the new hash, so the
NEXT sync takes the fast path (zero further vision)."""
root = tmp_path / "fastmeta"
root.mkdir()
path = root / "meta.pdf"
_build_garbage_pdf(path)
llm = _PageVisionLLM()
llm.settings = _pdf_settings(tmp_path)
try:
first = asyncio.run(import_sources([root], llm, session=db))
assert (first.added, first.pdfs_screenshotted) == (1, 1)
doc = _doc(db, root.name, "meta.pdf")
content1, digest1 = doc.content, doc.content_hash
hash1 = doc.file_hash
assert llm.vision_calls == 2
embeds1 = len(llm.calls)
# A metadata-only edit: the raw bytes change, the extracted
# text (and the rendered pages) do not.
with fitz.open(str(path)) as d:
d.set_metadata({"title": "metadata-only edit"})
d.saveIncr()
new_hash = _file_hash(path)
assert new_hash != hash1 # the bytes really moved
second = asyncio.run(import_sources([root], llm, session=db))
# The branch re-ran (gate re-fired, vision re-issued — the byte
# change's cost) but the content digest matched: the row is
# unchanged, no content change, no re-embed.
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
assert second.pdfs_screenshotted == 1 # the branch re-ran its gate
assert llm.vision_calls == 4 # the fallback re-issued (bytes changed)
assert len(llm.calls) == embeds1, "content-same: no re-embed"
db.expire_all()
doc2 = _doc(db, root.name, "meta.pdf")
assert doc2.content == content1 # content byte-identical
assert doc2.content_hash == digest1
assert doc2.file_hash == new_hash # STAMPED by the shared helper
# The NEXT sync is fast (the stamp landed): zero further vision.
third = asyncio.run(import_sources([root], llm, session=db))
assert (third.added, third.updated, third.unchanged) == (0, 0, 1)
assert third.pdfs_screenshotted == 0
assert llm.vision_calls == 4, "the third sync is the fast path"
assert len(llm.calls) == embeds1
finally:
_cleanup_source(db, root.name)
# ---------- pre-phase rows (NULL file_hash) -----------------------------------
def test_pre_phase_null_file_hash_runs_branch_path_and_stamps(
db, tmp_path: Path
) -> None:
"""A row that predates the column (``file_hash`` NULL) + an
unchanged file: the fast path cannot fire, so the branch's
content-digest path runs (today's behavior — the digest matches,
no re-embed), and the shared helper STAMPS the hash — the THIRD
sync then takes the fast path (proof the stamp landed)."""
root = tmp_path / "fastlegacy"
root.mkdir()
path = root / "note.md"
path.write_text("# Note\n\nmarkdown body\n")
llm = FakeEmbedder()
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, root.name, "note.md")
doc.file_hash = None # simulate the pre-phase row
db.commit()
embeds1, chats1 = len(llm.calls), len(llm.chat_calls)
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
# The content digest matched — no re-embed, no re-summary.
assert len(llm.calls) == embeds1
assert len(llm.chat_calls) == chats1
db.expire_all()
doc2 = _doc(db, root.name, "note.md")
assert doc2.file_hash == _file_hash(path) # stamped by the helper
# The third sync takes the FAST PATH (the stamp is committed):
third = asyncio.run(import_sources([root], llm, session=db))
assert third.unchanged == 1
assert len(llm.calls) == embeds1
assert len(llm.chat_calls) == chats1
finally:
_cleanup_source(db, root.name)
def test_pre_phase_null_file_hash_image_runs_branch_and_stamps(
db, tmp_path: Path
) -> None:
"""The image flavour of the branch path: a pre-phase row
(``file_hash`` NULL) + an unchanged image — the fast path cannot
fire, so the image branch's content-digest check runs (the raw
BYTES digest — the image's content identity — matches BEFORE the
expensive describe, so no re-describe, no re-embed), the row is
unchanged, and the shared helper STAMPS the hash — the next sync
is fast.
"""
root = tmp_path / "fastimglegacy"
root.mkdir()
path = root / "pic.png"
path.write_bytes(_png_bytes(4, 4, (0.2, 0.4, 0.6)))
llm = _VisionLLM()
llm.settings = Settings(
_env_file=None, # pyright: ignore[reportCallIssue] (kwarg exists at runtime)
images=True,
image_dir=str(tmp_path / "images"),
)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, root.name, "pic.png")
doc.file_hash = None # simulate the pre-phase row
db.commit()
chats1 = len(llm.chat_calls)
embeds1 = len(llm.calls)
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
# The branch's byte-digest check matched before the describe —
# zero llm work of any kind.
assert len(llm.chat_calls) == chats1
assert len(llm.calls) == embeds1, "content-same: no re-embed"
db.expire_all()
doc2 = _doc(db, root.name, "pic.png")
assert doc2.file_hash == _file_hash(path) # stamped by the helper
assert doc2.content == _VisionLLM.DESCRIPTION
# The next sync takes the fast path (zero further calls):
third = asyncio.run(import_sources([root], llm, session=db))
assert third.unchanged == 1
assert len(llm.chat_calls) == chats1
assert len(llm.calls) == embeds1
finally:
_cleanup_source(db, root.name)
# ---------- phase 163 (task 03): the deterministic PDF strip re-gate ----------
#
# The live defect (owner report, 2026-10-07): the `Letumdei` row carried
# `page_count = 36` over ZERO files on disk (the pre-phase-163 persist
# deleted the old copies BEFORE rendering, outside the DB transaction —
# a later per-file rollback restored the stale count over an empty
# directory; an external wipe of `pdf_page_dir` lands the same state).
# The phase-147 fast path believed "the copies persist" and never
# re-rendered, so the strip 404'd forever. `_regate_pdf_strip` now runs
# at the tail of `_unchanged_doc`: the changed-path gate mirrored from
# the current bytes + settings (ZERO vision/LLM calls), missing copies
# healed, a stale strip cleared.
#: The PNG signature (a healed copy is a real render, not a placeholder).
PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
def _regate_settings(tmp_path: Path, **kwargs: Any) -> Settings:
"""Settings for the re-gate matrix: the ``pdf`` token admitted, the
phase-140 knobs at their CODE DEFAULTS (``pdf_pictorial`` ON), and
the strip's home ``pdf_page_dir`` pinned under *tmp_path* (never the
real home)."""
kwargs.setdefault("_env_file", None)
kwargs.setdefault("import_extensions", "md,txt,pdf")
kwargs.setdefault("images", False)
kwargs.setdefault("image_dir", str(tmp_path / "images"))
kwargs.setdefault("pdf_page_dir", str(tmp_path / "pages"))
return Settings(**kwargs) # pyright: ignore[reportCallIssue]
def _pages_dir(tmp_path: Path) -> Path:
return tmp_path / "pages"
def _strip_copies(doc: Document, page_dir: Path, count: int) -> list[Path]:
return [page_dir / f"{doc.id}-p{n}.png" for n in range(1, count + 1)]
def test_regate_heals_deleted_copies_of_a_pictorial_strip(
db, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""THE live-loss reproduction (heal arm): a pictorial-class PDF is
indexed (``page_count`` set, copies written), its copies are deleted
out from under the row (the exact live state — mode NULL on a
pre-phase row), and an UNCHANGED re-sync restores every file with
real PNG bytes, stamps the mode ``additive``, keeps ``page_count``
(no drift), burns ZERO llm/vision calls, and logs the one greppable
INFO line. The NULL-mode stamp is SILENT (no ``mode`` segment — it
rides the line only when work followed)."""
root = tmp_path / "rgheal"
root.mkdir()
_build_ikea_like_pdf(root / "ikea.pdf")
llm = _PictorialLLM()
llm.settings = _regate_settings(tmp_path)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, "rgheal", "ikea.pdf")
assert (doc.page_count, doc.page_strip_mode) == (10, "additive")
page_dir = _pages_dir(tmp_path)
doc.page_strip_mode = None # the pre-phase row (the live shape)
db.commit()
for path in _strip_copies(doc, page_dir, 10):
path.unlink()
assert not list(page_dir.glob(f"{doc.id}-*.png"))
embeds1, chats1, vision1 = len(llm.calls), len(llm.chat_calls), llm.pictorial_calls
with caplog.at_level(logging.INFO, logger="app.importer"):
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
# ZERO llm work of any kind (the re-gate is deterministic, A5).
assert len(llm.calls) == embeds1, "the re-gate never re-embeds"
assert len(llm.chat_calls) == chats1, "the re-gate never chats"
assert llm.pictorial_calls == vision1, "the re-gate issues ZERO vision calls"
db.expire_all()
doc2 = _doc(db, "rgheal", "ikea.pdf")
assert doc2.page_count == 10, "no drift — the count is untouched"
assert doc2.page_strip_mode == "additive", "the mode is stamped"
for path in _strip_copies(doc2, page_dir, 10):
assert path.is_file(), f"copy {path.name} was not healed"
assert path.read_bytes()[:8] == PNG_MAGIC, f"{path.name} is not a PNG"
assert not list(page_dir.glob("*.tmp")), "no .tmp orphan from the heal"
messages = [r.getMessage() for r in caplog.records]
assert (
"import: PDF strip re-gate source=rgheal path=ikea.pdf — "
"healed 10 pages"
) in messages, messages
assert not any("mode " in m for m in messages if "re-gate" in m), (
"a NULL->stamp mode re-stamp is silent-beneficial " + str(messages)
)
finally:
_cleanup_source(db, "rgheal")
def test_regate_heals_a_fallback_strip_as_primary(
db, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""Heal arm, the FALLBACK class (phase 133's unusable text layer):
the copies of a garbage-layer PDF's strip are restored on an
unchanged re-sync and the mode lands ``primary`` (the strip IS the
view) — and the phase-133 fallback vision does NOT re-fire (the
class is re-derived from the bytes, no vision call)."""
root = tmp_path / "rgfall"
root.mkdir()
_build_garbage_pdf(root / "garbage.pdf") # 2 pages
llm = _PageVisionLLM()
llm.settings = _regate_settings(tmp_path)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, "rgfall", "garbage.pdf")
assert (doc.page_count, doc.page_strip_mode) == (2, "primary")
page_dir = _pages_dir(tmp_path)
for path in _strip_copies(doc, page_dir, 2):
path.unlink()
vision1, chats1, embeds1 = llm.vision_calls, len(llm.chat_calls), len(llm.calls)
with caplog.at_level(logging.INFO, logger="app.importer"):
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
assert second.pdfs_screenshotted == 0, "the fallback gate did not re-run"
assert llm.vision_calls == vision1, "ZERO vision calls on the re-gate"
assert len(llm.chat_calls) == chats1 and len(llm.calls) == embeds1
db.expire_all()
doc2 = _doc(db, "rgfall", "garbage.pdf")
assert doc2.page_strip_mode == "primary", "the unusable layer is primary"
assert doc2.page_count == 2
for path in _strip_copies(doc2, page_dir, 2):
assert path.is_file() and path.read_bytes()[:8] == PNG_MAGIC
assert any(
"healed 2 pages" in m and "re-gate" in m for m in caplog.messages
), caplog.messages
finally:
_cleanup_source(db, "rgfall")
def test_regate_noop_when_the_strip_is_healthy(
db, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""No-op arm (the IKEA-class doc whose copies all stand): an
unchanged re-sync writes NOTHING (every copy's mtime and bytes are
untouched), renders NOTHING, and logs NO re-gate line — the INFO is
never per-unchanged-doc — while the pre-existing ``import:
unchanged`` line still fires."""
root = tmp_path / "rgnoop"
root.mkdir()
_build_ikea_like_pdf(root / "ikea.pdf")
llm = _PictorialLLM()
llm.settings = _regate_settings(tmp_path)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, "rgnoop", "ikea.pdf")
page_dir = _pages_dir(tmp_path)
before = {
p: (p.stat().st_mtime_ns, p.read_bytes())
for p in _strip_copies(doc, page_dir, 10)
}
assert len(before) == 10
with caplog.at_level(logging.INFO, logger="app.importer"):
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
db.expire_all()
doc2 = _doc(db, "rgnoop", "ikea.pdf")
assert (doc2.page_count, doc2.page_strip_mode) == (10, "additive")
after = {
p: (p.stat().st_mtime_ns, p.read_bytes())
for p in _strip_copies(doc2, page_dir, 10)
}
assert after == before, "a healthy strip is not written at all"
messages = [r.getMessage() for r in caplog.records]
assert any(m.startswith("import: unchanged ") for m in messages), messages
assert not any("re-gate" in m for m in messages), (
f"the re-gate logged without acting: {messages}"
)
finally:
_cleanup_source(db, "rgnoop")
def test_regate_restamps_a_changed_mode(
db, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""Mode re-stamp (the class flip): a row whose stamped mode no
longer matches the gate's class (here a pictorial-class row carrying
``primary`` — a legacy/mis-stamped row) is re-stamped to
``additive``, and because the mode was ALREADY SET the change is
reported (``mode primary->additive``) — the only thing the re-gate
did, so that is the whole INFO line (no copies were missing, none
were written)."""
root = tmp_path / "rgmode"
root.mkdir()
_build_ikea_like_pdf(root / "ikea.pdf")
llm = _PictorialLLM()
llm.settings = _regate_settings(tmp_path)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, "rgmode", "ikea.pdf")
assert doc.page_strip_mode == "additive"
doc.page_strip_mode = "primary" # the mis-stamp the re-gate fixes
db.commit()
page_dir = _pages_dir(tmp_path)
before = {
p: p.stat().st_mtime_ns for p in _strip_copies(doc, page_dir, 10)
}
with caplog.at_level(logging.INFO, logger="app.importer"):
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.unchanged) == (0, 1)
db.expire_all()
doc2 = _doc(db, "rgmode", "ikea.pdf")
assert doc2.page_strip_mode == "additive", "the class is re-stamped"
assert doc2.page_count == 10
assert {
p: p.stat().st_mtime_ns for p in _strip_copies(doc2, page_dir, 10)
} == before, "a mode fix writes no files"
assert (
"import: PDF strip re-gate source=rgmode path=ikea.pdf — "
"mode primary->additive"
) in [r.getMessage() for r in caplog.records], caplog.messages
finally:
_cleanup_source(db, "rgmode")
def test_regate_clears_a_strip_that_no_longer_qualifies(
db, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""REPAIR arm: a row claims a strip (``page_count`` set, copies on
disk) on a PDF whose GATE now says ineligible (a clean, dense text
layer — zero sparse pages). The unchanged re-sync clears BOTH
columns and deletes the copies (the strip must not linger over a
document the viewer shows as text) + the ``cleared stale strip``
INFO line, with zero llm calls."""
root = tmp_path / "rgclear"
root.mkdir()
pdf = root / "clean.pdf"
_build_dense_pdf(pdf) # usable layer, zero candidates — never qualifies
llm = _PictorialLLM()
llm.settings = _regate_settings(tmp_path)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, "rgclear", "clean.pdf")
assert (doc.page_count, doc.page_strip_mode) == (None, None)
# The stale state the re-gate must repair (a strip stamped by an
# older gate/settings, copies still on disk).
page_dir = _pages_dir(tmp_path)
page_dir.mkdir(parents=True, exist_ok=True)
for path in _strip_copies(doc, page_dir, 2):
path.write_bytes(b"STALE-COPY")
doc.page_count = 2
doc.page_strip_mode = "additive"
db.commit()
embeds1, chats1 = len(llm.calls), len(llm.chat_calls)
with caplog.at_level(logging.INFO, logger="app.importer"):
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
assert len(llm.calls) == embeds1 and len(llm.chat_calls) == chats1
db.expire_all()
doc2 = _doc(db, "rgclear", "clean.pdf")
assert doc2.page_count is None, "the stale strip is cleared"
assert doc2.page_strip_mode is None, "the class is cleared with it"
assert not list(page_dir.glob(f"{doc2.id}-p*.png")), "the copies are deleted"
assert (
"import: PDF strip re-gate source=rgclear path=clean.pdf — "
"cleared stale strip"
) in [r.getMessage() for r in caplog.records], caplog.messages
finally:
_cleanup_source(db, "rgclear")
def test_regate_kill_switch_clears_the_pictorial_strip(
db, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""REPAIR arm via the kill switch (``pdf_pictorial=False``): the
changed-path semantics mirrored — with the pass off a sparse-page
PDF is NOT eligible, so its existing strip is cleared with its
copies on the next (unchanged) sync. The bytes never moved: the
setting alone decided."""
root = tmp_path / "rgkill"
root.mkdir()
_build_ikea_like_pdf(root / "ikea.pdf")
llm = _PictorialLLM()
llm.settings = _regate_settings(tmp_path)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, "rgkill", "ikea.pdf")
assert (doc.page_count, doc.page_strip_mode) == (10, "additive")
page_dir = _pages_dir(tmp_path)
assert len(list(page_dir.glob(f"{doc.id}-p*.png"))) == 10
vision1 = llm.pictorial_calls
llm.settings = _regate_settings(tmp_path, pdf_pictorial=False)
with caplog.at_level(logging.INFO, logger="app.importer"):
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
assert llm.pictorial_calls == vision1, "ZERO vision calls (the pass is off)"
db.expire_all()
doc2 = _doc(db, "rgkill", "ikea.pdf")
assert doc2.page_count is None and doc2.page_strip_mode is None
assert not list(page_dir.glob(f"{doc2.id}-p*.png")), "the copies went too"
assert any(
"cleared stale strip" in m for m in caplog.messages
), caplog.messages
finally:
_cleanup_source(db, "rgkill")
def test_regate_render_exception_is_fail_soft(
db,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Fail-soft: the heal's render RAISES — the sync completes
(``unchanged``, zero errors), the row keeps its prior state
(``page_count`` + mode untouched — the re-gate never clears a strip
on a failure), no copies are touched, and the WARNING is the only
signal."""
root = tmp_path / "rgboom"
root.mkdir()
_build_ikea_like_pdf(root / "ikea.pdf")
llm = _PictorialLLM()
llm.settings = _regate_settings(tmp_path)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, "rgboom", "ikea.pdf")
page_dir = _pages_dir(tmp_path)
for path in _strip_copies(doc, page_dir, 10):
path.unlink()
def boom(*args: object, **kwargs: object) -> list[bytes]:
raise RuntimeError("simulated render failure (test sentinel)")
monkeypatch.setattr(importer, "render_pdf_pages", boom)
with caplog.at_level(logging.WARNING, logger="app.importer"):
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged, second.errors) == (
0, 0, 1, 0,
)
db.expire_all()
doc2 = _doc(db, "rgboom", "ikea.pdf")
assert (doc2.page_count, doc2.page_strip_mode) == (10, "additive"), (
"the row keeps its pre-re-gate state"
)
assert not list(page_dir.glob(f"{doc.id}-p*.png")), "no copies were touched"
assert any(
"PDF strip re-gate failed" in m and "test sentinel" in m
for m in caplog.messages
), caplog.messages
finally:
_cleanup_source(db, "rgboom")
def test_regate_empty_render_keeps_the_row(
db,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Fail-soft on the no-pages render (unopenable right after the fast
path hashed the file — pathological): the row is kept EXACTLY as it
was (never cleared on a transient), nothing is written, and the
WARNING names the no-pages state."""
root = tmp_path / "rgempty"
root.mkdir()
_build_ikea_like_pdf(root / "ikea.pdf")
llm = _PictorialLLM()
llm.settings = _regate_settings(tmp_path)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, "rgempty", "ikea.pdf")
page_dir = _pages_dir(tmp_path)
for path in _strip_copies(doc, page_dir, 10):
path.unlink()
monkeypatch.setattr(importer, "render_pdf_pages", lambda *a, **k: [])
with caplog.at_level(logging.WARNING, logger="app.importer"):
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.unchanged, second.errors) == (0, 1, 0)
db.expire_all()
doc2 = _doc(db, "rgempty", "ikea.pdf")
assert (doc2.page_count, doc2.page_strip_mode) == (10, "additive")
assert not page_dir.exists() or not list(page_dir.glob(f"{doc.id}-*"))
assert any(
"re-render produced no pages (state kept)" in m
for m in caplog.messages
), caplog.messages
finally:
_cleanup_source(db, "rgempty")
def test_regate_reports_a_page_count_drift_on_the_heal(
db,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Drift (pathological on unchanged bytes, so it is LOUD): the heal
renders 6 pages for a row that claimed 10 — ``page_count`` follows
the fresh render, the ``p7..p10`` tail beyond it is deleted, and a
WARNING names both counts beside the ``healed`` INFO line."""
root = tmp_path / "rgdrift"
root.mkdir()
_build_ikea_like_pdf(root / "ikea.pdf")
llm = _PictorialLLM()
llm.settings = _regate_settings(tmp_path)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, "rgdrift", "ikea.pdf")
page_dir = _pages_dir(tmp_path)
for path in _strip_copies(doc, page_dir, 10):
path.unlink()
monkeypatch.setattr(
importer,
"render_pdf_pages",
lambda *a, **k: [PNG_MAGIC + f"{i}".encode() for i in range(6)],
)
with caplog.at_level(logging.INFO, logger="app.importer"):
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.unchanged) == (0, 1)
db.expire_all()
doc2 = _doc(db, "rgdrift", "ikea.pdf")
assert doc2.page_count == 6, "the count follows the fresh render"
for n in range(1, 7):
assert (page_dir / f"{doc2.id}-p{n}.png").is_file()
for n in range(7, 11):
assert not (page_dir / f"{doc2.id}-p{n}.png").exists(), f"p{n} leaked"
messages = [r.getMessage() for r in caplog.records]
assert any("page_count drift 10->6" in m for m in messages), messages
assert any("healed 10 pages" in m for m in messages), messages
finally:
monkeypatch.undo() # pyright: ignore[reportUnknownMemberType]
_cleanup_source(db, "rgdrift")
def test_regate_fires_on_the_content_digest_hit_path(
db, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""The SECOND unchanged path (the PDF branch's content-digest hit —
a pre-phase row with ``file_hash`` NULL): the branch re-runs its own
gate (the byte-change's honest cost) and lands unchanged, and the
shared helper's tail still heals the strip deleted out from under
the row — the re-gate is not fast-path-only."""
root = tmp_path / "rgbranch"
root.mkdir()
_build_ikea_like_pdf(root / "ikea.pdf")
llm = _PictorialLLM()
llm.settings = _regate_settings(tmp_path)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, "rgbranch", "ikea.pdf")
doc.file_hash = None # the pre-phase row: the fast path cannot fire
db.commit()
page_dir = _pages_dir(tmp_path)
for path in _strip_copies(doc, page_dir, 10):
path.unlink()
with caplog.at_level(logging.INFO, logger="app.importer"):
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
db.expire_all()
doc2 = _doc(db, "rgbranch", "ikea.pdf")
assert doc2.file_hash is not None, "the helper stamped the hash"
for path in _strip_copies(doc2, page_dir, 10):
assert path.is_file(), f"copy {path.name} was not healed"
assert any("healed 10 pages" in m for m in caplog.messages), caplog.messages
finally:
_cleanup_source(db, "rgbranch")
def test_regate_guard_costs_a_non_strip_file_nothing(
db, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The GUARD (A5): a non-PDF unchanged file and a PDF whose
``page_count`` is NULL reach the tail of ``_unchanged_doc`` and
``extract_pdf_text`` is NEVER called for either — every non-strip
file's fast path keeps its zero-extra-read contract (one sync of
each class, then the spy covers the second sync only)."""
root = tmp_path / "rgguard"
root.mkdir()
(root / "note.md").write_text("# Note\n\nplain text body\n")
_build_dense_pdf(root / "clean.pdf") # usable layer, no strip
llm = _PictorialLLM()
llm.settings = _regate_settings(tmp_path)
seen: list[str] = []
real_extract = importer.extract_pdf_text
def spy(path: Path) -> str:
seen.append(str(path))
return real_extract(path)
try:
asyncio.run(import_sources([root], llm, session=db))
monkeypatch.setattr(importer, "extract_pdf_text", spy)
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 2)
assert seen == [], f"the guard let a PDF parse through: {seen}"
finally:
monkeypatch.undo() # pyright: ignore[reportUnknownMemberType]
_cleanup_source(db, "rgguard")
# ---------- the preserved semantics on the fast path --------------------------
def test_manual_date_row_fast_path_no_date_write(db, tmp_path: Path) -> None:
"""D1 on the fast path: an unchanged file carrying the owner's
correction (``created_at_manual``) gets NO date write — the moved
mtime does not refresh it, ``dates_updated`` stays 0, the
correction + the flag survive, and the row's stamp is intact
(zero llm calls)."""
root = tmp_path / "fastmanual"
root.mkdir()
path = root / "note.md"
path.write_text("# Note\n\ncontent for the KB\n")
llm = FakeEmbedder()
correction = datetime(2023, 5, 5, 9, 30, 0, tzinfo=UTC)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, root.name, "note.md")
doc.created_at = correction
doc.created_at_manual = True
db.commit()
_utime(path, OLD_2021) # the source moved to a DIFFERENT date
embeds1, chats1 = len(llm.calls), len(llm.chat_calls)
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
assert second.dates_updated == 0 # the manual lock withheld it
assert len(llm.calls) == embeds1 and len(llm.chat_calls) == chats1
db.expire_all()
doc2 = _doc(db, root.name, "note.md")
assert doc2.created_at == correction # the correction survived
assert doc2.created_at_manual is True # the flag intact
assert doc2.file_hash == _file_hash(path) # the stamp intact
finally:
_cleanup_source(db, root.name)
def test_manual_date_null_hash_row_stamps_without_date_write(
db, tmp_path: Path
) -> None:
"""D1 on the BRANCH path (a pre-phase NULL-hash row that also
carries the manual correction): the shared helper's stamp is the
only write — it COMMITS (so the next sync is fast even for a
manual-date row) — while the DATE stays the manual value
(``dates_updated`` 0, the flag intact)."""
root = tmp_path / "fastmanualnull"
root.mkdir()
path = root / "note.md"
path.write_text("# Note\n\ncontent for the KB\n")
llm = FakeEmbedder()
correction = datetime(2023, 5, 5, 9, 30, 0, tzinfo=UTC)
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, root.name, "note.md")
doc.created_at = correction
doc.created_at_manual = True
doc.file_hash = None # pre-phase row
db.commit()
_utime(path, OLD_2021) # a refresh is DUE — the lock must withhold it
embeds1, chats1 = len(llm.calls), len(llm.chat_calls)
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
assert second.dates_updated == 0
# No summary work of any kind on this run (run 1's summary
# count is the baseline).
assert len(llm.chat_calls) == chats1
assert len(llm.calls) == embeds1
db.expire_all()
doc2 = _doc(db, root.name, "note.md")
assert doc2.created_at == correction # the correction survived
assert doc2.created_at_manual is True
assert doc2.file_hash == _file_hash(path) # the stamp committed
# The next sync is fast (the stamp landed despite the D1 lock):
third = asyncio.run(import_sources([root], llm, session=db))
assert third.unchanged == 1 and third.dates_updated == 0
db.expire_all()
assert _doc(db, root.name, "note.md").created_at == correction
finally:
_cleanup_source(db, root.name)
def test_unchanged_fast_path_date_refresh_may_go_older(
db, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""D4 on the fast path: an unchanged file whose mtime-derived date
moved (OLDER — no monotonic guard) refreshes the stored date on
the fast path: ``dates_updated == 1``, the date lands, and zero
llm calls (the refresh is bookkeeping, not a re-index). The
PRE-CHANGE log wordings are pinned verbatim (``import: unchanged``
+ ``import: date-refreshed``)."""
root = tmp_path / "fastdate"
root.mkdir()
path = root / "note.md"
path.write_text("# Note\n\ncontent for the KB\n")
llm = FakeEmbedder()
try:
asyncio.run(import_sources([root], llm, session=db))
_utime(path, OLD_2021) # the source date moved; content identical
embeds1, chats1 = len(llm.calls), len(llm.chat_calls)
with caplog.at_level(logging.INFO, logger="app.importer"):
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.unchanged) == (0, 0, 1)
assert second.dates_updated == 1 # the refresh ran (fast path)
assert len(llm.calls) == embeds1 and len(llm.chat_calls) == chats1
# The preserved wordings (the shared helper's log lines, verbatim
# from the per-branch blocks).
messages = [r.getMessage() for r in caplog.records]
assert f"import: unchanged source={root.name} path=note.md" in messages
assert any(
m.startswith(f"import: date-refreshed source={root.name} path=note.md date=")
for m in messages
)
db.expire_all()
doc2 = _doc(db, root.name, "note.md")
assert abs(doc2.created_at - OLD_2021) <= _TOL
finally:
_cleanup_source(db, root.name)
def test_null_summary_backfill_on_fast_path(db, tmp_path: Path) -> None:
"""Phase 118 (A2) via the fast path: an unchanged doc whose
summary is still NULL (the stored content is intact) gets the
summary-only backfill from the STORED ``doc.content`` — one
``is_summary`` chunk, no content re-embed, counted
``summary_backfilled`` (never ``summaries``), never
added/updated/pruned. The ``lite`` call fires (the backfill's own
work — the fast path's "zero llm calls" is "unless the backfill
fires"); nothing else does."""
root = tmp_path / "fastbfill"
root.mkdir()
(root / "note.md").write_text("# Note\n\nmarkdown body\n")
llm = FakeEmbedder()
try:
asyncio.run(import_sources([root], llm, session=db))
doc = _doc(db, root.name, "note.md")
# Simulate the NULL-summary target row: the content stays, only
# the summary + its chunk go away (the phase-118 convention).
doc.summary = None
for c in [c for c in doc.chunks if c.is_summary]:
doc.chunks.remove(c)
db.commit()
assert doc.file_hash is not None # the fast path can fire
embeds1, chats1 = len(llm.calls), len(llm.chat_calls)
second = asyncio.run(import_sources([root], llm, session=db))
assert (second.added, second.updated, second.pruned) == (0, 0, 0)
assert (second.unchanged, second.summary_backfilled, second.summaries) == (1, 1, 0)
assert second.summary_errors == 0
# Exactly the backfill's own work: one lite call, one embed
# batch (the summary chunk only — no content re-embed).
assert len(llm.chat_calls) == chats1 + 1
assert len(llm.calls) == embeds1 + 1
assert second.changed_paths == [] # no KB change recorded
db.expire_all()
doc2 = _doc(db, root.name, "note.md")
assert doc2.summary is not None
schunks = [c for c in doc2.chunks if c.is_summary]
assert len(schunks) == 1 and schunks[0].position == -1
assert schunks[0].content == doc2.summary
content = [c for c in doc2.chunks if not c.is_summary]
assert len(content) == 1 and content[0].embedding is not None
finally:
_cleanup_source(db, root.name)