From 632fb88e43e3057aa4e46f65030d03560a597ac1 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Fri, 16 Oct 2026 04:54:01 -0400 Subject: [PATCH] phase: 185_sync_defer_content_fast_path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All gates green. Report: **Implemented / verified (phase 185)** - `_doc_row_stmt` is the ONE deferral helper for all four fast-path row loads; only `Document.content` is deferred, no `raiseload`/`noload` (LOCKED A1/A2) — pinned by `tests/unit/test_importer_defer_content.py`. - `tests/integration/test_import_fast_path_columns.py` (5), `tests/e2e/test_fast_path_deferred_content.py` (3, isolation) green; added 2 constraint pins to `test_import_session_retention.py` (summary-NULL backfill survives the cadence; both prune guards cost the same statement budget). - **Fixed 6 pre-existing E2E failures** (all reproduce at HEAD with this phase's importer reverted): stale pins from phase 168 `Settings` nav link (`test_nav_rename_sources`), phase 182 `verbatim.js` script count (`test_steering`), phase 175 `/api/chats?limit=…` pagination breaking `endswith` counting (`test_navbar_refresh`), phase-167 lazy-drill race in the breadcrumb read (`test_modal_back_button`), nav-length tab budget (`test_suggestion_chips`). - **Fixed a genuine flake** in `test_source_chip_quality` (passed/failed across identical runs): `ORDER BY embedding <=> q` is not a total order, so equal-cosine fixtures get a different RRF rank each run. Pinned it as an explicit tie-group (`_assert_ranked` / `_assert_related_pair`) instead of an exact order. **Tests / lint (all 100% green)** - `uv run pytest --cov=app --cov-report=term-missing` → **4519 passed**, `app/rag/importer.py` **99%**, `TOTAL` **99%** (>90%). - `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors. - Full E2E sweep, one pytest process per file (the README's sanctioned loop) → **167/167 files OK, 0 FAIL** (re-run after the fixes). **Completion criteria**: helper shared by 4 loads ✅ · column-list + 1-SELECT + backfill pins ✅ · `test_importer_unchanged_fast_path.py` 24 passed, zero expectation edits ✅ · full suite + coverage + lint/types ✅ · phase E2E in isolation ✅ · docs accurate ✅ · commit left to the harness (no git commands run). **Documentation**: README + AGENTS.md already carry the phase-185 rule; full pass found and fixed no doc inconsistencies (env vars: all 85 `Settings` fields documented in `.env.example` + README, no ghosts; every `app/… tests/… scripts/…` path referenced in both files exists). **Deviations**: (1) the ranking non-determinism behind the chip-quality flake is real in `app/rag/retriever.py` — a SQL tie-break would cost pgvector index usage, so I kept the fix test-side; a follow-up phase should make vector-candidate ranking a total order. (2) Two tracked E2E screenshots were regenerated by the sweep (artifacts, left modified). **Next pending phase**: `186_git_log_streaming_dates`. --- .../00_phase.md | 0 .../01_fast_path_statement_probe.md | 0 .../02_defer_content.md | 0 .../03_e2e_deferred_content.md | 0 ...c_defer_content_fast_path__00_phase.a1.err | 0 ...nc_defer_content_fast_path__00_phase.a1.md | 20 + ...r_content_fast_path__00_phase.a1.md.status | 1 + ...er_content_fast_path__00_phase.a1.validate | 143 ++++ ..._path__01_fast_path_statement_probe.a1.err | 0 ...t_path__01_fast_path_statement_probe.a1.md | 18 + ..._01_fast_path_statement_probe.a1.md.status | 1 + ...__01_fast_path_statement_probe.a1.validate | 143 ++++ ...content_fast_path__02_defer_content.a1.err | 0 ..._content_fast_path__02_defer_content.a1.md | 15 + ...t_fast_path__02_defer_content.a1.md.status | 1 + ...nt_fast_path__02_defer_content.a1.validate | 143 ++++ ..._fast_path__03_e2e_deferred_content.a1.err | 0 ...t_fast_path__03_e2e_deferred_content.a1.md | 17 + ...path__03_e2e_deferred_content.a1.md.status | 1 + ..._path__03_e2e_deferred_content.a1.validate | 143 ++++ .../166_pre_answer_window_1280.png | Bin 69542 -> 71076 bytes .../screenshots/doc_modal_scrollbar_1280.png | Bin 43362 -> 43137 bytes AGENTS.md | 25 + README.md | 33 +- app/rag/importer.py | 66 +- tests/e2e/test_fast_path_deferred_content.py | 760 ++++++++++++++++++ tests/e2e/test_modal_back_button.py | 29 +- tests/e2e/test_nav_rename_sources.py | 7 +- tests/e2e/test_navbar_refresh.py | 19 +- tests/e2e/test_source_chip_quality.py | 88 +- tests/e2e/test_steering.py | 12 +- tests/e2e/test_suggestion_chips.py | 10 +- .../test_import_fast_path_columns.py | 749 +++++++++++++++++ .../test_import_session_retention.py | 298 +++++-- tests/unit/test_importer_defer_content.py | 258 ++++++ 35 files changed, 2862 insertions(+), 138 deletions(-) rename .agents/phases/{todo => complete}/185_sync_defer_content_fast_path/00_phase.md (100%) rename .agents/phases/{todo => complete}/185_sync_defer_content_fast_path/01_fast_path_statement_probe.md (100%) rename .agents/phases/{todo => complete}/185_sync_defer_content_fast_path/02_defer_content.md (100%) rename .agents/phases/{todo => complete}/185_sync_defer_content_fast_path/03_e2e_deferred_content.md (100%) create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.err create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.md create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.md.status create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.validate create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.err create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.md create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.md.status create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.validate create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.err create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.md create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.md.status create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.validate create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.err create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.md create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.md.status create mode 100644 .agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.validate create mode 100644 tests/e2e/test_fast_path_deferred_content.py create mode 100644 tests/integration/test_import_fast_path_columns.py create mode 100644 tests/unit/test_importer_defer_content.py diff --git a/.agents/phases/todo/185_sync_defer_content_fast_path/00_phase.md b/.agents/phases/complete/185_sync_defer_content_fast_path/00_phase.md similarity index 100% rename from .agents/phases/todo/185_sync_defer_content_fast_path/00_phase.md rename to .agents/phases/complete/185_sync_defer_content_fast_path/00_phase.md diff --git a/.agents/phases/todo/185_sync_defer_content_fast_path/01_fast_path_statement_probe.md b/.agents/phases/complete/185_sync_defer_content_fast_path/01_fast_path_statement_probe.md similarity index 100% rename from .agents/phases/todo/185_sync_defer_content_fast_path/01_fast_path_statement_probe.md rename to .agents/phases/complete/185_sync_defer_content_fast_path/01_fast_path_statement_probe.md diff --git a/.agents/phases/todo/185_sync_defer_content_fast_path/02_defer_content.md b/.agents/phases/complete/185_sync_defer_content_fast_path/02_defer_content.md similarity index 100% rename from .agents/phases/todo/185_sync_defer_content_fast_path/02_defer_content.md rename to .agents/phases/complete/185_sync_defer_content_fast_path/02_defer_content.md diff --git a/.agents/phases/todo/185_sync_defer_content_fast_path/03_e2e_deferred_content.md b/.agents/phases/complete/185_sync_defer_content_fast_path/03_e2e_deferred_content.md similarity index 100% rename from .agents/phases/todo/185_sync_defer_content_fast_path/03_e2e_deferred_content.md rename to .agents/phases/complete/185_sync_defer_content_fast_path/03_e2e_deferred_content.md diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.err b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.md b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.md new file mode 100644 index 0000000..1d35b39 --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.md @@ -0,0 +1,20 @@ +All gates green. Report: + +**Implemented / verified (phase 185)** +- `_doc_row_stmt` is the ONE deferral helper for all four fast-path row loads; only `Document.content` is deferred, no `raiseload`/`noload` (LOCKED A1/A2) — pinned by `tests/unit/test_importer_defer_content.py`. +- `tests/integration/test_import_fast_path_columns.py` (5), `tests/e2e/test_fast_path_deferred_content.py` (3, isolation) green; added 2 constraint pins to `test_import_session_retention.py` (summary-NULL backfill survives the cadence; both prune guards cost the same statement budget). +- **Fixed 6 pre-existing E2E failures** (all reproduce at HEAD with this phase's importer reverted): stale pins from phase 168 `Settings` nav link (`test_nav_rename_sources`), phase 182 `verbatim.js` script count (`test_steering`), phase 175 `/api/chats?limit=…` pagination breaking `endswith` counting (`test_navbar_refresh`), phase-167 lazy-drill race in the breadcrumb read (`test_modal_back_button`), nav-length tab budget (`test_suggestion_chips`). +- **Fixed a genuine flake** in `test_source_chip_quality` (passed/failed across identical runs): `ORDER BY embedding <=> q` is not a total order, so equal-cosine fixtures get a different RRF rank each run. Pinned it as an explicit tie-group (`_assert_ranked` / `_assert_related_pair`) instead of an exact order. + +**Tests / lint (all 100% green)** +- `uv run pytest --cov=app --cov-report=term-missing` → **4519 passed**, `app/rag/importer.py` **99%**, `TOTAL` **99%** (>90%). +- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors. +- Full E2E sweep, one pytest process per file (the README's sanctioned loop) → **167/167 files OK, 0 FAIL** (re-run after the fixes). + +**Completion criteria**: helper shared by 4 loads ✅ · column-list + 1-SELECT + backfill pins ✅ · `test_importer_unchanged_fast_path.py` 24 passed, zero expectation edits ✅ · full suite + coverage + lint/types ✅ · phase E2E in isolation ✅ · docs accurate ✅ · commit left to the harness (no git commands run). + +**Documentation**: README + AGENTS.md already carry the phase-185 rule; full pass found and fixed no doc inconsistencies (env vars: all 85 `Settings` fields documented in `.env.example` + README, no ghosts; every `app/… tests/… scripts/…` path referenced in both files exists). + +**Deviations**: (1) the ranking non-determinism behind the chip-quality flake is real in `app/rag/retriever.py` — a SQL tie-break would cost pgvector index usage, so I kept the fix test-side; a follow-up phase should make vector-candidate ranking a total order. (2) Two tracked E2E screenshots were regenerated by the sweep (artifacts, left modified). + +**Next pending phase**: `186_git_log_streaming_dates`. diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.md.status b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.validate b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.validate new file mode 100644 index 0000000..ffe0103 --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__00_phase.a1.validate @@ -0,0 +1,143 @@ +........................................................................ [ 1%] +........................................................................ [ 3%] +........................................................................ [ 4%] +........................................................................ [ 6%] +........................................................................ [ 7%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 12%] +........................................................................ [ 14%] +........................................................................ [ 15%] +........................................................................ [ 17%] +........................................................................ [ 19%] +........................................................................ [ 20%] +........................................................................ [ 22%] +........................................................................ [ 23%] +........................................................................ [ 25%] +........................................................................ [ 27%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 31%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 36%] +........................................................................ [ 38%] +........................................................................ [ 39%] +........................................................................ [ 41%] +........................................................................ [ 43%] +........................................................................ [ 44%] +........................................................................ [ 46%] +........................................................................ [ 47%] +........................................................................ [ 49%] +........................................................................ [ 50%] +........................................................................ [ 52%] +........................................................................ [ 54%] +........................................................................ [ 55%] +........................................................................ [ 57%] +........................................................................ [ 58%] +........................................................................ [ 60%] +........................................................................ [ 62%] +........................................................................ [ 63%] +........................................................................ [ 65%] +........................................................................ [ 66%] +........................................................................ [ 68%] +........................................................................ [ 70%] +........................................................................ [ 71%] +........................................................................ [ 73%] +........................................................................ [ 74%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 79%] +........................................................................ [ 81%] +........................................................................ [ 82%] +........................................................................ [ 84%] +........................................................................ [ 86%] +........................................................................ [ 87%] +........................................................................ [ 89%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 94%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [ 98%] +....................................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/admin_settings.py 114 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 292 1 99% +app/api/chat_images.py 52 0 100% +app/api/chats.py 111 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 239 1 99% +app/api/git_sources.py 259 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 204 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 318 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/compression.py 59 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 43 0 100% +app/core/errors.py 5 0 100% +app/core/http_cache.py 22 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/response_budget.py 63 0 100% +app/core/security_headers.py 20 0 100% +app/core/settings_catalog.py 18 0 100% +app/core/settings_migrate.py 32 0 100% +app/core/settings_overlay.py 111 0 100% +app/core/theming.py 40 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 105 0 100% +app/models.py 141 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 494 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 220 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 134 0 100% +app/rag/git_sources.py 77 0 100% +app/rag/importer.py 864 3 99% +app/rag/llm.py 253 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 78 0 100% +app/rag/pdf.py 192 0 100% +app/rag/prompts.py 162 0 100% +app/rag/retriever.py 252 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 58 0 100% +app/rag/svg_text.py 111 0 100% +app/rag/verbatim.py 160 0 100% +app/schemas.py 549 0 100% +-------------------------------------------------- +TOTAL 6872 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.err b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.md b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.md new file mode 100644 index 0000000..79d422e --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.md @@ -0,0 +1,18 @@ +**Task 01 complete — probe written, green, and it pins the defect.** + +**Implemented** +- `tests/integration/test_import_fast_path_columns.py` (new, test-only; no `app/` change): fixture KB of 4 × ~8 KB `.md` files seeded through the real `import_sources`, then a SECOND unchanged `import_sources` captured via a `sqlalchemy.event` `before_cursor_execute` listener (phase-183 technique, removed in teardown) with `(statement, params)` so every claim is attributable per file/PK. +- Helpers `doc_selects()` / `loads_content()` (projection-only, `\bdocuments\.content\b` token so `content_hash` can never match — stated in the comment), plus `row_loads()` (the `(source, path)` fast-path shape), `pk_followups()` (the deferred-column lazy-load shape) and a matcher self-test. +- `test_unchanged_run_hydrates_content_pre_fix`: 4 row loads, **all naming `documents.content`**, 0 PK follow-ups, +2 per-source image post-passes pinned apart; carries `# task 02 flips this`. +- `test_unchanged_file_is_one_select`: exactly **1** documents SELECT per summary-bearing file, 0 follow-ups — explicitly marked MUST-STAY-GREEN post-fix (the anti-lazy-load-storm guard). +- `test_summary_null_backfill_reads_stored_content`: staged `summary=NULL` row (stale `is_summary` chunk stamped) → backfilled from the stored markdown (the `lite` call receives `documents.content` verbatim), chunk replaced; `EXPECTED_EXTRA_DOC_SELECTS_FOR_BACKFILL = 0` → 1 with the arithmetic in the comment. +- D4 reference test: unchanged-run counts (`unchanged=4`, `summary_backfilled=1`, `summaries=0`, `chunks=0`, `changed_paths=[]`) + verbatim log words (`import: unchanged` ×4, `import: summary-backfill` ×1, no `import: summary source=`/`date-refreshed`) and the intact KB. +- Module docstring records the baseline (17,407 rows · avg 15,217 chars · 252.6 MB), the mechanism (`_index_file` whole-entity load + `expire_on_commit=False`), the captured statement verbatim, the post-fix lazy-load shape measured via an ad-hoc `defer(Document.content)`, and the verdict **CONFIRMED — the fast path SELECTs `content` for every file inspected**. + +**Gates** — `uv run pytest tests/integration/test_import_fast_path_columns.py -v` → 5 passed; `uv run pytest --cov=app --cov-report=term-missing` → **4511 passed, 0 failed, exit 0**, `app/` coverage **99%** (importer 99%); `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors. + +**Docs** — no doc impact (test-only; the README/AGENTS sentences belong to tasks 02/03 per the phase plan). + +**Notable decisions** — the two per-source image post-pass SELECTs (`_enrich_pending_images`, `_backfill_image_contexts`) still load `content` and are deliberately *out of scope* (once per source, not per file), so the flip target is the `row_loads()` subset, not "every documents SELECT" — documented so task 02 flips cleanly. Three earlier `test_sync_api.py`/`test_chat_api.py` errors were self-inflicted: I ran probe tests concurrently against the shared Postgres; the serial re-run is fully green, no pre-existing defects. + +**Next pending task:** `.agents/phases/todo/185_sync_defer_content_fast_path/02_defer_content.md` (wire the shared `_doc_row_stmt`/`defer(Document.content)` helper into the four fast-path row loads, flip the probe). diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.md.status b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.validate b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.validate new file mode 100644 index 0000000..83c766f --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__01_fast_path_statement_probe.a1.validate @@ -0,0 +1,143 @@ +........................................................................ [ 1%] +........................................................................ [ 3%] +........................................................................ [ 4%] +........................................................................ [ 6%] +........................................................................ [ 7%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 12%] +........................................................................ [ 14%] +........................................................................ [ 15%] +........................................................................ [ 17%] +........................................................................ [ 19%] +........................................................................ [ 20%] +........................................................................ [ 22%] +........................................................................ [ 23%] +........................................................................ [ 25%] +........................................................................ [ 27%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 31%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 36%] +........................................................................ [ 38%] +........................................................................ [ 39%] +........................................................................ [ 41%] +........................................................................ [ 43%] +........................................................................ [ 44%] +........................................................................ [ 46%] +........................................................................ [ 47%] +........................................................................ [ 49%] +........................................................................ [ 51%] +........................................................................ [ 52%] +........................................................................ [ 54%] +........................................................................ [ 55%] +........................................................................ [ 57%] +........................................................................ [ 59%] +........................................................................ [ 60%] +........................................................................ [ 62%] +........................................................................ [ 63%] +........................................................................ [ 65%] +........................................................................ [ 67%] +........................................................................ [ 68%] +........................................................................ [ 70%] +........................................................................ [ 71%] +........................................................................ [ 73%] +........................................................................ [ 75%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 79%] +........................................................................ [ 81%] +........................................................................ [ 82%] +........................................................................ [ 84%] +........................................................................ [ 86%] +........................................................................ [ 87%] +........................................................................ [ 89%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 94%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [ 98%] +............................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/admin_settings.py 114 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 292 1 99% +app/api/chat_images.py 52 0 100% +app/api/chats.py 111 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 239 1 99% +app/api/git_sources.py 259 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 204 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 318 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/compression.py 59 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 43 0 100% +app/core/errors.py 5 0 100% +app/core/http_cache.py 22 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/response_budget.py 63 0 100% +app/core/security_headers.py 20 0 100% +app/core/settings_catalog.py 18 0 100% +app/core/settings_migrate.py 32 0 100% +app/core/settings_overlay.py 111 0 100% +app/core/theming.py 40 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 105 0 100% +app/models.py 141 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 494 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 220 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 134 0 100% +app/rag/git_sources.py 77 0 100% +app/rag/importer.py 862 3 99% +app/rag/llm.py 253 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 78 0 100% +app/rag/pdf.py 192 0 100% +app/rag/prompts.py 162 0 100% +app/rag/retriever.py 252 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 58 0 100% +app/rag/svg_text.py 111 0 100% +app/rag/verbatim.py 160 0 100% +app/schemas.py 549 0 100% +-------------------------------------------------- +TOTAL 6870 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.err b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.md b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.md new file mode 100644 index 0000000..564c1e6 --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.md @@ -0,0 +1,15 @@ +**Implemented (phase 185 task 02)** +- `app/rag/importer.py`: new module-level `_doc_row_stmt(source, rel)` (next to `_file_hash`) returning `select(Document).options(defer(Document.content)).where(source, path)`; `Select` + `defer` imports added. Only `content` is deferred — no `load_only`, no `raiseload`/`noload` (LOCKED A1/A2). +- All four fast-path row loads now share it: `_index_file` L1557, `_index_pdf_file` L2018, `_index_one_embedded_image` L2777 (`image_doc`), `_index_image_file` L3777. Surrounding logic untouched. +- Fifth-site note: the only other `select(Document)`s are the two per-source image post-passes (L3336/L3345, `_backfill_image_contexts` L3495) and `_prune`'s phase-184 `load_only` light row (L4076) — NOT fast-path loads, left as-is by D1. +- Docstrings: module phase-147 paragraph, `_index_file`, `_unchanged_doc` step-3 comment (the backfill is the ONE read that pulls deferred `content` back — one SELECT, one time). +- Probe flipped in `tests/integration/test_import_fast_path_columns.py`: `EXPECTED_EXTRA_DOC_SELECTS_FOR_BACKFILL = 1`, `test_unchanged_run_hydrates_content_pre_fix` → `test_unchanged_run_defers_content_post_fix` (no `documents.content` in the fast-path SELECT, exactly 1 PK follow-up run-wide), docstring verdict **CONFIRMED → FIXED** with the post-fix captured statement (verified verbatim against a live capture) and the 6→7 statement arithmetic. +- New `tests/unit/test_importer_defer_content.py` (the D1 unit pins): `content` is the only column missing from the compiled projection vs the full mapper set; predicate stays `(source, path)`; all four functions call the helper and none builds its own `select(Document)`; exactly 4 call sites; AST guard that no `raiseload`/`noload` is called/imported; real-session read-back of the deferred column. + +**Gates** — `uv run pytest` → **4517 passed, 0 failed**; `uv run pytest --cov=app --cov-report=term-missing` → **app/ 99%** (importer.py 99%), >90% met; `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors. `test_importer_unchanged_fast_path.py` green with **zero edited expectations** (no harness change was needed — those tests use the real Postgres session, so the lazy load works as-is). Extra regression runs: `test_sync_changed_only.py` (3), `test_document_viewer.py` + `test_document_summaries.py` (12), `test_sync_session_reset.py` (4) all pass. + +**Docs** — `README.md`: fast-path row lookup is metadata-only + a new "A full walk reads row metadata, not the corpus (phase 185)" bullet under *Sync from the UI*. `AGENTS.md`: new operating rule 15 (share `_doc_row_stmt`, never a fifth whole-entity load, never `load_only`/`raiseload`/`noload`, pinned by the two test files). + +**Deviations** — none functional; the phase's D1 "unit pin" landed in a new file so the phase-147 matrix stayed byte-untouched. + +**Next pending task:** `.agents/phases/todo/185_sync_defer_content_fast_path/03_e2e_deferred_content.md`. diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.md.status b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.validate b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.validate new file mode 100644 index 0000000..9be08e5 --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__02_defer_content.a1.validate @@ -0,0 +1,143 @@ +........................................................................ [ 1%] +........................................................................ [ 3%] +........................................................................ [ 4%] +........................................................................ [ 6%] +........................................................................ [ 7%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 12%] +........................................................................ [ 14%] +........................................................................ [ 15%] +........................................................................ [ 17%] +........................................................................ [ 19%] +........................................................................ [ 20%] +........................................................................ [ 22%] +........................................................................ [ 23%] +........................................................................ [ 25%] +........................................................................ [ 27%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 31%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 36%] +........................................................................ [ 38%] +........................................................................ [ 39%] +........................................................................ [ 41%] +........................................................................ [ 43%] +........................................................................ [ 44%] +........................................................................ [ 46%] +........................................................................ [ 47%] +........................................................................ [ 49%] +........................................................................ [ 51%] +........................................................................ [ 52%] +........................................................................ [ 54%] +........................................................................ [ 55%] +........................................................................ [ 57%] +........................................................................ [ 58%] +........................................................................ [ 60%] +........................................................................ [ 62%] +........................................................................ [ 63%] +........................................................................ [ 65%] +........................................................................ [ 66%] +........................................................................ [ 68%] +........................................................................ [ 70%] +........................................................................ [ 71%] +........................................................................ [ 73%] +........................................................................ [ 74%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 79%] +........................................................................ [ 81%] +........................................................................ [ 82%] +........................................................................ [ 84%] +........................................................................ [ 86%] +........................................................................ [ 87%] +........................................................................ [ 89%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 94%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [ 98%] +..................................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/admin_settings.py 114 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 292 1 99% +app/api/chat_images.py 52 0 100% +app/api/chats.py 111 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 239 1 99% +app/api/git_sources.py 259 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 204 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 318 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/compression.py 59 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 43 0 100% +app/core/errors.py 5 0 100% +app/core/http_cache.py 22 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/response_budget.py 63 0 100% +app/core/security_headers.py 20 0 100% +app/core/settings_catalog.py 18 0 100% +app/core/settings_migrate.py 32 0 100% +app/core/settings_overlay.py 111 0 100% +app/core/theming.py 40 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 105 0 100% +app/models.py 141 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 494 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 220 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 134 0 100% +app/rag/git_sources.py 77 0 100% +app/rag/importer.py 864 3 99% +app/rag/llm.py 253 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 78 0 100% +app/rag/pdf.py 192 0 100% +app/rag/prompts.py 162 0 100% +app/rag/retriever.py 252 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 58 0 100% +app/rag/svg_text.py 111 0 100% +app/rag/verbatim.py 160 0 100% +app/schemas.py 549 0 100% +-------------------------------------------------- +TOTAL 6872 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.err b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.md b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.md new file mode 100644 index 0000000..2d13467 --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.md @@ -0,0 +1,17 @@ +**Task 03 complete — phase 185's dedicated Playwright suite is green in isolation and the whole project is 100% green.** + +**Implemented** +- `tests/e2e/test_fast_path_deferred_content.py` (new, mock-only, dedicated module app on port 8202, dedicated `BOR_SOURCES_DIR`, per-test truncate before/after + settled-sync guard, fixture tree re-seeded to original bytes each test so single-test re-runs stay honest): **T1** hand-nulled `documents.summary` + deleted stale `is_summary` chunk → Sources-page re-sync settles `0 added · 2 unchanged` (`summaries`/`dates_updated` stay 0), summary comes back byte-identical from the STORED content through the one deferred-column lazy read, exactly one `is_summary` chunk, viewer shows it; **T2** untouched re-sync then viewer renders the FULL stored text (head/tail/middle sentinels + every stored line, whitespace-collapsed); **T3** rewritten file → `0 added · 1 updated · 1 unchanged`, stored content equals new bytes, chunk count moved, neighbour hashes byte-still, viewer shows only the new text, third sync all-`unchanged`. Docstring: incident, A1/A2 deferral, test→observable mapping, isolation command. +- Docs: README phase-185 operations note was already landed (task 02); extended the README and AGENTS (rule 15) pin lists to cite the new E2E suite; full review of both files surfaced no other inconsistencies (no test asserts doc contents). + +**Gates (exact)** +- `uv run pytest tests/e2e/test_fast_path_deferred_content.py -v --no-cov` → **3 passed** (in isolation) +- `uv run pytest tests/e2e/test_document_dates.py -v --no-cov` → **6 passed**; `…test_sync_changed_only.py -v --no-cov` → **3 passed** (in isolation) +- `uv run pytest --cov=app --cov-report=term-missing` → **4517 passed, 0 failed**, `app/` coverage **99%** (importer 99%) — >90% met +- `uv run ruff check .` → All checks passed; `uv run pyright` → **0 errors** + +**Docs** — `README.md` + `AGENTS.md`: phase-185 pin lists now include the E2E suite; no other doc impact. + +**Notable** — Commit + phase-dir move left to the harness per pipeline rules (project per-phase-commit instruction overridden). Two transient E2E failures during development were my own fixture bugs (missing source registration per test), not app defects; the shared Postgres was left clean. + +**Next pending task:** none in `todo/185_sync_defer_content_fast_path/` — the phase completes with this task (harness commit + move outstanding). diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.md.status b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.md.status new file mode 100644 index 0000000..9766475 --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.md.status @@ -0,0 +1 @@ +ok diff --git a/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.validate b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.validate new file mode 100644 index 0000000..9be08e5 --- /dev/null +++ b/.agents/reports/185_sync_defer_content_fast_path/185_sync_defer_content_fast_path__03_e2e_deferred_content.a1.validate @@ -0,0 +1,143 @@ +........................................................................ [ 1%] +........................................................................ [ 3%] +........................................................................ [ 4%] +........................................................................ [ 6%] +........................................................................ [ 7%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 12%] +........................................................................ [ 14%] +........................................................................ [ 15%] +........................................................................ [ 17%] +........................................................................ [ 19%] +........................................................................ [ 20%] +........................................................................ [ 22%] +........................................................................ [ 23%] +........................................................................ [ 25%] +........................................................................ [ 27%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 31%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 36%] +........................................................................ [ 38%] +........................................................................ [ 39%] +........................................................................ [ 41%] +........................................................................ [ 43%] +........................................................................ [ 44%] +........................................................................ [ 46%] +........................................................................ [ 47%] +........................................................................ [ 49%] +........................................................................ [ 51%] +........................................................................ [ 52%] +........................................................................ [ 54%] +........................................................................ [ 55%] +........................................................................ [ 57%] +........................................................................ [ 58%] +........................................................................ [ 60%] +........................................................................ [ 62%] +........................................................................ [ 63%] +........................................................................ [ 65%] +........................................................................ [ 66%] +........................................................................ [ 68%] +........................................................................ [ 70%] +........................................................................ [ 71%] +........................................................................ [ 73%] +........................................................................ [ 74%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 79%] +........................................................................ [ 81%] +........................................................................ [ 82%] +........................................................................ [ 84%] +........................................................................ [ 86%] +........................................................................ [ 87%] +........................................................................ [ 89%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 94%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [ 98%] +..................................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/admin_settings.py 114 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 292 1 99% +app/api/chat_images.py 52 0 100% +app/api/chats.py 111 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 239 1 99% +app/api/git_sources.py 259 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 204 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 318 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/compression.py 59 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 43 0 100% +app/core/errors.py 5 0 100% +app/core/http_cache.py 22 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/response_budget.py 63 0 100% +app/core/security_headers.py 20 0 100% +app/core/settings_catalog.py 18 0 100% +app/core/settings_migrate.py 32 0 100% +app/core/settings_overlay.py 111 0 100% +app/core/theming.py 40 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 105 0 100% +app/models.py 141 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 494 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 220 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 134 0 100% +app/rag/git_sources.py 77 0 100% +app/rag/importer.py 864 3 99% +app/rag/llm.py 253 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 78 0 100% +app/rag/pdf.py 192 0 100% +app/rag/prompts.py 162 0 100% +app/rag/retriever.py 252 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 58 0 100% +app/rag/svg_text.py 111 0 100% +app/rag/verbatim.py 160 0 100% +app/schemas.py 549 0 100% +-------------------------------------------------- +TOTAL 6872 16 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/screenshots/166_pre_answer_window_1280.png b/.agents/screenshots/166_pre_answer_window_1280.png index 350bd53b235b9c29e8bf29854601f49b1408a3d0..0ad1f103d8c438bb9dec8c18da42fca3171e5aac 100644 GIT binary patch literal 71076 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

Gj;!n7>+ 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

x~* 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%0DGh9(``LN-I$8` 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 U%<z>% literal 69542 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%0AFgi(}0DSti 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 str: return h.hexdigest() +def _doc_row_stmt(source: str, rel: str) -> Select[tuple[Document]]: + """The importer's single Document-row statement (phase 185). + + Used by the FOUR loads that share the phase-147 unchanged fast path — + :func:`_index_file`, :func:`_index_pdf_file`, + :func:`_index_one_embedded_image` and :func:`_index_image_file` — so + the deferral below cannot drift between them. + + ``defer(Document.content)`` — the unchanged fast path (phase 147) reads + file_hash/is_image/page_count/summary/dates, never the stored markdown, + and ``app/db.py`` builds sessions with ``expire_on_commit=False``, so + hydrating ``content`` for EVERY inspected file is what took the + 2026-10-15 run to ~250 MB on a 17,407-document KB (avg 15,217 + chars/row): the walk inspected ~27k files and re-indexed 217. + + ONE column is deferred on purpose (LOCKED A1): a ``load_only`` + allowlist would silently defer every column a later phase adds, and the + deferred read would then surface as an unexplained N+1. No + ``raiseload``/``noload`` either (LOCKED A2): the phase-118 summary-NULL + backfill (``_unchanged_doc`` step 3) genuinely reads ``doc.content`` and + must keep working — it costs one extra SELECT for that one row, a + per-row, one-time, already-rare cost, accepted instead of a rule that + would crash the branch where it should query. + """ + return ( + select(Document) + .options(defer(Document.content)) + .where(Document.source == source, Document.path == rel) + ) + + async def import_sources( sources: list[Path], llm: Embedder, @@ -1194,7 +1233,10 @@ async def _unchanged_doc( # branch paths — the next sync is then fast either way). doc.file_hash = file_digest # 3 — the phase-118 (A2) summary-NULL backfill, from the stored - # content (see the docstring — never a re-read/re-extract). + # content (see the docstring — never a re-read/re-extract). Phase 185: + # this is the ONE fast-path read that pulls the deferred ``content`` + # back — one SELECT, one time, by design (LOCKED A2: no raiseload, the + # branch must query rather than crash). if doc.summary is None: await _store_summary( session, doc=doc, source=source, rel=rel, content=doc.content, @@ -1457,8 +1499,12 @@ async def _index_file( Phase 147 (task 02): the UNCHANGED FAST PATH. BEFORE the image/text/PDF dispatch, the file's RAW source bytes are hashed (:func:`_file_hash` — streamed), the ``(source, path)`` row is - looked up, and the D2 mtime fallback for *raw_date* runs (once, - here — the branches receive the already-computed date) — a + looked up through the shared :func:`_doc_row_stmt` (phase 185: with + ``Document.content`` DEFERRED — the fast path reads row METADATA only, + so a full walk no longer hydrates the KB's markdown; the phase-118 + summary-NULL backfill is the one read that pulls the text back, one + SELECT for that row), and the D2 mtime fallback for *raw_date* runs + (once, here — the branches receive the already-computed date) — a ``doc.file_hash == file_digest`` match returns through the shared unchanged path (:func:`_unchanged_doc`) with ZERO file read beyond the streamed hash, ZERO PDF extraction, ZERO vision call, and @@ -1508,7 +1554,7 @@ async def _index_file( # ``read_text``/``read_bytes``, no ``extract_pdf_text``, no # fallback/pictorial vision, no re-chunk/re-embed/re-summary. file_digest = _file_hash(full_path) - doc = session.scalar(select(Document).where(Document.source == source, Document.path == rel)) + doc = session.scalar(_doc_row_stmt(source, rel)) if raw_date is None: # D2 fallback: no source date in the map → the file's mtime # (one stat). Read before the unchanged early-return — the @@ -1969,7 +2015,7 @@ async def _index_pdf_file( else hashlib.sha256(b"").hexdigest() ) title = _extract_pdf_title(full_path, fallback=full_path.stem) - doc = session.scalar(select(Document).where(Document.source == source, Document.path == rel)) + doc = session.scalar(_doc_row_stmt(source, rel)) if doc is not None and doc.content_hash == digest: # Phase 147: the shared unchanged path — a pre-phase NULL-hash # row, or a bytes-changed/content-same PDF (the content digest @@ -2728,9 +2774,7 @@ async def _index_one_embedded_image( return image_rel = f"{rel}#page={img.page}#img={img.index}{ext}" digest = hashlib.sha256(img.data).hexdigest() - image_doc = session.scalar( - select(Document).where(Document.source == source, Document.path == image_rel) - ) + image_doc = session.scalar(_doc_row_stmt(source, image_rel)) if image_doc is not None and image_doc.content_hash == digest: # Unchanged embedded image (byte digest) — no re-describe, no # re-embed (the standalone image's unchanged rule). @@ -3730,7 +3774,7 @@ async def _index_image_file( # byte change. data = full_path.read_bytes() digest = hashlib.sha256(data).hexdigest() - doc = session.scalar(select(Document).where(Document.source == source, Document.path == rel)) + doc = session.scalar(_doc_row_stmt(source, rel)) if doc is not None and doc.is_image and doc.content_hash == digest: # Phase 147: the shared unchanged path — a pre-phase NULL-hash diff --git a/tests/e2e/test_fast_path_deferred_content.py b/tests/e2e/test_fast_path_deferred_content.py new file mode 100644 index 0000000..d60d772 --- /dev/null +++ b/tests/e2e/test_fast_path_deferred_content.py @@ -0,0 +1,760 @@ +"""Phase 185 E2E (Playwright, mock-only): the deferred column still shows up +where the owner reads it. + +Source: owner incident 2026-10-15 (the same run phases 183/184 fixed). The +unchanged fast path looked each file up by a WHOLE-ENTITY ``select(Document)`` +— ``documents.content`` included — and ``app/db.py`` builds sessions with +``expire_on_commit=False``, so the 2026-10-15 sync inspected ~27k files and +held ~252.6 MB of markdown (17,407 rows × avg 15,217 chars) resident to +discover that 217 files had changed. + +The fix (``app/rag/importer.py``): the four fast-path row loads share the ONE +statement ``_doc_row_stmt``, which defers ``Document.content`` — exactly ONE +column (LOCKED A1: no ``load_only`` allowlist, so a future column can never +silently become lazy) and deliberately NO ``raiseload``/``noload`` (LOCKED +A2: the phase-118 summary-NULL backfill genuinely reads the stored markdown +and must pay one per-row, one-time SELECT instead of crashing). The +statement-level proof — the captured fast-path ``SELECT`` names no +``documents.content``, one ``SELECT`` per unchanged file with a summary, one +lazy follow-up for the backfill — lives in +``tests/integration/test_import_fast_path_columns.py``. What a browser can +see is what THIS module pins: the deferral is invisible to the owner. + +Run in isolation (DB must be up: ``podman compose up -d db``): + + uv run pytest tests/e2e/test_fast_path_deferred_content.py -v --no-cov + +MOCK-ONLY suite: ``E2E_REAL_LLM=1`` is skipped — the backfilled summary is +compared BYTE-IDENTICAL against the summary the first import stored, which +is a property of the deterministic mock summarizer; against the real endpoint +the comparison would be a measurement, not a pin. + +One module app on a dedicated port (the conftest session app owns 8123 in a +combined run), booting against the mock LLM with ``BOR_GIT_SOURCES`` forced +empty (an operator's local ``.env`` must never leak a real source into a +run) and its own ``BOR_SOURCES_DIR`` / ``BOR_PDF_PAGE_DIR`` under +``tmp_path_factory``. The fixture KB is ONE plain (non-git) directory named +:data:`SOURCE_NAME` (``kind='local'`` rows index under ``Path(path).name``, +so the source label is stable whatever temp root pytest picked) holding TWO +``.md`` files; both carry a sentinel phrase on their FIRST content line and +another on their LAST line, so "the viewer shows the FULL stored text" is +provable at both ends of a file. + +Isolation: the shared E2E Postgres is truncated BEFORE **and** AFTER every +test (``chunks, documents, query_log, kb_overview, git_sources, sources_meta, +folder_summaries``), after waiting for the app to hold no running sync (the +run state lives in the app's memory — a leaked run would keep importing into +the next test's truncated KB). The fixture TREE is re-seeded to its original +bytes at the top of every test too (test 3 rewrites one file — a lone re-run +of this module must still see ``2 added``, not ``0 added · 2 unchanged``), +and each test registers its own source row. + +Test → observable mapping (Playwright Mapping Rule): +1. ``test_summary_null_backfill_survives_the_deferral`` — with one row's + ``documents.summary`` nulled AND its stale ``is_summary`` chunk deleted + directly (the phase-118 staging, extended so the chunk's REAPPEARANCE + proves the backfill ran), the Sources-page re-sync still settles + ``0 added · 2 unchanged`` (a backfill is not an ``updated``/``added``), + the document page shows a non-empty summary identical to the one run 1 + stored — produced from the STORED content through the deferred-column + lazy read — and exactly ONE ``is_summary`` chunk exists for the doc + (no duplicate). +2. ``test_unchanged_document_viewer_shows_full_stored_text`` — a second + sync of untouched files settles ``0 added · 2 unchanged`` and the + document view then renders the FULL stored text: the head AND the tail + sentinel both present, every stored line present, no truncation, no + empty body. The row lookup the deferral thinned is the same lookup the + fast path answers with; the viewer reads its row separately and still + gets every byte. +3. ``test_changed_file_reindexes_under_the_deferral`` — one file is + rewritten on disk (new head/tail sentinels, more sections, different + length); the re-sync settles ``0 added · 1 updated · 1 unchanged``, the + viewer shows the NEW text with NO stale fragment, the stored content + equals the new file bytes, the content-chunk count moved, the + untouched neighbour row did not, and a THIRD sync reports all + ``unchanged`` — the fast path still fires after a deferral-mediated + write. +""" +from __future__ import annotations + +import os +import re +import subprocess +import sys +import time +from collections.abc import Iterator +from datetime import datetime +from pathlib import Path +from typing import Any + +import httpx +import pytest +from playwright.sync_api import Page, expect +from sqlalchemy import delete, func, select, text + +from app.db import SessionLocal +from app.models import Chunk, Document +from e2e.auth_helpers import login +from e2e.conftest import ( + ADMIN_PASSWORD, + MOCK_PORT, + SESSION_SECRET, + USE_REAL_LLM, + _wait_http, +) + +REPO = Path(__file__).resolve().parents[2] + +#: Dedicated module port (the house env-overridable pattern): the conftest +#: session app owns 8123 in a combined run. +APP_PORT = int(os.environ.get("E2E_APP_PORT_DEFERCONTENT", "8202")) +APP_URL = f"http://127.0.0.1:{APP_PORT}" + +SOURCES_URL = "/sources.html" + +#: The fixture directory's basename — the ``source`` the importer records for +#: a ``kind='local'`` row (``Path(path).name``), stable whatever temp root +#: this run got. +SOURCE_NAME = "defer-fast-path" + +ALPHA_PATH = "notes/alpha.md" +BETA_PATH = "notes/beta.md" +N_FILES = 2 + +#: Head/tail sentinels: the FIRST content line and the LAST line of each +#: fixture file, so "the viewer shows the FULL stored text" is proven at +#: both ends (test 2), "no stale fragment" is proven at both ends (test 3). +ALPHA_HEAD = "RESE-DEFER-ALPHA-HEAD-3c81" +ALPHA_TAIL = "RESE-DEFER-ALPHA-TAIL-7e52" +ALPHA_MIDDLE = "RESE-DEFER-ALPHA-MIDDLE-0f64" +BETA_HEAD = "RESE-DEFER-BETA-HEAD-5b19" +BETA_TAIL = "RESE-DEFER-BETA-TAIL-8a64" +#: What test 3 rewrites alpha.md into (and what must have replaced every +#: ALPHA_* marker in the KB and in the viewer). +REVISED_HEAD = "RESE-DEFER-ALPHA2-HEAD-e208" +REVISED_TAIL = "RESE-DEFER-ALPHA2-TAIL-41cb" + +#: Test 1 stages THIS row (phase-118's summary-NULL backfill case). +BACKFILL_PATH = BETA_PATH + +#: "Synced HH:MM" — the local-time last-result label (sources.js's +#: fmtSyncTime), any hour/minute (the test_sync_button.py pattern). +SYNCED_LABEL = re.compile(r"Synced \d{1,2}:\d{2}$") + +#: A two-file mock sync (walk + embeds + summary + KB overview) lands in +#: seconds; this is an order of magnitude of head-room with no client-side +#: hard timeout (the test_sync_session_reset.py budget). +SETTLE_TIMEOUT_MS = 90_000 + + +# --------------------------------------------------------------------------- +# The fixture source +# --------------------------------------------------------------------------- + + +def _alpha_text() -> str: + """The ORIGINAL bytes of ``notes/alpha.md`` (test 2's full-text subject, + test 3's rewrite subject). Two H2 sections — a section is the md chunker's + unit, so the rewrite's extra sections move the chunk count.""" + return ( + "# Deferred content alpha\n" + "\n" + f"Head marker {ALPHA_HEAD} opens this file.\n" + "\n" + f"Middle marker {ALPHA_MIDDLE} sits between the sections.\n" + "\n" + "## Alpha body\n" + "\n" + "Alpha prose that the unchanged fast path must never need to read: " + "the walk wants the row metadata, not these paragraphs. It keeps " + "growing so the file is unmistakably a document and not a stub.\n" + "\n" + "## Alpha notes\n" + "\n" + "More alpha prose so the content chunk count is more than one. Each " + "section is its own chunk, which is what test 3 counts when it " + "rewrites this file under the deferral.\n" + "\n" + f"Tail marker {ALPHA_TAIL} closes this file.\n" + ) + + +def _revised_alpha_text() -> str: + """The rewrite: NEW head/tail sentinels, two extra sections, clearly + different length — every old marker is gone from the bytes.""" + return ( + "# Deferred content alpha revised\n" + "\n" + f"Head marker {REVISED_HEAD} opens the revised file.\n" + "\n" + "## Alpha body\n" + "\n" + "The revised alpha prose. The deferral changed which columns the " + "unchanged fast path reads; a CHANGED file must still re-index its " + "full text, so this paragraph and its siblings must land in the KB " + "verbatim, and every marker of the original file must be gone.\n" + "\n" + "## Alpha notes\n" + "\n" + "Revised notes prose, longer than before on purpose so the stored " + "length and the viewer both prove the write path carried the new " + "text through the same session that read the row with content " + "deferred.\n" + "\n" + "## Alpha appendix one\n" + "\n" + "A brand new section that did not exist before the rewrite, added " + "so the content chunk count of this document must change when the " + "re-indexed file is re-chunked.\n" + "\n" + "## Alpha appendix two\n" + "\n" + "A second brand new section, because the point of the phase is " + "that NOTHING about what a run does changed — only which columns " + "travel on an unchanged row lookup.\n" + "\n" + f"Tail marker {REVISED_TAIL} closes the revised file.\n" + ) + + +def _beta_text() -> str: + """``notes/beta.md`` — test 1's backfill subject. Ordinary prose with + its own head/tail sentinels.""" + return ( + "# Deferred content beta\n" + "\n" + f"Head marker {BETA_HEAD} opens this file.\n" + "\n" + "## Beta body\n" + "\n" + "Beta is the document whose stored summary test 1 nulls directly, " + "so the Sources-page re-sync must backfill it from the stored " + "content — the ONE fast-path read that pulls the deferred column " + "back, one SELECT, one time, by design.\n" + "\n" + f"Tail marker {BETA_TAIL} closes this file.\n" + ) + + +def _seed_source(root: Path) -> Path: + """(Re-)write BOTH fixture files under ``root / SOURCE_NAME``. + + Idempotent on purpose: every test calls it first, so the tree is at its + ORIGINAL bytes whatever an earlier test (test 3!) left behind — a lone + re-run of any single test still sees a fresh-KB ``2 added``. + """ + base = root / SOURCE_NAME + files = {ALPHA_PATH: _alpha_text(), BETA_PATH: _beta_text()} + for rel, body in files.items(): + target = base / rel + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(body, encoding="utf-8") + assert not (base / ".git").exists() # a plain directory — no git anywhere + return base + + +@pytest.fixture(scope="module") +def source_dir(tmp_path_factory: pytest.TempPathFactory) -> Path: + """The fixture tree the module app walks (the app server runs on the same + host, so the path is visible to it).""" + return _seed_source(tmp_path_factory.mktemp("bor_fast_path_deferred")) + + +# --------------------------------------------------------------------------- +# The module app +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def app_server( + mock_llm: int, source_dir: Path, tmp_path_factory: pytest.TempPathFactory +) -> Iterator[str]: + """The real app under test — per-module env: the mock LLM, the + mock-calibrated gate (the conftest convention), NO git fallback, and a + dedicated ``BOR_SOURCES_DIR`` so no run writes into the operator's. + + The phase's subject needs no knob: the ``defer(Document.content)`` fast + path is the code default (LOCKED A1/A2) — this app is the app every + other suite runs with, walked over a fixture small enough to read. + """ + if USE_REAL_LLM: + pytest.skip( + "mock-only suite — the byte-identical backfilled summary is a " + "property of the deterministic mock (E2E_REAL_LLM unsupported)" + ) + assert mock_llm and source_dir + scratch = tmp_path_factory.mktemp("bor_fast_path_deferred_app") + env = dict(os.environ) + env.pop("DEBUGPY", None) + env["BOR_ENVIRONMENT"] = "e2e" + env["BOR_STATIC_DIR"] = str(REPO / "frontend") + env["BOR_LLM_BASE_URL"] = f"http://127.0.0.1:{MOCK_PORT}/v1" + # Mock-calibrated gate (the conftest pattern): both floors must stay <= + # the threshold or the app refuses to boot (app/config.py). + env["BOR_RELEVANCE_THRESHOLD"] = "0.30" + env["BOR_LEXICAL_SUPPORT_FLOOR"] = "0.15" + env["BOR_SOURCE_USEFULNESS_FLOOR"] = "0.15" + env.setdefault( + "BOR_DATABASE_URL", + "postgresql+psycopg://reese:reese@localhost:5432/brain_of_reese", + ) + # Phase 16: admin auth must be set or create_app() refuses to boot. + env["BOR_ADMIN_PASSWORD"] = ADMIN_PASSWORD + env["BOR_SESSION_SECRET"] = SESSION_SECRET + # No env-fallback rows: the registry each test writes through the + # authenticated API is the only source list. + env["BOR_GIT_SOURCES"] = "" + env["BOR_SOURCES_DIR"] = str(scratch / "checkouts") + env["BOR_PDF_PAGE_DIR"] = str(scratch / "pdf-pages") + proc = subprocess.Popen( + [ + sys.executable, "-m", "uvicorn", "app.main:app", + "--host", "127.0.0.1", "--port", str(APP_PORT), "--log-level", "warning", + ], + cwd=REPO, + env=env, + ) + try: + _wait_http(f"{APP_URL}/api/health") + yield APP_URL + finally: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + + +@pytest.fixture(scope="module") +def app_url(app_server: str) -> str: + return app_server + + +# --------------------------------------------------------------------------- +# Isolation +# --------------------------------------------------------------------------- + + +def _truncate_all() -> None: + """Fresh registry + KB (the E2E isolation pattern, the phase-184 list: + ``sources_meta`` and ``folder_summaries`` reset with the KB).""" + with SessionLocal() as db: + db.execute( + text( + "TRUNCATE chunks, documents, query_log, kb_overview, " + "git_sources, sources_meta, folder_summaries" + ) + ) + db.commit() # without the commit the TRUNCATE rolls back + + +def _wait_sync_settled(base_url: str, timeout_s: float = 180.0) -> None: + """No run may leak across a truncate: wait for a non-running status with + an own admin session (the endpoint is admin-only). Usually a no-op — the + tests settle before they end.""" + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + with httpx.Client(base_url=base_url, timeout=5.0) as client: + client.post("/api/login", json={"password": ADMIN_PASSWORD}) + r = client.get("/api/sync/status") + assert r.status_code == 200, r.text + if r.json()["state"] != "running": + return + time.sleep(0.2) + raise AssertionError(f"a sync was still running at a test boundary: {base_url}") + + +@pytest.fixture(autouse=True) +def _clean(app_url: str, db_ready: None) -> Iterator[None]: + """Per-test KB isolation: the shared E2E Postgres is truncated before AND + after every test, the app holding no running sync at both boundaries.""" + _wait_sync_settled(app_url) + _truncate_all() + yield + _wait_sync_settled(app_url) + _truncate_all() + + +# --------------------------------------------------------------------------- +# Helpers: the UI side +# --------------------------------------------------------------------------- + + +def _add_local_source(page: Page, base_url: str, path: Path) -> None: + """Register the fixture directory through the authenticated API (the + phase-49 shape — the admin cookie rides ``page.request``).""" + r = page.request.post( + f"{base_url}/api/git-sources", + data={"kind": "local", "path": str(path)}, + ) + assert r.status == 201, f"expected 201 for {path}: {r.status} {r.text}" + + +def _started_at(body: dict[str, Any]) -> float: + """The status body's ``started_at`` as a wall-clock epoch (the server + stamps it in UTC and this process runs on the same host).""" + started = body.get("started_at") + if not started: + return 0.0 + try: + return datetime.fromisoformat(str(started)).timestamp() + except ValueError: + return 0.0 + + +def _sync_from_sources_page(page: Page, base_url: str) -> dict[str, Any]: + """Click the Sources page **Sync sources** button and wait for THIS run's + terminal state (the module app is shared, so a terminal counts only after + a ``running`` tick or when its ``started_at`` post-dates the click). + + Settles the UI on success: the button re-enabled, ``Synced HH:MM``, no + error banner (the test_sync_session_reset.py helper).""" + btn = page.locator("#sync-btn") + expect(btn).to_be_visible(timeout=30_000) + expect(btn).to_be_enabled() + clicked_at = time.time() + btn.click() + expect(btn).to_be_disabled() + expect(page.locator("#sync-label")).to_have_text("Syncing…") + + deadline = time.monotonic() + SETTLE_TIMEOUT_MS / 1000 + saw_running = False + body: dict[str, Any] = {} + while time.monotonic() < deadline: + r = page.request.get(f"{base_url}/api/sync/status") + assert r.status == 200, r.text + body = r.json() + state = body["state"] + if state == "running": + saw_running = True + elif state in ("success", "failed", "cancelled") and ( + saw_running or _started_at(body) >= clicked_at - 1.0 + ): + break + time.sleep(0.2) + else: + raise AssertionError(f"sync never reached a terminal state: {body}") + + assert body["state"] == "success", f"sync failed: {body}" + assert body["error"] is None, body + expect(btn).to_be_enabled(timeout=15_000) + expect(page.locator("#sync-label")).to_have_text(SYNCED_LABEL, timeout=15_000) + expect(page.locator("#sync-error-banner")).to_be_hidden() + return body + + +def _expect_counts_line(page: Page, expected: str) -> None: + """The counts line — the owner-visible sync report on the single sync + text surface (phase 150; sources.js fmtSyncResult; the zero "updated" + part is omitted).""" + expect(page.locator("#sync-progress-text")).to_have_text(expected, timeout=15_000) + + +def _goto_sources(page: Page, base_url: str) -> None: + """Back to the Sources page (the sync button's home) after a viewer + detour — the session cookie rides the navigation.""" + page.goto(base_url + SOURCES_URL) + expect(page.locator("#sync-btn")).to_be_visible(timeout=30_000) + expect(page.locator("#sync-btn")).to_be_enabled() + + +def _open_document_page(page: Page, base_url: str, path: str) -> None: + """Open the standalone viewer for one fixture document (the phase-10 + surface; the session cookie from ``login`` rides the navigation).""" + page.goto( + f"{base_url}/document.html?source={SOURCE_NAME}" + f"&path={path.replace('/', '%2F')}" + ) + expect(page.locator("#doc-content .doc-md")).to_have_count(1, timeout=30_000) + + +# --------------------------------------------------------------------------- +# Helpers: the DB side +# --------------------------------------------------------------------------- + + +def _summary_map() -> dict[str, str | None]: + """``path`` → ``documents.summary`` for both fixture rows.""" + with SessionLocal() as db: + rows = db.execute( + select(Document.path, Document.summary).where( + Document.source == SOURCE_NAME + ) + ).all() + return {str(path): (None if summary is None else str(summary)) for path, summary in rows} + + +def _summary_chunk_contents(path: str) -> list[str]: + """The ``is_summary`` chunk contents of one document — the duplicate + check (phase 30: at most one per doc at a time).""" + with SessionLocal() as db: + return [ + str(content) + for (content,) in db.execute( + select(Chunk.content) + .join(Document, Document.id == Chunk.document_id) + .where( + Document.source == SOURCE_NAME, + Document.path == path, + Chunk.is_summary.is_(True), + ) + ).all() + ] + + +def _content_chunk_counts() -> dict[str, int]: + """``path`` → content-chunk count per fixture doc (the ``position >= 0`` + rows — the ``is_summary`` position −1 chunk stays out).""" + with SessionLocal() as db: + rows = db.execute( + select(Document.path, func.count(Chunk.id)) + .join(Chunk, Chunk.document_id == Document.id) + .where( + Document.source == SOURCE_NAME, + Chunk.position >= 0, + ) + .group_by(Document.path) + ).all() + return {str(path): int(count) for path, count in rows} + + +def _stored_content(path: str) -> str: + with SessionLocal() as db: + content = db.scalar( + select(Document.content).where( + Document.source == SOURCE_NAME, Document.path == path + ) + ) + assert content is not None, f"{path} is not in the KB" + return str(content) + + +def _row_stamp(path: str) -> tuple[Any, Any]: + """``(content_hash, file_hash)`` of one fixture row — the neighbour's + byte-still proof (a re-index would re-stamp both).""" + with SessionLocal() as db: + row = db.execute( + select(Document.content_hash, Document.file_hash).where( + Document.source == SOURCE_NAME, Document.path == path + ) + ).one() + return row[0], row[1] + + +def _stage_null_summary(path: str) -> None: + """The phase-118 backfill case, staged directly (the way phase 118's + tests null the column), extended for this phase: the stale + ``is_summary`` CHUNK goes too, so the chunk's REAPPEARANCE after the + re-sync proves the backfill ran rather than merely survived.""" + with SessionLocal() as db: + doc_id = db.scalar( + select(Document.id).where( + Document.source == SOURCE_NAME, Document.path == path + ) + ) + assert doc_id is not None, f"{path} is not in the KB" + db.execute(delete(Chunk).where(Chunk.document_id == doc_id, Chunk.is_summary.is_(True))) + db.execute( + text( + "UPDATE documents SET summary = NULL " + "WHERE source = :s AND path = :p" + ), + {"s": SOURCE_NAME, "p": path}, + ) + db.commit() + # The stage landed — otherwise every later assertion would pass vacuously. + assert _summary_map()[path] is None + assert _summary_chunk_contents(path) == [] + + +# --------------------------------------------------------------------------- +# 1. The summary-NULL backfill survives the deferral +# --------------------------------------------------------------------------- + + +def test_summary_null_backfill_survives_the_deferral( + page: Page, app_url: str, source_dir: Path, db_ready: None +) -> None: + """Run 1 indexes both docs and summarizes them (phase 118 A2: markdown + docs are summarized too). One row is then staged by hand — + ``documents.summary = NULL`` AND its ``is_summary`` chunk deleted — and + the Sources-page re-sync runs WITH the deferral in place: + + * the run reports the doc as ``unchanged``, not ``added``/``updated`` + (the settle line and the terminal detail both say so — the backfill + must not masquerade as a re-index, and ``dates_updated``/``summaries`` + stay 0); + * the document page shows a NON-EMPTY summary again, byte-identical to + the one run 1 stored — produced from the STORED content through the + one lazy read the deferral leaves on this branch (LOCKED A2); + * exactly ONE ``is_summary`` chunk exists for the doc (it was recreated, + not duplicated).""" + page.set_default_timeout(30_000) + _seed_source(source_dir.parent) # idempotent — the tree starts at its original bytes + login(page, app_url, next=SOURCES_URL) + _add_local_source(page, app_url, source_dir) + + # --- run 1: everything is new, both docs get a summary ---------------- + body1 = _sync_from_sources_page(page, app_url) + _expect_counts_line(page, f"{N_FILES} added") + assert body1["detail"]["added"] == N_FILES, body1["detail"] + originals = _summary_map() + assert set(originals) == {ALPHA_PATH, BACKFILL_PATH}, originals + stored_summary = originals[BACKFILL_PATH] + assert stored_summary, "run 1 must have summarized the markdown doc (phase 118 A2)" + + # --- stage the phase-118 case directly -------------------------------- + _stage_null_summary(BACKFILL_PATH) + + # --- run 2: the unchanged re-sync backfills from the STORED content ---- + body2 = _sync_from_sources_page(page, app_url) + _expect_counts_line(page, f"0 added · {N_FILES} unchanged") + detail = body2["detail"] + assert detail["added"] == 0 and detail["updated"] == 0, detail + assert detail["unchanged"] == N_FILES, detail + assert detail["errors"] == 0, detail + # The counts stay honest: a backfill is neither a re-summary of changed + # content (``summaries``) nor a date refresh (``dates_updated``). + assert detail["summaries"] == 0, detail + assert detail["dates_updated"] == 0, detail + assert detail["pruned"] == 0, detail + + # --- the DB side: the summary is back, byte-identical, one chunk ------ + after = _summary_map() + assert after[BACKFILL_PATH] == stored_summary, ( + "the backfilled summary must equal what run 1 stored from the same " + "content (the deterministic mock makes byte-equality a pin)" + ) + chunks = _summary_chunk_contents(BACKFILL_PATH) + assert len(chunks) == 1, f"exactly one is_summary chunk, got {len(chunks)}" + assert chunks[0] == stored_summary + + # --- the owner's surface: the document page shows the summary --------- + _open_document_page(page, app_url, BACKFILL_PATH) + expect(page.locator("#doc-title")).to_have_text("Deferred content beta") + summary_text = page.locator("#doc-content .doc-summary-text") + expect(summary_text).to_have_count(1) + expect(summary_text).not_to_be_empty() + expect(summary_text).to_have_text(stored_summary) + # The deferral did not thin the doc for the viewer either. + expect(page.locator("#doc-content")).to_contain_text(BETA_HEAD) + expect(page.locator("#doc-content")).to_contain_text(BETA_TAIL) + + +# --------------------------------------------------------------------------- +# 2. An unchanged document's viewer still shows the FULL stored text +# --------------------------------------------------------------------------- + + +def test_unchanged_document_viewer_shows_full_stored_text( + page: Page, app_url: str, source_dir: Path, db_ready: None +) -> None: + """Run 1 indexes, run 2 changes NOTHING (untouched bytes, so every file + walks the deferred-row fast path) — and the document view then renders + the FULL stored text: the sentinel at the START and the sentinel at the + END are both present, every stored line is present, no truncation, no + empty body. The deferral must not have made the viewer's source row + thinner.""" + page.set_default_timeout(30_000) + _seed_source(source_dir.parent) + login(page, app_url, next=SOURCES_URL) + _add_local_source(page, app_url, source_dir) + + _sync_from_sources_page(page, app_url) + _expect_counts_line(page, f"{N_FILES} added") + _sync_from_sources_page(page, app_url) + _expect_counts_line(page, f"0 added · {N_FILES} unchanged") + + stored = _stored_content(ALPHA_PATH) + assert stored == _alpha_text(), "the stored content is the fixture's original bytes" + + _open_document_page(page, app_url, ALPHA_PATH) + expect(page.locator("#doc-title")).to_have_text("Deferred content alpha") + body = page.locator("#doc-content .doc-md") + expect(body).not_to_be_empty() + + # Both ENDS of the file, in the rendered view (the task's pin): a viewer + # fed by a truncated row would be missing one of them. + expect(body).to_contain_text(ALPHA_HEAD) + expect(body).to_contain_text(ALPHA_TAIL) + expect(body).to_contain_text(ALPHA_MIDDLE) + + # The strong form: EVERY stored line renders (headings render without + # their leading '#', the fixture carries no list/inline markup, so the + # comparison is line-for-line on whitespace-collapsed text — a viewer + # fed by a thinner row, or truncated anywhere in the middle, drops one + # of these). + rendered = " ".join(body.inner_text().split()) + for line in stored.splitlines(): + if not line.strip(): + continue + expected = " ".join(line.lstrip("#").split()) + assert expected in rendered, f"stored line not rendered: {line!r}" + + +# --------------------------------------------------------------------------- +# 3. A changed file re-indexes under the deferral +# --------------------------------------------------------------------------- + + +def test_changed_file_reindexes_under_the_deferral( + page: Page, app_url: str, source_dir: Path, db_ready: None +) -> None: + """One file is rewritten on disk (new head/tail sentinels, two extra + sections, clearly different length). The re-sync runs with the row read + deferred and must behave exactly as before the phase: ``1 updated`` with + the neighbour ``unchanged``, the viewer shows the NEW text with NO stale + fragment, the stored content equals the new bytes, the content-chunk + count moved, and the untouched neighbour's hashes did not. A THIRD sync + then reports everything ``unchanged`` — the fast path still fires after + a deferral-mediated write.""" + page.set_default_timeout(30_000) + _seed_source(source_dir.parent) + login(page, app_url, next=SOURCES_URL) + _add_local_source(page, app_url, source_dir) + + _sync_from_sources_page(page, app_url) + _expect_counts_line(page, f"{N_FILES} added") + chunks_before = _content_chunk_counts() + stamps_before = _row_stamp(BETA_PATH) + assert set(chunks_before) == {ALPHA_PATH, BETA_PATH}, chunks_before + + # --- rewrite alpha on disk (new sentinels, longer, more sections) ----- + (source_dir / ALPHA_PATH).write_text(_revised_alpha_text(), encoding="utf-8") + + body = _sync_from_sources_page(page, app_url) + _expect_counts_line(page, f"0 added · 1 updated · {N_FILES - 1} unchanged") + detail = body["detail"] + assert detail["added"] == 0 and detail["updated"] == 1, detail + assert detail["unchanged"] == N_FILES - 1, detail + assert detail["errors"] == 0, detail + + # The stored content is the NEW file bytes — the write path carried the + # full text through the same session whose row read deferred it. + assert _stored_content(ALPHA_PATH) == _revised_alpha_text() + + # The content-chunk count moved (the rewrite added two whole sections). + chunks_after = _content_chunk_counts() + assert chunks_after[ALPHA_PATH] != chunks_before[ALPHA_PATH], ( + f"chunk count must move on a re-index: {chunks_before} -> {chunks_after}" + ) + assert chunks_after[ALPHA_PATH] > 0 and chunks_before[ALPHA_PATH] > 0 + # The untouched neighbour row did not move (both hashes byte-still). + assert _row_stamp(BETA_PATH) == stamps_before + + # The viewer shows the NEW text and NO stale fragment. + _open_document_page(page, app_url, ALPHA_PATH) + expect(page.locator("#doc-title")).to_have_text("Deferred content alpha revised") + rendered = page.locator("#doc-content .doc-md") + expect(rendered).to_contain_text(REVISED_HEAD) + expect(rendered).to_contain_text(REVISED_TAIL) + expect(rendered).not_to_contain_text(ALPHA_HEAD) + expect(rendered).not_to_contain_text(ALPHA_TAIL) + expect(rendered).not_to_contain_text(ALPHA_MIDDLE) + + # --- run 3: the fast path still fires after the deferral-mediated write + _goto_sources(page, app_url) + body3 = _sync_from_sources_page(page, app_url) + _expect_counts_line(page, f"0 added · {N_FILES} unchanged") + assert body3["detail"]["updated"] == 0, body3["detail"] + assert body3["detail"]["unchanged"] == N_FILES, body3["detail"] diff --git a/tests/e2e/test_modal_back_button.py b/tests/e2e/test_modal_back_button.py index e4c6ecc..e2dfec1 100644 --- a/tests/e2e/test_modal_back_button.py +++ b/tests/e2e/test_modal_back_button.py @@ -99,6 +99,7 @@ Test → observable mapping (Playwright Mapping Rule): from __future__ import annotations import asyncio +import time from pathlib import Path from threading import Thread from typing import Any @@ -229,13 +230,29 @@ def _assert_crumb(page: Page, links: list[str], current: str) -> None: ``.kb-crumb-link`` per ancestor (in order — the first is always the ``Knowledge base`` jump-to-top segment), then the last segment as the ``.kb-crumb-current`` span with ``aria-current="page"``. - Hidden = the TOP level (no drill).""" + Hidden = the TOP level (no drill). + + The chain is POLLED, not read once: phase 167 made a drill LAZY (the + level and its breadcrumb render when ``/api/docs/tree/node`` resolves), + so the single read that sufficed while the whole tree shipped at boot + now races the fetch and intermittently returns the level the user came + FROM. + """ + expected = [*links, current] expect(page.locator("#kb-crumb")).to_be_visible() - texts = page.eval_on_selector_all( - "#kb-crumb :is(.kb-crumb-link, .kb-crumb-current)", - "(els) => els.map((el) => el.textContent)", - ) - assert texts == [*links, current], texts + + def _crumb_texts() -> list[str]: + return page.eval_on_selector_all( + "#kb-crumb :is(.kb-crumb-link, .kb-crumb-current)", + "(els) => els.map((el) => el.textContent)", + ) + + deadline = time.monotonic() + 30.0 + texts = _crumb_texts() + while texts != expected and time.monotonic() < deadline: + page.wait_for_timeout(50) + texts = _crumb_texts() + assert texts == expected, texts cur = page.locator("#kb-crumb .kb-crumb-current") expect(cur).to_have_count(1) expect(cur).to_have_attribute("aria-current", "page") diff --git a/tests/e2e/test_nav_rename_sources.py b/tests/e2e/test_nav_rename_sources.py index 4df355f..a866172 100644 --- a/tests/e2e/test_nav_rename_sources.py +++ b/tests/e2e/test_nav_rename_sources.py @@ -112,13 +112,14 @@ CURRENT_LINK: dict[str, str | None] = { #: The four primary nav links, in their physical DOM order — the labels #: after the phase-48 swap (ids/hrefs unchanged). NAV_LABELS = ("Chat", "RAG", "Sources", "Tuning") -#: The FIFTH–SEVENTH a.nav-link in every page header (phase 53 +#: The FIFTH–EIGHTH a.nav-link in every page header (phase 53 #: saved-chat history + phase 79 task 06 access tokens + phase 91 -#: theme tab — all ship-hidden, revealed by header.js for admins). +#: theme tab + phase 168 admin settings panel — all ship-hidden, +#: revealed by header.js for admins). #: The DOM enumeration below therefore always sees them (pre-existing; #: the list matches the real nav — the phase-34 one-bar contract ships #: the SAME nav, incl. the Tokens and Theme links, on every page). -NAV_TAIL = ("History", "Tokens", "Theme") +NAV_TAIL = ("History", "Tokens", "Theme", "Settings") #: The login.js script — route pattern for the redirect suppression. LOGIN_JS_ROUTE = re.compile(r"/assets/login\.js(\?.*)?$") diff --git a/tests/e2e/test_navbar_refresh.py b/tests/e2e/test_navbar_refresh.py index 28320da..2ac20f5 100644 --- a/tests/e2e/test_navbar_refresh.py +++ b/tests/e2e/test_navbar_refresh.py @@ -71,6 +71,7 @@ from collections.abc import Callable from pathlib import Path from threading import Thread from typing import Any +from urllib.parse import urlsplit import httpx from playwright.sync_api import Locator, Page, Request, expect @@ -510,7 +511,23 @@ def test_all_four_data_views_refetch_on_reshow( log = _get_logger(page) def count(endpoint: str) -> int: - return sum(1 for u in log if u.endswith(endpoint)) + """Requests to ONE endpoint: exact path, plus every query pair the + pin names. Phase 175 paginated the History list + (``/api/chats?limit=25&offset=0``, ``history.js``), so the plain + ``endswith`` this suite used to count with stopped seeing the + view's own fetch altogether — the mount baseline never arrived and + the loop timed out on the FIRST view.""" + path, _, query = endpoint.partition("?") + wanted = [pair for pair in query.split("&") if pair] + total = 0 + for url in log: + parts = urlsplit(url) + if parts.path != path: + continue + params = parts.query.split("&") if parts.query else [] + if all(pair in params for pair in wanted): + total += 1 + return total for nav_sel, view_path, endpoint in DATA_VIEWS: # First show (the mount) — wait until the mount's own load diff --git a/tests/e2e/test_source_chip_quality.py b/tests/e2e/test_source_chip_quality.py index 3953953..1204a2f 100644 --- a/tests/e2e/test_source_chip_quality.py +++ b/tests/e2e/test_source_chip_quality.py @@ -114,6 +114,46 @@ def _ask(page: Page, message: str) -> None: page.click("#send-btn") +def _assert_ranked(recorded: str, groups: list[list[str]]) -> None: + """Compare a ``query_log.sources`` string against an expected rank list + whose trailing entries are TIE-GROUPS (order-free sets). + + WHY the groups exist: the E2E mock embedder builds real token-overlap + vectors, so several tiny fixture documents end up on EXACTLY the same + fused score. ``fuse`` then sorts by ``(-score, -cosine, path, + position)``… but that only orders chunks whose SCORES differ; between + two documents tied in both lists the vector rank itself comes out of + ``ORDER BY embedding <=> q``, which is not a total order, so Postgres + (a parallel seq scan handing its partial sorts back) may return the + tied pair the other way round between two runs of the SAME fixture. + That was this suite's flake. The rank of every block, and the exact + SET inside it, are still pinned — what the story pins is that the + record carries the FULL retrieval (LOCKED A3), not the luck of a tie. + """ + docs = [d.strip() for d in recorded.split(",")] + expected = [d for group in groups for d in group] + assert len(docs) == len(expected), f"recorded {docs}, expected {expected}" + at = 0 + for group in groups: + window = docs[at : at + len(group)] + assert sorted(window) == sorted(group), ( + f"rank block {at + 1}..{at + len(group)}: got {window}, " + f"expected the set {sorted(group)}" + ) + at += len(group) + + +def _assert_related_pair(links: object, titles: set[str]) -> None: + """The two related-doc links carry EXACTLY these two documents, in + either order (the same tie the record pins as a group — see + :func:`_assert_ranked`).""" + found = { + links.nth(i).get_attribute("title") # pyright: ignore[reportAttributeAccessIssue] + for i in range(2) + } + assert found == titles, f"related pair is {found}, expected {titles}" + + def test_single_source_question_shows_exactly_one_citation_chip( page: Page, app_url: str, mock_llm: int, db_ready: None ) -> None: @@ -179,15 +219,18 @@ def test_single_source_question_shows_exactly_one_citation_chip( # (best eff 0.016277) now out-ranks ``kubernetes.md`` (0.016036); # re-pinned against the post-phase walk (the record's CONTENT — # suggested + related + read, deduped — is LOCKED A3, untouched). - assert row_log.sources == ( - "docs/homelab/ssh/ssh_aliases.txt, " - "docs/homelab/container_gitlab/gitlab-compose.yaml, " - "docs/homelab/scripts/uptime_probe.py, " - "docs/homelab/backups.md, " - "docs/homelab/kubernetes.md, " - "docs/homelab/quadlet/compose.container, " - "docs/homelab/networking/static-dns.json" - ), row_log.sources + _assert_ranked( + row_log.sources, + [ + ["docs/homelab/ssh/ssh_aliases.txt"], + ["docs/homelab/container_gitlab/gitlab-compose.yaml"], + ["docs/homelab/scripts/uptime_probe.py"], + # The tied pair this record used to pin in an arbitrary order. + ["docs/homelab/backups.md", "docs/homelab/kubernetes.md"], + ["docs/homelab/quadlet/compose.container"], + ["docs/homelab/networking/static-dns.json"], + ], + ) def test_deflected_question_shows_zero_citation_chips( @@ -225,8 +268,9 @@ def test_deflected_question_shows_zero_citation_chips( # record, but a deflected turn cites nothing, so they never render # as chips. expect(links).to_have_count(2) - expect(links.nth(0)).to_contain_text("backups.md") - expect(links.nth(1)).to_contain_text("kubernetes.md") + # The pair, as a SET: the two are tied in the fused score (see + # :func:`_assert_ranked`), so only their presence is a contract. + _assert_related_pair(links, {"docs/homelab/backups.md", "docs/homelab/kubernetes.md"}) # ZERO citation chips under the bubble — the weak hits are scored # docs, not citations (the phase-112/113 contract on the wire). @@ -243,12 +287,16 @@ def test_deflected_question_shows_zero_citation_chips( assert row_log.deflected is True assert 0.0 < row_log.top_score < get_settings().relevance_threshold assert row_log.fts_hits == 0 - assert row_log.sources == ( - "docs/deployments/new-service.md, " - "docs/homelab/ssh/ssh_aliases.txt, " - "docs/homelab/quadlet/lan.network, " - "docs/homelab/scripts/uptime_probe.py, " - "docs/homelab/quadlet/compose.container, " - "docs/homelab/backups.md, " - "docs/homelab/kubernetes.md" - ), row_log.sources + _assert_ranked( + row_log.sources, + [ + ["docs/deployments/new-service.md"], + ["docs/homelab/ssh/ssh_aliases.txt"], + ["docs/homelab/quadlet/lan.network"], + ["docs/homelab/scripts/uptime_probe.py"], + ["docs/homelab/quadlet/compose.container"], + # The related row IS this pair, and the row's order is the same + # tie — pinned as a set below (``_assert_related_pair``). + ["docs/homelab/backups.md", "docs/homelab/kubernetes.md"], + ], + ) diff --git a/tests/e2e/test_steering.py b/tests/e2e/test_steering.py index 3606c8e..01540ff 100644 --- a/tests/e2e/test_steering.py +++ b/tests/e2e/test_steering.py @@ -48,11 +48,13 @@ NOTE = "STEEER-MARKER be concise" XSS_NOTE = "" #: The shell (index.html — served for BOTH / and /tuning.html since #: phase 76 task 01, when the Tuning view folded into it) ships exactly -#: FIVE classic/module script tags: the phase-39 brand.js classic layer -#: + markdown.js + the chat module (app.js) + the shell router -#: (router.js, which lazy-imports the tuning.js view module) + the -#: phase-79 token gate (token-gate.js, phase 79 task 05). -BASE_SCRIPT_COUNT = 5 +#: SIX classic/module script tags: the phase-39 brand.js classic layer +#: + markdown.js + the phase-182 verbatim.js hydrator (a classic script +#: loaded AFTER markdown.js, which is what put it in this count) + the +#: chat module (app.js) + the shell router (router.js, which +#: lazy-imports the tuning.js view module) + the phase-79 token gate +#: (token-gate.js, phase 79 task 05). +BASE_SCRIPT_COUNT = 6 async def _import_fixtures(mock_port: int) -> ImportSummary: diff --git a/tests/e2e/test_suggestion_chips.py b/tests/e2e/test_suggestion_chips.py index 827b9cc..16a64ef 100644 --- a/tests/e2e/test_suggestion_chips.py +++ b/tests/e2e/test_suggestion_chips.py @@ -463,10 +463,14 @@ def test_chips_keyboard_accessible( assert chip_text # Tab from the page start: the first chip must be reachable on the - # keyboard, and before the composer input (skip-link + 2 nav links come - # first). Track tab stops until we land on a chip. + # keyboard, and before the composer input. What comes FIRST has grown + # with the nav itself — skip-link + the eight admin links (Chat, RAG, + # Sources, Tuning, History, Tokens, Theme, and the phase-168 Settings + # panel) + the header controls — so the walk allowance is the whole + # header, not the phase-05 two-link bar. The contract under test is the + # ORDER (a chip before the composer), pinned by the input check below. reached_chip_at: int | None = None - for step in range(1, 11): + for step in range(1, 25): page.keyboard.press("Tab") state = page.evaluate( """() => { diff --git a/tests/integration/test_import_fast_path_columns.py b/tests/integration/test_import_fast_path_columns.py new file mode 100644 index 0000000..7d475a0 --- /dev/null +++ b/tests/integration/test_import_fast_path_columns.py @@ -0,0 +1,749 @@ +"""Integration: phase 185 — what the UNCHANGED fast path SELECTs (the probe). + +**VERDICT: CONFIRMED (task 01) → FIXED (phase 185 task 02, +``defer(Document.content)`` in ``app/rag/importer.py::_doc_row_stmt``).** +The fast path used to SELECT ``content`` for every +file it inspects. Each inspected file is looked up by +``app/rag/importer.py::_index_file`` (``select(Document).where( +Document.source == source, Document.path == rel)``), which is a WHOLE-ENTITY +load: its select list names ``documents.content`` — the full stored markdown, +**15,217** chars on average — next to the metadata the fast path actually +reads. The three sibling loads that share the phase-147 unchanged path (the +PDF branch, the PDF-embedded-image branch, the standalone-image branch) +compile to the same statement, so the whole KB walk pays this for EVERY file +it looks at, not only for the ones that changed. Nothing is released +afterwards: ``app/db.py:30`` builds every session with +``expire_on_commit=False``, so a row the run read — and then dirtied with the +``file_hash`` stamp (phase 184) — stays resident with its markdown until the +run ends. + +**What the fix changed (measured by this file, post-fix).** The four fast-path +row loads now share ``_doc_row_stmt``, whose projection is every ``Document`` +column EXCEPT ``content``, and the unchanged run issues **7** documents +SELECTs for the same four files: the **4** metadata-only row loads, **1** +primary-key follow-up (``SELECT documents.content AS documents_content … +WHERE documents.id = …``) — paid by the ONE summary-NULL row whose phase-118 +backfill genuinely reads the stored markdown — and the same **2** per-source +post-passes. A file that already has a summary still costs exactly **1** +documents SELECT and zero follow-ups (``test_unchanged_file_is_one_select``). + +**The incident (owner, 2026-10-15 — the same run phases 183/184 fixed).** +``documents`` held **17,407** rows at ``avg(length(content)) = 15,217`` +characters = **252.6 MB of markdown**. The sync inspected ~27k files and +re-indexed **217**; the remaining ~26.8k took the fast path and still hydrated +their markdown to discover that nothing had changed. Phase 183 took ``content`` +out of the prune's row (six light columns), phase 184 bounded what the walk +RETAINS (``session.expunge_all()`` every ``BOR_SYNC_SESSION_RESET_FILES`` +files); this phase takes the markdown out of what the walk READS per unchanged +file, so a full re-sync's memory scales with row metadata instead of with the +corpus. + +**Measured BY THIS FILE (2026-10-15, the dev Postgres; fixture: 4 markdown +files of ~8 KB seeded through the real ``import_sources``, then a SECOND +``import_sources`` over the same tree — the unchanged run — captured with +``before_cursor_execute``, the phase-183 task-01 technique).** PRE-FIX the +unchanged run issued **6** statements over ``documents``: + +* **4** per-file ``(source, path)`` row loads — exactly one per inspected + file, the fast path's ONE lookup, every one of them naming + ``documents.content`` (the load-bearing pin, flipped by task 02); +* **0** primary-key follow-up SELECTs over ``documents`` — nothing was + deferred yet, so nothing could be re-read; every file that already has a + summary costs exactly **1** documents SELECT in total; +* **2** per-source post-pass walks (``_enrich_pending_images`` + + ``_backfill_image_contexts``, both ``WHERE documents.source = … AND + documents.is_image IS true``) — twice per RUN, independent of the file + count, and outside this phase's scope (D1 names exactly the four fast-path + row loads); +* the one staged row (``documents.summary = NULL``, its stale ``is_summary`` + chunk stamped so replacement is observable) cost **1** documents SELECT + total — the row load, whose already-hydrated ``content`` the phase-118 + backfill consumed directly. Post-task-02 that row is the ONLY one that pays + a second, single-column SELECT. + +The captured fast-path statement, verbatim for one file **POST-FIX** — every +column of the PRE-FIX list below EXCEPT ``documents.content`` (which sat +between ``title`` and ``content_hash``), so no markdown travels with an +inspected row; the fast path reads ``file_hash``, then ``summary`` / +``created_at`` / ``page_count`` / ``path`` / ``full_path`` — never the text +this projection drops: + +``` +SELECT documents.id, documents.source, documents.path, documents.full_path, +documents.title, documents.content_hash, documents.file_hash, +documents.indexed_at, documents.created_at, documents.created_at_manual, +documents.summary, documents.is_image, documents.image_path, +documents.image_context, documents.page_count, documents.page_strip_mode +FROM documents +WHERE documents.source = %(source_1)s::VARCHAR AND documents.path = +%(path_1)s::VARCHAR +``` + +**The PRE-FIX shape** (task 01's evidence — the statement above WITH +``documents.content`` between ``title`` and ``content_hash``, which is what +put ~250 MB of markdown in front of a run that re-indexed 217 of ~27k files) +and **the follow-up a deferred row pays when it genuinely needs the +markdown** — the phase-118 summary-NULL backfill, ``_unchanged_doc`` step 3, +and nothing else on the fast path (measured 2026-10-15 by putting the same +row behind an ad-hoc ``defer(Document.content)`` load and then reading +``doc.content``):: + + SELECT documents.content AS documents_content + FROM documents + WHERE documents.id = %(pk_1)s::UUID + +That is the whole trade (phase 185 D2 / LOCKED A2): a doc WITH a stored +summary pays ZERO extra queries, a summary-NULL backfill pays ONE per-row +one-time SELECT, and deliberately NO ``raiseload``/``noload`` — those would +turn the backfill into a crash instead of a query. + +**What this file does NOT touch:** the two per-source post-pass walks above, +``_prune``'s light row (phase 184), the identity-map cadence (phase 184), +``app/db.py:30``, ``iter_importable_files`` (phase 185 LOCKED A3) and the +``doc.chunks = [...]`` re-embed path (phase 183 LOCKED A2). +""" +from __future__ import annotations + +import asyncio +import logging +import re +from collections.abc import Iterator +from pathlib import Path +from typing import Any, NamedTuple + +import pytest +from sqlalchemy import event, func, select, text +from sqlalchemy.orm import Session + +from app.config import Settings +from app.db import SessionLocal, engine +from app.models import Chunk, Document +from app.rag.importer import ImportSummary, import_sources +from tests.fakes import FakeEmbedder + +# --------------------------------------------------------------------------- +# The fixture: 4 markdown files of ~8 KB, one seed run + one unchanged run +# --------------------------------------------------------------------------- + +#: The probe's own source name — nothing else in the KB uses it. +PROBE_SOURCE = "fastpath-probe" + +#: Four files: enough that "one row load per file" is arithmetic rather than +#: a single sample, few enough that the seed run is instant. +FILE_COUNT = 4 + +#: The row whose stored ``summary`` is nulled before the second run — the +#: phase-118 backfill case, the ONE fast-path branch that genuinely needs the +#: stored markdown. +BACKFILL_PATH = "probe-02.md" + +#: What the pre-existing ``is_summary`` chunk of that row is stamped with, so +#: "the backfill replaced the chunk" is observable instead of assumed (the +#: replacement is the only thing that can make the chunk read ``expected``). +STALE_SUMMARY = "STALE SUMMARY CHUNK — the backfill must replace this" + +#: One repeated sentence, so every file has the same shape (and therefore the +#: same chunk count) as every other. +_SENTENCE = ( + "The homelab runs podman quadlets on the hnb host with a static DNS " + "zone, nightly restic backups and a systemd timer that probes uptime. " +) + +#: ~8 KB per file — production ``content`` averages 15.2 KB, so a whole-entity +#: row load is tens of kilobytes per inspected file and ``content`` is +#: unmistakably the fat column of the row (and stays under +#: ``BOR_SUMMARY_MAX_CHARS``, so the backfill's summary call is uncapped). +CONTENT_CHARS_PER_FILE = 8_000 + +#: The documents SELECTs of the two per-source post-passes, measured +#: 2026-10-15 — asserted so a future per-file extra SELECT cannot hide among +#: them (and so a post-pass that stopped querying is noticed too). +POST_PASS_DOC_SELECTS = 2 + +#: **0 pre-phase-185** — the whole-entity row load already carries +#: ``content``, so the phase-118 backfill needs no follow-up query. **1 +#: **0 pre-phase-185** — the whole-entity row load already carried +#: ``content``, so the phase-118 backfill needed no follow-up query. **1 +#: post-phase-185 (the value now)** — the row load defers ``content``, so the +#: ONE backfill pays exactly ONE lazy ``SELECT documents.content … WHERE +#: documents.id = …``. The arithmetic: the backfilled file costs ``1 row load +#: + EXPECTED_EXTRA_DOC_SELECTS_FOR_BACKFILL`` documents SELECTs, every other +#: unchanged file costs exactly 1 (``test_unchanged_file_is_one_select``). +#: Everything else about the backfill is identical across the flip: the stored +#: summary, its ``is_summary`` chunk, the run's counts and its log words +#: (``test_unchanged_run_keeps_every_row_intact``). +EXPECTED_EXTRA_DOC_SELECTS_FOR_BACKFILL = 1 + + +class Captured(NamedTuple): + """One executed statement plus its bind parameters. + + The parameters make the probe attributable: the fast-path row load carries + ``path_1`` (the file it looked up) while the deferred column's lazy + follow-up carries only the primary key — so "one SELECT per unchanged + file" and "one follow-up for the backfill" are two different, separately + asserted claims. + """ + + text: str + params: dict[str, Any] + + +class UnchangedRun(NamedTuple): + """The unchanged run, snapshotted at the moment it ENDED. + + ``captured`` is a copy: the listener stays installed for the rest of the + test, so the assertions must not race the probe's own read queries. + """ + + captured: list[Captured] + summary: ImportSummary + llm: FakeEmbedder + + @property + def lite_prompts(self) -> list[str]: + """The user-message text of every ``lite`` call the run made.""" + return [ + str(message.get("content")) + for call in self.llm.chat_calls + for message in call + if message.get("role") == "user" + ] + + +def _file_body(n: int) -> str: + """One ~8 KB markdown document, carrying a per-file sentinel per section.""" + parts = [f"# Probe document {n}\n"] + section = 0 + while sum(len(p) for p in parts) < CONTENT_CHARS_PER_FILE: + parts.append(f"\n## Section {section} of probe {n} (fastpath-sentinel-{n})\n\n") + parts.extend(_SENTENCE for _ in range(4)) + parts.append("\n") + section += 1 + return "".join(parts) + + +def _seed_files(tmp: Path) -> Path: + """``FILE_COUNT`` markdown files under a fresh source root; returns it.""" + root = tmp / PROBE_SOURCE + root.mkdir(parents=True, exist_ok=True) + for n in range(FILE_COUNT): + body = _file_body(n) + assert 8_000 <= len(body) <= 9_000, len(body) + (root / f"probe-{n:02d}.md").write_text(body) + return root + + +def _probe_llm() -> FakeEmbedder: + """The deterministic fake embedder/summarizer on the SHIPPED defaults + (``sync_session_reset_files=200`` — no identity-map release lands inside a + four-file walk, so nothing but the row loads is in play here).""" + llm = FakeEmbedder() + llm.settings = Settings(_env_file=None) # pyright: ignore[reportCallIssue] + return llm + + +def _run_import(root: Path, llm: FakeEmbedder) -> ImportSummary: + """One real ``import_sources`` pass over *root*, in its own session.""" + session = SessionLocal() + try: + return asyncio.run( + import_sources([root], llm, prune=False, session=session) + ) + finally: + session.close() + + +def _doc_id(db: Session, path: str) -> Any: + """The id of one probe row (lazy follow-ups are keyed by primary key).""" + doc_id = db.scalar( + select(Document.id).where( + Document.source == PROBE_SOURCE, Document.path == path + ) + ) + assert doc_id is not None + return doc_id + + +def _stage_the_backfill(db: Session, path: str) -> None: + """The phase-118 state: a row whose stored ``summary`` is NULL while its + content and ``file_hash`` are current (the direct UPDATE of + ``tests/integration/test_importer_dates.py`` — exactly what a pre-phase-118 + import left behind). + + The stale ``is_summary`` chunk is stamped rather than deleted: after the + run a chunk carrying the backfill text can only have got there through + :func:`app.rag.importer._store_summary`'s replacement rule, which is what + makes "the backfill ran" a fact rather than an assumption. + """ + doc_id = _doc_id(db, path) + db.execute( + text( + "UPDATE documents SET summary = NULL WHERE source = :s AND path = :p" + ), + {"s": PROBE_SOURCE, "p": path}, + ) + db.execute( + text("UPDATE chunks SET content = :stale WHERE document_id = :d AND is_summary"), + {"stale": STALE_SUMMARY, "d": doc_id}, + ) + db.commit() + db.expunge_all() + + +def _two_runs(db: Session, tmp_path: Path, captured: list[Captured]) -> UnchangedRun: + """Seed the KB, then run the UNCHANGED pass with the capture active. + + The seed run's SQL is cleared away at the boundary: this file pins what + the unchanged walk costs, not what first indexing costs. + """ + root = _seed_files(tmp_path) + first = _run_import(root, _probe_llm()) + assert (first.files, first.added, first.unchanged, first.errors) == ( + FILE_COUNT, + FILE_COUNT, + 0, + 0, + ) + _stage_the_backfill(db, BACKFILL_PATH) + + captured.clear() + llm = _probe_llm() + second = _run_import(root, llm) + run = UnchangedRun(list(captured), second, llm) + + assert (second.files, second.added, second.unchanged, second.errors) == ( + FILE_COUNT, + 0, + FILE_COUNT, + 0, + ) + assert second.chunks == 0 # the fast path re-embeds no content + assert second.summary_backfilled == 1 # the staged row was backfilled + return run + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def clean_kb(db: Session) -> Iterator[None]: + """``documents`` / ``chunks`` are global state: reset around every test + (the ``test_sync_api.py`` truncate pattern) and drop whatever the session + cached, so the next read is the database's truth.""" + db.execute(text("TRUNCATE chunks, documents")) + db.commit() + db.expunge_all() + yield + db.execute(text("TRUNCATE chunks, documents")) + db.commit() + db.expunge_all() + + +@pytest.fixture() +def statement_capture() -> Iterator[list[Captured]]: + """Every SQL the engine executes for the duration of ONE test. + + The ``before_cursor_execute`` technique of phase 183 task 01 + (``tests/integration/test_prune_cascade_statements.py``): the column list + a SELECT uses is invisible in the ORM's Python API and plain to see in the + statement it compiles, so a capture is the only deterministic way to pin + what the fast path's row actually contains. Installed on the engine and + REMOVED in teardown, so no other test of the session ever sees the + listener (verified by the full ``uv run pytest`` run being green with this + file in it). + """ + captured: list[Captured] = [] + + def _listener( + conn: object, # noqa: ARG001 + cursor: object, # noqa: ARG001 + statement: str, + parameters: object, + context: object, # noqa: ARG001 + executemany: bool, # noqa: ARG001 + ) -> None: + params = dict(parameters) if isinstance(parameters, dict) else {} + captured.append(Captured(statement, params)) + + event.listen(engine, "before_cursor_execute", _listener) + try: + yield captured + finally: + event.remove(engine, "before_cursor_execute", _listener) + + +# --------------------------------------------------------------------------- +# Helpers — reading a captured statement +# --------------------------------------------------------------------------- + +#: ``… FROM documents`` — case-insensitive, word-bounded. +_FROM_DOCUMENTS_RE = re.compile(r"\bfrom\s+documents\b", re.IGNORECASE) + +#: Where the projection ends: SQLAlchemy compiles ``FROM`` onto its own line, +#: and everything after it is a predicate, not a column the row hydrates. +_FROM_RE = re.compile(r"\bFROM\b") + +#: The ``content`` COLUMN TOKEN, aliased or plain. The trailing ``\\b`` is what +#: keeps the neighbours out: ``_`` is a WORD character, so the ``documents.content`` +#: inside ``documents.content_hash`` has no boundary after the ``t`` and never +#: matches. That is why this is a token regex and not a substring test — +#: ``content_hash`` is in EVERY one of these select lists, and a substring +#: match would report a content load for every row of every run, pre- and +#: post-fix alike. +_CONTENT_TOKEN_RE = re.compile(r"\bdocuments\.content\b") + + +def doc_selects(captured: list[Captured]) -> list[Captured]: + """The captured statements that SELECT the ``documents`` table.""" + return [ + c + for c in captured + if c.text.lstrip().lower().startswith("select") + and _FROM_DOCUMENTS_RE.search(c.text) + ] + + +def loads_content(statement: Captured | str) -> bool: + """``True`` when the statement's SELECT LIST names the ``content`` column. + + Only the projection is inspected (see :data:`_FROM_RE`), and only the + column token matches (see :data:`_CONTENT_TOKEN_RE`). + """ + text = statement.text if isinstance(statement, Captured) else statement + from_at = _FROM_RE.search(text) + projection = text[: from_at.start()] if from_at else text + return _CONTENT_TOKEN_RE.search(projection) is not None + + +def test_the_content_matcher_names_the_column_and_nothing_else() -> None: + """The matcher's own pin — every other assertion here rests on it. + + ``SELECT … documents.content, documents.content_hash …`` is a content load + because of the FIRST token; ``documents.content_hash`` alone is not. If + this ever stops holding, the pre-/post-fix assertion above is meaningless + in BOTH directions, so it is asserted directly rather than trusted. + """ + assert loads_content( + "SELECT documents.id, documents.content, documents.content_hash\n" + "FROM documents\nWHERE documents.id = %(pk_1)s" + ) + assert loads_content( + "SELECT documents.content AS documents_content\nFROM documents\n" + "WHERE documents.id = %(pk_1)s" + ) + assert not loads_content( + "SELECT documents.id, documents.content_hash, documents.file_hash\n" + "FROM documents\nWHERE documents.source = %(source_1)s" + ) + assert not loads_content( + "SELECT documents.summary, documents.is_image, documents.page_count\n" + "FROM documents\nWHERE documents.id = %(pk_1)s" + ) + # A predicate mention is not a hydration. + assert not loads_content( + "SELECT documents.id\nFROM documents\nWHERE documents.content = %(c_1)s" + ) + + +def _predicate(statement: str) -> str: + """Everything after the first ``FROM`` — the statement's predicate.""" + from_at = _FROM_RE.search(statement) + return statement[from_at.end():] if from_at else "" + + +def row_loads(captured: list[Captured]) -> list[Captured]: + """The ``(source, path)`` row loads — the fast path's ONE lookup per file. + + Matched on the PREDICATE, not the projection: the four sites of phase 185 + D1 (``_index_file`` plus the PDF / PDF-embedded-image / image branch loads) + all compile to ``WHERE documents.source = … AND documents.path = …``. The + two per-source post-pass walks filter on ``documents.is_image`` instead + and are therefore excluded here — they are not the fast path's row load. + """ + return [ + c + for c in doc_selects(captured) + if "documents.source" in _predicate(c.text) + and "documents.path" in _predicate(c.text) + ] + + +def pk_followups(captured: list[Captured]) -> list[Captured]: + """Documents SELECTs keyed by PRIMARY KEY — the deferred-column shape. + + A ``defer``-red column loads as ``SELECT documents.content … WHERE + documents.id = …``: no ``source``/``path`` predicate at all. Pre-fix this + list is EMPTY (the row was loaded whole, so nothing is deferred); post-fix + it holds the ONE query the phase-118 backfill pays. + """ + return [ + c + for c in doc_selects(captured) + if "documents.source" not in _predicate(c.text) + and "documents.path" not in _predicate(c.text) + ] + + +def _summary_chunks(db: Session, path: str) -> list[Chunk]: + """The ``is_summary`` chunks of one document, read by ``document_id`` so + the parent row's ``content`` is never what this probe reads.""" + return list( + db.scalars( + select(Chunk) + .where( + Chunk.document_id == _doc_id(db, path), + Chunk.is_summary.is_(True), + ) + .order_by(Chunk.id) + ) + ) + + +# --------------------------------------------------------------------------- +# The probe +# --------------------------------------------------------------------------- + + +def test_unchanged_run_defers_content_post_fix( + db: Session, tmp_path: Path, statement_capture: list[Captured] +) -> None: + """THE PROBE (post-fix): no file the unchanged walk inspects SELECTs its + markdown. + + Four unchanged files → four ``(source, path)`` row loads, and NONE of them + names ``documents.content`` in its projection — the fast path reads row + metadata only (phase 185, ``_doc_row_stmt``). The only follow-up the run + may pay is the backfill row's, attributed per primary key in + :func:`test_summary_null_backfill_reads_stored_content`, while + :func:`test_unchanged_file_is_one_select` pins that every + summary-bearing file pays none. The rest of the run's documents traffic + is the two per-source image post-passes, pinned apart so a future + per-file extra SELECT cannot hide inside the total. + + (Task 01's PRE-fix assertion was the inverse — ``all(loads_content(c))`` + with zero follow-ups — and flipped here exactly as planned. A deferred + column that is genuinely READ still shows up, as a PK follow-up, so + neither a re-appeared ``content`` nor a new lazy reader hides here.) + """ + run = _two_runs(db, tmp_path, statement_capture) + + selects = doc_selects(run.captured) + loads = row_loads(run.captured) + assert len(loads) == FILE_COUNT, ( + f"a {FILE_COUNT}-file unchanged walk issued {len(loads)} (source, " + "path) row loads — the fast path looks each file up exactly once" + ) + assert len(selects) >= FILE_COUNT + + # Post-fix (phase 185 task 02): no content in the fast-path SELECT. + assert not any(loads_content(c) for c in loads), ( + "a fast-path row load SELECTs documents.content again — the deferral " + "in _doc_row_stmt is gone or bypassed, which re-hydrates the corpus " + "(252.6 MB on the incident box) for a walk that reads metadata only" + ) + followups = pk_followups(run.captured) + assert len(followups) == EXPECTED_EXTRA_DOC_SELECTS_FOR_BACKFILL, ( + f"an unchanged run over {FILE_COUNT} files (one with summary IS NULL) " + f"paid {len(followups)} primary-key follow-up SELECTs over documents " + f"while EXPECTED_EXTRA_DOC_SELECTS_FOR_BACKFILL is " + f"{EXPECTED_EXTRA_DOC_SELECTS_FOR_BACKFILL} — the backfill's single " + "deferred-column read is the only one the fast path may need" + ) + post_passes = [c for c in selects if c not in loads and c not in followups] + assert len(post_passes) == POST_PASS_DOC_SELECTS, ( + f"the unchanged run did {len(post_passes)} documents SELECTs beyond " + f"the {FILE_COUNT} row loads — expected the {POST_PASS_DOC_SELECTS} " + "per-source image post-passes and nothing else" + ) + + +def test_unchanged_file_is_one_select( + db: Session, tmp_path: Path, statement_capture: list[Captured] +) -> None: + """One documents SELECT per unchanged file — MUST STAY GREEN POST-FIX. + + A file WITH a stored summary reads nothing beyond what its row load + already carries: the ``file_hash`` comparison, then ``_unchanged_doc``'s + ``summary`` / ``created_at`` / ``page_count`` bookkeeping. Pre-fix that is + one whole-entity load; post-fix it must still be exactly ONE — this is the + guard that ``defer(Document.content)`` does not decay into a lazy-load + storm (``summary``, then ``created_at``, then ``page_count`` … one SELECT + apiece), which would trade 15 KB of residency for five round trips. + """ + run = _two_runs(db, tmp_path, statement_capture) + + for n in range(FILE_COUNT): + path = f"probe-{n:02d}.md" + if path == BACKFILL_PATH: + continue # asserted separately, with its documented extra + one_file = [c for c in row_loads(run.captured) if c.params.get("path_1") == path] + assert one_file, ( + f"{path} was walked with no (source, path) row load carrying " + "path_1 — the row-load statement shape changed, so this probe is " + "attributing nothing" + ) + assert len(one_file) == 1, ( + f"{path} cost {len(one_file)} documents row loads — the fast path " + "looks a file up ONCE" + ) + doc_id = str(_doc_id(db, path)) + followups = [ + c for c in pk_followups(run.captured) if str(c.params.get("pk_1")) == doc_id + ] + assert followups == [], ( + f"{path} paid {len(followups)} deferred-column follow-up SELECT(s) " + "despite having a stored summary — nothing on its fast path may " + "read content, so a follow-up means a new reader crept in" + ) + + +def test_summary_null_backfill_reads_stored_content( + db: Session, tmp_path: Path, statement_capture: list[Captured] +) -> None: + """The ONE branch that genuinely needs the markdown: ``summary IS NULL``. + + ``_unchanged_doc`` step 3 (phase 118, A2) backfills the summary FROM THE + STORED ``doc.content`` — never by re-reading the file, re-extracting or + re-describing. So the probe pins the behaviour AND its cost: + + * the run backfills exactly one summary (``summary_backfilled=1`` while + ``summaries`` stays ``0`` — a backfill is not a KB-change signal), the + stored summary comes back and its single ``is_summary`` chunk is the + REPLACED one (the pre-existing chunk still carried :data:`STALE_SUMMARY` + when the run started, and replacement is the only way it changes); + * the text the ``lite`` call was given IS the stored markdown (~8 KB, with + the file's own sentinel in every section) — the bytes travelled from + ``documents.content``, not from a re-read of the file; + * the documents SELECT count for that file is ``1 + + EXPECTED_EXTRA_DOC_SELECTS_FOR_BACKFILL``: the row load plus, once task + 02 defers ``content``, exactly one lazy follow-up keyed by primary key + (D2 — a per-row, one-time, already-rare cost, accepted precisely instead + of a ``raiseload`` that would crash this branch where it should query). + """ + run = _two_runs(db, tmp_path, statement_capture) + n = int(BACKFILL_PATH.removeprefix("probe-").removesuffix(".md")) + expected = f"Summary of #\nSource: {PROBE_SOURCE}/{BACKFILL_PATH}" + + # The backfill happened, from the stored content. + assert run.summary.summary_backfilled == 1 + assert run.summary.summaries == 0 + stored = db.scalar( + select(Document.content).where( + Document.source == PROBE_SOURCE, Document.path == BACKFILL_PATH + ) + ) + assert stored is not None and len(stored) >= 8_000 + assert f"fastpath-sentinel-{n}" in stored + + doc = db.scalar( + select(Document).where( + Document.source == PROBE_SOURCE, Document.path == BACKFILL_PATH + ) + ) + assert doc is not None + assert doc.summary == expected + assert doc.file_hash is not None + + chunks = _summary_chunks(db, BACKFILL_PATH) + assert [c.content for c in chunks] == [expected], ( + "the summary chunk was not replaced by the backfill — a stale chunk " + "survived, or more than one is_summary chunk exists" + ) + assert chunks[0].position == -1 + assert chunks[0].embedding is not None + + # The lite call got the STORED markdown, sentinel and all — uncapped. + assert run.lite_prompts, "the backfill made no lite-model call" + assert any(stored == prompt for prompt in run.lite_prompts), ( + "the summary model was not fed documents.content verbatim — the " + "phase-118 backfill reads something other than the stored markdown" + ) + + # And the cost of that branch, as a documented constant. + doc_id = str(_doc_id(db, BACKFILL_PATH)) + loads = [c for c in row_loads(run.captured) if c.params.get("path_1") == BACKFILL_PATH] + followups = [c for c in pk_followups(run.captured) if str(c.params.get("pk_1")) == doc_id] + assert len(loads) == 1 + assert len(followups) == EXPECTED_EXTRA_DOC_SELECTS_FOR_BACKFILL, ( + f"the summary-NULL backfill paid {len(followups)} lazy content " + f"SELECT(s) while EXPECTED_EXTRA_DOC_SELECTS_FOR_BACKFILL is " + f"{EXPECTED_EXTRA_DOC_SELECTS_FOR_BACKFILL} — 0 while the row load " + "carries content (pre task 02), 1 once content is deferred" + ) + assert all(loads_content(c) for c in followups) + + +def test_unchanged_run_keeps_every_row_intact( + db: Session, tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """The observable contract the deferral must not move (phase 185 D4). + + The reference for task 02's byte-identity claim: the unchanged run's whole + ``ImportSummary`` shape and the whole KB after it — four unchanged files, + one backfill, no re-embed, no date refresh, every file still holding its + full ~8 KB of content, its summary and all of its chunks. ``changed_paths`` + stays empty (the backfill is not a document change, so the folder-summary + regeneration never sees it) and no source is pruned. + """ + root = _seed_files(tmp_path) + first = _run_import(root, _probe_llm()) + assert (first.files, first.added, first.summary_backfilled) == ( + FILE_COUNT, + FILE_COUNT, + 0, + ) + content_chunks_after_seed = db.scalar( + select(func.count()).select_from(Chunk).where(Chunk.is_summary.is_(False)) + ) + assert content_chunks_after_seed is not None and content_chunks_after_seed > FILE_COUNT + _stage_the_backfill(db, BACKFILL_PATH) + + caplog.clear() + with caplog.at_level(logging.INFO, logger="app.importer"): + second = _run_import(root, _probe_llm()) + messages = [r.getMessage() for r in caplog.records if r.name == "app.importer"] + + # The phase-147/118 log words, verbatim (D4: task 02 may change WHICH + # COLUMNS travel, never what the run says it did). ``import: summary + # source=`` is the FRESH-summary line — it must not appear on an unchanged + # run, where only the backfill's own wording is allowed. + assert sum(m.startswith("import: unchanged ") for m in messages) == FILE_COUNT + assert sum(m.startswith("import: summary-backfill ") for m in messages) == 1 + assert not [m for m in messages if m.startswith("import: summary source=")] + assert not [m for m in messages if m.startswith("import: date-refreshed ")] + assert not [m for m in messages if m.startswith("import: added ")] + assert not [m for m in messages if m.startswith("import: updated ")] + + assert (second.files, second.added, second.updated) == (FILE_COUNT, 0, 0) + assert (second.unchanged, second.pruned, second.errors) == (FILE_COUNT, 0, 0) + assert second.summary_backfilled == 1 + assert second.summaries == 0 + assert second.summary_errors == 0 + assert second.dates_updated == 0 + assert second.chunks == 0 + assert second.changed_paths == [] + assert second.formats == {"md": FILE_COUNT} + assert second.format_counts() == "md:4" + + # Nothing was re-embedded, nothing was lost. + assert db.scalar( + select(func.count()).select_from(Chunk).where(Chunk.is_summary.is_(False)) + ) == content_chunks_after_seed + assert db.scalar( + select(func.count()) + .select_from(Document) + .where(Document.source == PROBE_SOURCE, Document.summary.is_not(None)) + ) == FILE_COUNT + assert all( + len(c) >= 8_000 + for c in db.scalars( + select(Document.content).where(Document.source == PROBE_SOURCE) + ) + ) + # Exactly one summary chunk per document (the backfill replaced, not added). + assert db.scalar( + select(func.count()).select_from(Chunk).where(Chunk.is_summary.is_(True)) + ) == FILE_COUNT diff --git a/tests/integration/test_import_session_retention.py b/tests/integration/test_import_session_retention.py index 3772be2..c798264 100644 --- a/tests/integration/test_import_session_retention.py +++ b/tests/integration/test_import_session_retention.py @@ -136,6 +136,7 @@ lazy SELECT per row and fail that test, not get papered over. ``scripts/git_sync.py`` (phase 186), ``app/db.py:30``, and the ``doc.chunks = [...]`` re-embed path (phase 183 LOCKED A2). """ + from __future__ import annotations import asyncio @@ -189,9 +190,7 @@ EXPECTED_SUMMARY_CHUNKS = 12 #: Every ORM object a full ADD run creates: the 12 documents, their content #: chunks and their summary chunks. What the identity map would have to #: hold if the ADDED path retained what the phase-184 baseline describes. -OBJECTS_CREATED_BY_ADD_RUN = ( - FILE_COUNT + EXPECTED_CONTENT_CHUNKS + EXPECTED_SUMMARY_CHUNKS -) +OBJECTS_CREATED_BY_ADD_RUN = FILE_COUNT + EXPECTED_CONTENT_CHUNKS + EXPECTED_SUMMARY_CHUNKS #: The measured number of identity-map entries at the end of an UNCHANGED #: run with the cadence OFF — one per file walked, each a full ``Document`` @@ -322,9 +321,7 @@ def _kb_snapshot(db: Session) -> dict[str, tuple[str, str | None, str, int]]: } snapshot: dict[str, tuple[str, str | None, str, int]] = {} for doc in db.scalars( - select(Document) - .where(Document.source == PROBE_SOURCE) - .order_by(Document.path) + select(Document).where(Document.source == PROBE_SOURCE).order_by(Document.path) ): assert doc.file_hash is not None # the phase-147 stamp is always set snapshot[doc.path] = ( @@ -373,9 +370,7 @@ def test_the_added_run_holds_its_objects_only_until_a_collection( root = _seed_files(tmp_path) session = SessionLocal() try: - summary = asyncio.run( - import_sources([root], _probe_llm(), prune=False, session=session) - ) + summary = asyncio.run(import_sources([root], _probe_llm(), prune=False, session=session)) assert (summary.files, summary.added, summary.unchanged) == ( FILE_COUNT, FILE_COUNT, @@ -405,9 +400,7 @@ def test_the_added_run_holds_its_objects_only_until_a_collection( session.close() -def test_the_unchanged_run_keeps_every_document_it_walks( - db: Session, tmp_path: Path -) -> None: +def test_the_unchanged_run_keeps_every_document_it_walks(db: Session, tmp_path: Path) -> None: """THE PROBE: a repeat sync holds one full ``Document`` per file walked. The production-shaped case — the second sync of the same source, every @@ -446,9 +439,7 @@ def test_the_unchanged_run_keeps_every_document_it_walks( staircase.append(_map_size(session)) summary = asyncio.run( - import_sources( - [root], _probe_llm(), prune=False, session=session, progress=progress - ) + import_sources([root], _probe_llm(), prune=False, session=session, progress=progress) ) assert (summary.files, summary.added, summary.unchanged) == ( FILE_COUNT, @@ -459,8 +450,7 @@ def test_the_unchanged_run_keeps_every_document_it_walks( # One row resident per file already walked — a staircase, never a # plateau and never a release. assert staircase == list(range(FILE_COUNT)), ( - "expected exactly one document retained per file already walked," - f" got {staircase}" + f"expected exactly one document retained per file already walked, got {staircase}" ) assert _map_size(session) == MEASURED_UNCHANGED_MAP_SIZE @@ -643,8 +633,7 @@ def test_reset_cadence_bounds_the_unchanged_walk(db: Session, tmp_path: Path) -> FILE_COUNT, ) assert staircase == [n % RESET_FILES for n in range(FILE_COUNT)], ( - f"the bounded walk sampled {staircase} — expected a release every" - f" {RESET_FILES} files" + f"the bounded walk sampled {staircase} — expected a release every {RESET_FILES} files" ) assert max(staircase) < MEASURED_UNCHANGED_MAP_SIZE assert _map_size(session) == 0, ( @@ -682,14 +671,10 @@ def test_reset_cadence_survives_an_embedding_error(db: Session, tmp_path: Path) session = SessionLocal() try: - summary = asyncio.run( - import_sources([root], llm, prune=False, session=session) - ) + summary = asyncio.run(import_sources([root], llm, prune=False, session=session)) assert (summary.files, summary.errors) == (FILE_COUNT, 1) assert summary.added == FILE_COUNT - 1 - assert summary.chunks == EXPECTED_CONTENT_CHUNKS - ( - EXPECTED_CONTENT_CHUNKS // FILE_COUNT - ) + assert summary.chunks == EXPECTED_CONTENT_CHUNKS - (EXPECTED_CONTENT_CHUNKS // FILE_COUNT) assert summary.summaries == EXPECTED_SUMMARY_CHUNKS - 1 assert _map_size(session) <= MAP_BOUND finally: @@ -743,9 +728,7 @@ def test_a_clean_row_leaves_the_map_and_a_dirty_one_never_does(db: Session) -> N assert _map_size(session) == 0, "a clean committed row must be weakly held" row = session.scalar( - select(Document).where( - Document.source == PROBE_SOURCE, Document.path == "mechanism.md" - ) + select(Document).where(Document.source == PROBE_SOURCE, Document.path == "mechanism.md") ) assert row is not None row.file_hash = row.file_hash # the stamp: same value, dirty state @@ -765,9 +748,7 @@ def test_a_clean_row_leaves_the_map_and_a_dirty_one_never_does(db: Session) -> N assert _map_size(session) == 0 assert ( db.scalar( - select(func.count()) - .select_from(Document) - .where(Document.source == PROBE_SOURCE) + select(func.count()).select_from(Document).where(Document.source == PROBE_SOURCE) ) == 1 ) @@ -801,9 +782,7 @@ PRUNE_BATCH = 10 #: The SIX columns the prune loads — the ``load_only`` set of ``_prune`` #: (docstring: the light-row contract). An eighth name here means the walk #: got heavier; one missing means a guard lost the column it reads. -PRUNE_COLUMNS = frozenset( - {"id", "source", "path", "is_image", "image_path", "page_count"} -) +PRUNE_COLUMNS = frozenset({"id", "source", "path", "is_image", "image_path", "page_count"}) #: ``… FROM documents`` — the prune's own walk AND (that is the pin) any #: per-document follow-up lazy load of a deferred column. The word boundary @@ -862,9 +841,7 @@ def _projected_columns(statement: str) -> frozenset[str]: return frozenset(re.findall(r"documents\.\w+", head)) -def _seed_prune_docs( - session: Session, *, page_dir: Path | None = None -) -> list[UUID]: +def _seed_prune_docs(session: Session, *, page_dir: Path | None = None) -> list[UUID]: """``PRUNE_DOC_COUNT`` indexed docs of :data:`PRUNE_SOURCE`, each with ``PRUNE_CHUNKS_PER_DOC`` embedded chunks and a full-fat ``content``. @@ -924,9 +901,7 @@ def _arm_commit_counter(session: Session) -> list[int]: return commits -def test_prune_holds_no_content( - db: Session, tmp_path: Path, statement_capture: list[str] -) -> None: +def test_prune_holds_no_content(db: Session, tmp_path: Path, statement_capture: list[str]) -> None: """THE PRUNE PROBE: the walk loads SIX columns and never ``content``. 30 documents × 5 chunks are pruned through the importer's own ``_prune`` @@ -971,10 +946,7 @@ def test_prune_holds_no_content( "prune — a deferred column is being read per row, so the light row " "is incomplete (add the column to load_only)" ) - projected = { - col.removeprefix("documents.") - for col in _projected_columns(selects[0]) - } + projected = {col.removeprefix("documents.") for col in _projected_columns(selects[0])} assert projected == PRUNE_COLUMNS, ( f"the prune's row is not the six-column light row: {sorted(projected)}" ) @@ -985,12 +957,11 @@ def test_prune_holds_no_content( # Everything really went, children included (no orphans — phase 183 D4, # asserted here too so a light row cannot quietly change it). - assert db.scalar( - select(func.count()).select_from(Document).where(Document.source == PRUNE_SOURCE) - ) == 0 - assert db.scalar( - select(func.count()).select_from(Chunk).where(Chunk.document_id.in_(ids)) - ) == 0 + assert ( + db.scalar(select(func.count()).select_from(Document).where(Document.source == PRUNE_SOURCE)) + == 0 + ) + assert db.scalar(select(func.count()).select_from(Chunk).where(Chunk.document_id.in_(ids))) == 0 assert sorted(summary.changed_paths) == sorted( (PRUNE_SOURCE, f"docs/doc-{n:04d}.md") for n in range(PRUNE_DOC_COUNT) ) @@ -1001,9 +972,7 @@ def test_prune_holds_no_content( assert _map_classes(db) == {} -def test_prune_without_sources_touches_nothing( - db: Session, statement_capture: list[str] -) -> None: +def test_prune_without_sources_touches_nothing(db: Session, statement_capture: list[str]) -> None: """The empty-source guard is a real short-circuit, not a walk for naught. ``_prune`` with no source names returns ``0`` before the SELECT — so a @@ -1017,9 +986,10 @@ def test_prune_without_sources_touches_nothing( assert _prune(db, set(), set(), summary=summary, prune_batch=10) == 0 assert statement_capture == [] assert summary.changed_paths == [] - assert db.scalar( - select(func.count()).select_from(Document).where(Document.source == PRUNE_SOURCE) - ) == PRUNE_DOC_COUNT + assert ( + db.scalar(select(func.count()).select_from(Document).where(Document.source == PRUNE_SOURCE)) + == PRUNE_DOC_COUNT + ) def test_prune_batch_commits(db: Session) -> None: @@ -1059,11 +1029,10 @@ def test_prune_batch_commits(db: Session) -> None: session.close() # The FK cascade did the child work in EVERY transaction the # prune opened, not just the last one. - assert db.scalar( - select(func.count()) - .select_from(Chunk) - .where(Chunk.document_id.in_(ids)) - ) == 0 + assert ( + db.scalar(select(func.count()).select_from(Chunk).where(Chunk.document_id.in_(ids))) + == 0 + ) pruned, summary, commits = run(PRUNE_BATCH) assert pruned == PRUNE_DOC_COUNT @@ -1074,9 +1043,10 @@ def test_prune_batch_commits(db: Session) -> None: assert sorted(summary.changed_paths) == sorted( (PRUNE_SOURCE, f"docs/doc-{n:04d}.md") for n in range(PRUNE_DOC_COUNT) ) - assert db.scalar( - select(func.count()).select_from(Document).where(Document.source == PRUNE_SOURCE) - ) == 0 + assert ( + db.scalar(select(func.count()).select_from(Document).where(Document.source == PRUNE_SOURCE)) + == 0 + ) # …and the pre-phase run does the SAME work in ONE transaction. _truncate(db) @@ -1087,9 +1057,10 @@ def test_prune_batch_commits(db: Session) -> None: "reproduce the pre-phase single transaction exactly" ) assert sorted(summary_0.changed_paths) == sorted(summary.changed_paths) - assert db.scalar( - select(func.count()).select_from(Document).where(Document.source == PRUNE_SOURCE) - ) == 0 + assert ( + db.scalar(select(func.count()).select_from(Document).where(Document.source == PRUNE_SOURCE)) + == 0 + ) def test_prune_batch_setting_reaches_the_prune( @@ -1137,9 +1108,7 @@ def test_prune_batch_setting_reaches_the_prune( monkeypatch.setattr(importer, "_prune", spy) second = asyncio.run(import_sources([root], llm, prune=True, session=db)) - assert seen_kwargs == [3], ( - "import_sources did not pass Settings.sync_prune_batch to _prune" - ) + assert seen_kwargs == [3], "import_sources did not pass Settings.sync_prune_batch to _prune" assert (second.files, second.pruned, second.errors) == (1, 3, 0) assert sorted(path for (_src, path) in second.changed_paths) == [ "keep-0.md", @@ -1152,16 +1121,99 @@ def test_prune_batch_setting_reaches_the_prune( select(func.count()).select_from(Chunk).where(Chunk.embedding.is_not(None)) ) assert embedded is not None and embedded > 0 - assert db.scalar( - select(func.count()) - .select_from(Chunk) - .where(Chunk.document_id.not_in(select(Document.id))) - ) == 0 + assert ( + db.scalar( + select(func.count()) + .select_from(Chunk) + .where(Chunk.document_id.not_in(select(Document.id))) + ) + == 0 + ) -def test_prune_guards_unaffected_with_light_rows( - db: Session, tmp_path: Path -) -> None: +def test_the_summary_backfill_survives_the_reset_cadence(db: Session, tmp_path: Path) -> None: + """The constraint that fixes WHERE the cadence may sit, in ONE file. + + ``_unchanged_doc`` (phase 118) backfills a NULL summary by calling + :func:`app.rag.summarizer.summarize_document`, which reads + ``doc.content`` AFTER the row was loaded and its ``file_hash`` stamped. + That local is live across statements: a + ``session.expunge_all()`` moved into ``_unchanged_doc`` — or a + ``noload``/``raiseload`` added to the row it walks — would discard or + break the very row the backfill reads. Phase 185 task 01 chose + ``defer`` over ``noload``/``raiseload`` (LOCKED A2) for the same reason, + and task 05 of THIS phase pinned the choice on the statement itself + (``tests/unit/test_importer_defer_content.py``); this pins the SAME + constraint on the run. + + The fixture is a document whose ``is_summary`` chunk is missing while + its ``file_hash`` matches (a re-sync with a NULL summary — exactly the + row the backfill exists for), imported at the tightest cadence the + setting allows that still resets inside a 12-file walk. Three + observables, none of them a memory proxy: + + * the backfill RUNS — one and only one ``is_summary`` chunk appears, + carrying the fake-embedder's summary text; + * every other file is still ``unchanged`` and the summary count is + exactly the file count afterwards, i.e. the reset cadence destroyed + no summary on the way through; + * the doc's own text is still intact afterwards (``content`` was never + rewritten by a re-index). + """ + root = _seed_files(tmp_path) + seed = SessionLocal() + try: + first = asyncio.run(import_sources([root], _probe_llm(), session=seed)) + assert first.added == FILE_COUNT + finally: + seed.close() + + # Hand the next sync ONE document with a NULL summary: it takes + # ``_unchanged_doc``'s backfill branch instead of the early return. + probe_rel = "probe-00.md" + before = _kb_snapshot(db)[probe_rel] + target = db.execute( + select(Document).where(Document.source == PROBE_SOURCE, Document.path == probe_rel) + ).scalar_one() + db.query(Chunk).filter(Chunk.document_id == target.id, Chunk.is_summary.is_(True)).delete() + target.summary = None + db.commit() + db.expunge_all() + assert _kb_snapshot(db)[probe_rel][1] is None + + session = SessionLocal() + try: + summary = asyncio.run( + import_sources( + [root], _probe_llm(reset_files=RESET_FILES), prune=False, session=session + ) + ) + finally: + session.close() + # Everything stayed unchanged — the backfill never re-indexed. + assert (summary.files, summary.added, summary.updated, summary.unchanged) == ( + FILE_COUNT, + 0, + 0, + FILE_COUNT, + ) + assert (summary.errors, summary.summary_backfilled) == (0, 1) + + snap = _kb_snapshot(db) + backfilled = snap[probe_rel] + assert backfilled[1] is not None, "the summary-NULL backfill did not run" + # The one doc that needed the deferred read still has its OWN text and + # its own hash: a summary was added, the document was not rewritten. + assert backfilled[0] == before[0] + assert backfilled[2] == before[2] + # And the cadence destroyed nothing on its way through: every file is + # back to 5 content chunks + 1 summary chunk. + assert {p: c for p, (_, _, _, c) in snap.items()} == { + f"probe-{n:02d}.md": 6 for n in range(FILE_COUNT) + } + + +def test_prune_guards_unaffected_with_light_rows(db: Session, tmp_path: Path) -> None: """The guards run off SIX columns — the light row is COMPLETE. A column missing from ``load_only`` does not announce itself: the guard @@ -1216,9 +1268,7 @@ def test_prune_guards_unaffected_with_light_rows( seen = {(PRUNE_SOURCE, "notes/kept.md"), (PRUNE_SOURCE, "deck.pdf")} session = SessionLocal() try: - pruned_toggle_off = _prune( - session, {PRUNE_SOURCE}, seen, images=False, prune_batch=0 - ) + pruned_toggle_off = _prune(session, {PRUNE_SOURCE}, seen, images=False, prune_batch=0) finally: session.close() # Toggle OFF: BOTH image docs are invisible to the walk, not deleted — @@ -1228,9 +1278,7 @@ def test_prune_guards_unaffected_with_light_rows( session = SessionLocal() try: - pruned_toggle_on = _prune( - session, {PRUNE_SOURCE}, seen, images=True, prune_batch=0 - ) + pruned_toggle_on = _prune(session, {PRUNE_SOURCE}, seen, images=True, prune_batch=0) finally: session.close() # Toggle ON: the deleted image file prunes its row AND its copy (the @@ -1240,8 +1288,82 @@ def test_prune_guards_unaffected_with_light_rows( assert not gone_copy.exists() assert kept_copy.exists() assert sorted( - path - for (path,) in db.execute( - select(Document.path).where(Document.source == PRUNE_SOURCE) - ) + path for (path,) in db.execute(select(Document.path).where(Document.source == PRUNE_SOURCE)) ) == ["deck.pdf", "deck.pdf#page=1#img=0.png", "notes/kept.md"] + + +def test_both_prune_guards_cost_the_same_statement_budget( + db: Session, tmp_path: Path, statement_capture: list[str] +) -> None: + """The guards add NO hidden read — proved by statement count, both ways. + + :func:`test_prune_guards_unaffected_with_light_rows` above proves the + guards DECIDE correctly off the six light columns. This pins the other + half of the same contract, the half that is invisible in behaviour: a + guard reaching for a deferred attribute would still decide correctly and + only show up as ONE MORE SELECT PER ROW. So the assertion here is a + statement count, not an outcome: + + * the SAME projection runs under both toggles — one candidate walk + apiece, naming the same six columns (images OFF only skips rows + through ``is_image``, a column the walk already reads); + * neither walk projects ``documents.content``; + * deleting an image row + copying its file costs no extra document + SELECT (the ``image_path`` the copy cleanup uses is in the projection). + + Total: exactly TWO document SELECTs for the two syncs, whatever the + guards do. + """ + image_dir = tmp_path / "image_dir" + image_dir.mkdir() + gone_copy = image_dir / "gone.png" + gone_copy.write_bytes(b"\x89PNG\r\n\x1a\n gone") + + def add(path: str, *, is_image: bool, copy: Path | None = None) -> None: + db.add( + Document( + source=PRUNE_SOURCE, + path=path, + full_path=f"/srv/brain/{PRUNE_SOURCE}/{path}", + title=f"Budget probe {path}", + content=PRUNE_CONTENT, + content_hash=f"{abs(hash(path)):064x}"[:64], + is_image=is_image, + image_path=str(copy) if copy else None, + ) + ) + + add("notes/kept.md", is_image=False) + add("notes/gone.md", is_image=False) + add("img/gone.png", is_image=True, copy=gone_copy) + add("deck.pdf#page=1#img=0.png", is_image=True) + db.commit() + + seen = {(PRUNE_SOURCE, "notes/kept.md")} + projections: list[frozenset[str]] = [] + for toggle in (False, True): + since = len(statement_capture) + session = SessionLocal() + try: + _prune(session, {PRUNE_SOURCE}, seen, images=toggle, prune_batch=0) + finally: + session.close() + # Only what THIS sync emitted — the capture is cumulative. + walks = [ + s for s in _doc_selects(statement_capture[since:]) if "WHERE documents.source" in s + ] + assert len(walks) == 1, ( + f"the images={toggle} prune ran {len(walks)} candidate walks; " + "one per sync is the budget (a second is a per-row lazy load)" + ) + projections.append(_projected_columns(walks[0])) + + assert projections[0] == projections[1], ( + "the two toggles read DIFFERENT columns — a guard grew a deferred " + f"read:\n off: {sorted(projections[0])}\n on: {sorted(projections[1])}" + ) + assert "documents.content" not in projections[0] + assert len(_doc_selects(statement_capture)) == 2, ( + "two syncs must cost exactly two document SELECTs in total, guards " + "included — any extra is a per-row read the light row must cover" + ) diff --git a/tests/unit/test_importer_defer_content.py b/tests/unit/test_importer_defer_content.py new file mode 100644 index 0000000..e64b6d6 --- /dev/null +++ b/tests/unit/test_importer_defer_content.py @@ -0,0 +1,258 @@ +"""Phase 185, task 02 — the ONE fast-path row statement (unit pins). + +``app/rag/importer.py::_doc_row_stmt`` is the single place the importer's +unchanged fast path (phase 147) loads a ``Document`` row, and it defers +exactly ONE column: ``Document.content``. That shape is load-bearing twice +over — it is what keeps a full re-sync's memory bound to row METADATA +instead of to the corpus (the 2026-10-15 incident: 17,407 rows × avg +15,217 chars = 252.6 MB of markdown hydrated for a run that re-indexed +217 files), AND it is what must NOT grow into a lazy-load landmine six +phases from now. + +Pinned here (the behaviour itself — the captured SQL, the one-SELECT-per-file +count and the backfill's single lazy read — lives in +``tests/integration/test_import_fast_path_columns.py``): + +* the statement defers ``content`` and NOTHING ELSE: its projection names + every other mapped column of ``Document``, so a column a later phase adds + keeps today's whole-row semantics (LOCKED A1 — no ``load_only`` allowlist, + which would silently defer every future column and resurface as an + unexplained N+1); +* the statement is the ``(source, path)`` lookup the fast path has always + used — the deferral changed the projection, never the predicate; +* all FOUR fast-path row loads call the helper (``_index_file``, + ``_index_pdf_file``, ``_index_one_embedded_image``, + ``_index_image_file``) and none of them builds its own + ``select(Document)`` — the four sites cannot drift; +* the importer contains no ``raiseload``/``noload`` (LOCKED A2): the + phase-118 summary-NULL backfill genuinely reads ``doc.content`` and must + pay one SELECT, not crash; +* reading the deferred column through a real session still yields the stored + text (the honest "plain ``defer`` keeps today's behaviour" claim). +""" +from __future__ import annotations + +import ast +import inspect +import re +import uuid +from collections.abc import Iterator + +import pytest +from sqlalchemy import inspect as sa_inspect +from sqlalchemy import text +from sqlalchemy.dialects import postgresql +from sqlalchemy.orm import Session + +from app.models import Document +from app.rag import importer +from app.rag.importer import _doc_row_stmt + +#: The four loads that share the phase-147 unchanged fast path (phase 185 +#: D1) — the ONLY call sites of :func:`_doc_row_stmt`. +FAST_PATH_FUNCTIONS = ( + "_index_file", + "_index_pdf_file", + "_index_one_embedded_image", + "_index_image_file", +) + +#: The helper call as it appears at an importer call site (``session.scalar( +#: _doc_row_stmt(source, rel))``) — counted, not trusted. +_HELPER_CALL = "_doc_row_stmt(" + +#: ``documents.`` inside a compiled projection (everything the +#: statement SELECTs before ``FROM``). The loader options are invisible in +#: the ORM statement's own column list — only the COMPILED projection shows +#: what actually travels to the database, which is what this phase is about. +_PROJECTION_COLUMN_RE = re.compile(r"documents\.([a-z_]+)") + + +def _projected_columns() -> set[str]: + """The ``documents`` columns the fast-path statement actually SELECTs.""" + compiled = str(_doc_row_stmt("s", "p").compile(dialect=postgresql.dialect())) + projection = compiled.split("\nFROM")[0].split(" FROM ")[0] + return set(_PROJECTION_COLUMN_RE.findall(projection)) + + +def test_helper_defers_content_and_nothing_else() -> None: + """``content`` is the ONLY column missing from the fast-path projection. + + The compiled projection is compared against the mapper's FULL column set, + so a future ``Document`` column must appear there — if someone swaps the + ``defer`` for a ``load_only`` allowlist, the new column goes missing and + this test fails (which is exactly the drift LOCKED A1 forbids). + """ + mapped = {c.name for c in sa_inspect(Document).columns} + projected = _projected_columns() + + assert "content" in mapped, "Document.content vanished — this pin needs a new shape" + assert projected, "the fast-path statement projects no documents columns at all" + assert "content" not in projected, ( + "the fast-path row statement no longer defers Document.content — a " + "full walk would hydrate the corpus markdown again (252.6 MB on the " + "incident box)" + ) + assert projected == mapped - {"content"}, ( + "the fast-path row statement defers more (or other) columns than " + f"content: missing={sorted(mapped - projected - {'content'})} " + f"unexpected={sorted(projected - mapped)} — only Document.content " + "may be deferred (LOCKED A1)" + ) + + +def test_helper_keeps_the_fast_path_predicate() -> None: + """The predicate stays the ``(source, path)`` lookup of phase 147.""" + compiled = str(_doc_row_stmt("the-source", "the/path.md").compile()).lower() + + assert "documents.source" in compiled + assert "documents.path" in compiled + assert "where" in compiled + # The bind names the integration probe attributes statements by. + assert "source_1" in compiled and "path_1" in compiled + + +def test_every_fast_path_load_calls_the_helper() -> None: + """All four fast-path row loads go through ONE statement (phase 185 D1). + + Source-level on purpose: the deferral must be impossible to bypass, and + a fifth whole-entity load would silently undo the phase for one document + class. The two per-source post-pass walks + (``_enrich_pending_images``, ``_backfill_image_contexts``) and the prune + (``load_only`` of six light columns, phase 184) are NOT fast-path loads + and are deliberately excluded — see the module docstring of + ``tests/integration/test_import_fast_path_columns.py``. + """ + for name in FAST_PATH_FUNCTIONS: + fn = getattr(importer, name) + src = inspect.getsource(fn) + assert _HELPER_CALL in src, ( + f"{name} no longer loads its row through importer.{_HELPER_CALL} — " + "the four fast-path row loads must share the ONE deferring " + "statement so they cannot drift" + ) + assert "select(Document)" not in src, ( + f"{name} builds its own select(Document) row again — a whole-entity " + "fast-path load re-hydrates Document.content (phase 185)" + ) + + +def test_helper_has_exactly_the_four_fast_path_call_sites() -> None: + """The importer calls the helper exactly four times (one per fast path). + + A fifth ``session.scalar(_doc_row_stmt(…)`` would mean a new consumer was + wired to the fast-path statement (it is the ``(source, path)`` deferring + lookup, not a general-purpose loader), and a surviving whole-entity + ``(source, path)`` ``select(Document)`` would mean a bypass. + """ + src = inspect.getsource(importer) + calls = src.count(f"session.scalar({_HELPER_CALL}") + assert calls == len(FAST_PATH_FUNCTIONS), ( + f"importer.py calls session.scalar({_HELPER_CALL}…) {calls} times — " + f"expected exactly {len(FAST_PATH_FUNCTIONS)} (the four fast-path " + "row loads)" + ) + # No fast-path-shaped whole-entity load survives next to the helper. + assert src.count("select(Document).where(Document.source ==") == 0, ( + "a (source, path) whole-entity Document load is back — route it " + "through _doc_row_stmt or give it its own explicit projection" + ) + + +def test_no_raiseload_or_noload_anywhere_in_the_importer() -> None: + """LOCKED A2: the deferred column must QUERY, never raise or come back None. + + ``raiseload``/``noload`` would turn the phase-118 summary-NULL backfill + (``_unchanged_doc`` step 3, ``content=doc.content``) into a crash or an + empty summary — a behaviour regression on the one branch that genuinely + needs the stored markdown. The accepted cost is one SELECT for that row. + + Checked through the AST, not the text: the module's own docstrings argue + against both options by NAME, so a substring scan would fail on the + prose. Only real call/import names count here. + """ + tree = ast.parse(inspect.getsource(importer)) + called: set[str] = set() + imported: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Call): + if isinstance(node.func, ast.Name): + called.add(node.func.id) + elif isinstance(node.func, ast.Attribute): + called.add(node.func.attr) + elif isinstance(node, ast.ImportFrom | ast.Import): + imported.update(alias.name for alias in node.names) + + for forbidden in ("raiseload", "noload"): + assert forbidden not in called | imported, ( + f"importer.py calls/imports {forbidden} — phase 185 LOCKED A2 " + "keeps plain defer() so the summary-NULL backfill reads the " + "stored content instead of raising" + ) + assert "defer" in called, "the fast path no longer calls defer() at all" + # The importer's ONLY load_only is _prune's six-light-column row + # (phase 184) — it must never reach the fast path, whose shape is + # "everything except content" (LOCKED A1). + prune_src = inspect.getsource(importer._prune) + assert "load_only" in inspect.getsource(importer) + assert "defer" not in prune_src, ( + "_prune picked up a defer() — its light row is an explicit " + "load_only allowlist by phase 184, not a deferral" + ) + + +@pytest.fixture() +def probe_row(db: Session) -> Iterator[tuple[str, str]]: + """One seeded row (raw SQL — this file pins a SELECT, not the importer) + removed again after the test.""" + source = f"phase185-unit-{uuid.uuid4().hex[:8]}" + rel = "notes/deferred.md" + db.execute( + text( + "INSERT INTO documents (id, source, path, full_path, title, content, " + "content_hash, file_hash, indexed_at, created_at, created_at_manual, " + "summary, is_image) VALUES (:id, :source, :path, :full_path, :title, " + ":content, :content_hash, :file_hash, now(), now(), false, :summary, " + "false)" + ), + { + "id": uuid.uuid4(), + "source": source, + "path": rel, + "full_path": f"/tmp/{source}/{rel}", + "title": "Deferred", + "content": "the stored markdown body — phase 185", + "content_hash": "ch", + "file_hash": "fh", + "summary": None, + }, + ) + db.commit() + db.expunge_all() + try: + yield source, rel + finally: + db.execute(text("DELETE FROM documents WHERE source = :s"), {"s": source}) + db.commit() + db.expunge_all() + + +def test_deferred_content_still_reads_back(db: Session, probe_row: tuple[str, str]) -> None: + """A deferred column is LAZY, not gone: the row reads back whole. + + The trade phase 185 accepted (D2): the metadata arrives with the row, and + the markdown arrives on demand for the one reader that asks for it. With + ``raiseload`` this would raise; with ``noload`` it would be ``None`` — + both would break the phase-118 backfill, and both are pinned out by + :func:`test_no_raiseload_or_noload_anywhere_in_the_importer`. + """ + source, rel = probe_row + + doc = db.scalar(_doc_row_stmt(source, rel)) + + assert doc is not None + assert doc.file_hash == "fh" + assert doc.summary is None + assert doc.content == "the stored markdown body — phase 185" + # And the second read is served from the identity map, not the database. + assert doc.content == "the stored markdown body — phase 185"