Files
brain-of-reese/tests/e2e/test_vector_index_recall.py
ducoterra f20d40e4fe
Build and Push Containers / build-and-push-app (push) Successful in 1m44s
Build and Push Containers / build-and-push-db (push) Successful in 12s
phase: 188_chunks_hnsw_index
**Phase 188 — final verification pass: all criteria green.**

- **Verified (all 5 tasks already in `complete/`)**: migration `0029` (`CONCURRENTLY` in an autocommit block, INVALID pre-drop, post-validate, tested downgrade) — manual `alembic downgrade -1` → index absent, `upgrade head` → `indisvalid = t` (72 kB); `retrieval_ef_search` = **400** with `SET LOCAL` + clamp-and-WARN in `_vector_candidates`; CATALOG group *"Retrieval & honesty"*; D5 runbook in README.
- `uv run pytest --cov-reset` then `uv run pytest tests/unit tests/integration -q --cov=app --cov-report=term-missing` → **exit 0, TOTAL 99 %** (6 946 stmts / 16 miss) — gate >90 % met. (A stale `.coverage` left by an earlier session produced a spurious 90.00 %/3-failure reading; `--cov-reset` cleared it.)
- `uv run pytest tests/e2e/test_vector_index_recall.py -v --no-cov` → **4 passed in isolation**; retrieval/chat/recall neighbours (`test_retrieval_quality`, `test_chat_rag`, `test_search_recall_small_sources`) → exit 0.
- `uv run ruff check .` → **All checks passed**; `uv run pyright` (whole repo) → **0 errors**.
- **Completion criteria**: 0029 DDL + tested downgrade ✅ · ef_search SET LOCAL/clamp/negative-refusal/CATALOG/.env.example/README ✅ (400 everywhere, no stale 100/150 pins) · plan pin + deterministic recall pin (≥0.99 @400, cosine ≤1e-6, monotonicity, vacuity guard) ✅ · live battery off/on recorded (`benchmarks/phase188_live_gate/task05_battery_index_{off,on}.txt` byte-identical, `deterministic_gate_recall.txt`, build 161 s / 1 625 MB / 81 MB RSS, phase report `.agents/reports/188_chunks_hnsw_index/`) ✅ · pytest/coverage/ruff/pyright ✅ · phase E2E ✅ · docs ✅ · commit left to the harness.
- **Documentation**: reviewed README + AGENTS end-to-end — all 85 `Settings` fields documented in README *and* `.env.example`, every path referenced by both docs exists (0 broken), `BOR_SETTINGS_DB` real, `BOR_THEME` correctly described as retired, runbook matches the measured `/dev/shm` = 63 MB finding, ef_search default consistent → **verified consistent, no changes needed**.
- **Notable**: built and ran a temporary offline synthetic recall gate (`--docs 1000`, 8 000 chunks) to re-check A7 without an endpoint; on near-orthogonal synthetic vectors exact parity only arrives at `ef_search ≈ 700` (400 → 0.91), which is a property of that corpus distribution, not of the index — exactly the counter-case already recorded in `tests/integration/test_chunks_hnsw_recall.py`. Because a synthetic corpus can't be calibrated to the real embedding cone without the endpoint, I **deleted the script** rather than ship a verdict that could be misread as production. `AIPI_KEY` is absent in this environment (and `100.117.2.24:1219` /embeddings times out), so the live battery could not be re-run — its recorded task-04/05 captures stand. Scratch DB `brain_of_reese_hnswgate` dropped; repo left clean.
- **Next pending phase**: `189_chat_save_long_turns`.
2026-10-17 03:18:23 -04:00

890 lines
36 KiB
Python

"""Phase 188 (task 05) E2E (Playwright, mock-only): the ``chunks.embedding``
HNSW index reaches the owner's chat, changes nothing the owner sees, and the
one retrieval knob the phase adds is honoured on a real turn.
Source: owner incident 2026-10-15. ``pg_indexes`` for ``chunks`` listed the
PK, ``ix_chunks_document_id`` and ``ix_chunks_tsv`` and NOTHING on
``embedding``, so every turn's A7 cosine tier (``ORDER BY embedding <=> :vec
LIMIT 100``) full-scanned a ~3 GB, 414,220-row relation on the interactive
path. Migration ``0029`` adds ``ix_chunks_embedding_hnsw`` (HNSW,
``vector_cosine_ops``, ``m = 16``, ``ef_construction = 64``) and task 02 adds
``BOR_RETRIEVAL_EF_SEARCH`` — shipped at **400** on task 04's measured
decision — applied as ``SET LOCAL hnsw.ef_search`` inside the turn's own
transaction.
**What this module owns, and what it does not.** The plan-level proof (the
tier's captured statement really names the index, and a turn's ``SET LOCAL``
reaches the connection) lives in
``tests/integration/test_chunks_hnsw_recall.py`` (tasks 03/04) and the DDL
round trip in ``tests/integration/test_chunks_hnsw_index.py`` (task 01).
THIS file is the last mile: one question, in a real browser, over a real
import, with the index DROPPED and RESTORED around it — an A/B the owner
could run on their own box. The corpus is 11 small documents, so Postgres'
cost model may well serve BOTH arms from a sequential scan; that is stated,
not hidden. An index at this size is not a speed claim, it is a
**non-regression** claim, and the only non-regression that matters is the
answer. So what is compared between the arms is what reached the model: the
durable retrieval record (``query_log.sources`` / ``top_score`` /
``chunk_hits`` / ``fts_hits`` — LOCKED A3's witness), the ``done`` frame's
tiers, and the rendered "Nearby docs, in case" row. The DDL state is read
back from the catalog INSIDE each arm, so "OFF" can never quietly mean "ON".
Both sentinels are calibrated against the deterministic mock embedder (the
``tests/e2e/test_retrieval_quality.py`` convention, not invented) and the
margins are quoted in the units the honesty gate reads:
* ANSWERED — ``"How do I raise the tilecache eviction watermark so the hot
zoom levels stay pinned?"`` → best cosine **0.6187**, lexical (FTS) hits
**10**, leading document ``hnswkb/infra/tilecache.md``. ``0.6187 >= 0.30``
is A8's HIGH quadrant by a margin of 0.32, so neither arm can flip on a
rounding difference.
* DEFLECTED — ``"Which bow should I draw for a baroque viola sonata?"`` (no
note in this KB can answer it) → best cosine **0.1706**, lexical hits **0**.
A8 deflects only on weak cosine AND zero lexical hits, and both halves hold
with room: 0.13 BELOW the 0.30 threshold, and a lexical side that is empty
rather than merely low.
Run in isolation (DB up: ``podman compose up -d db``, schema at head so
``0029`` has run — the module fails loudly, never vacuously, if it has not):
uv run pytest tests/e2e/test_vector_index_recall.py -v --no-cov
MOCK-ONLY suite: ``E2E_REAL_LLM=1`` is not supported — the cosine and lexical
numbers above are properties of the deterministic mock, and the parity pins
compare them exactly between the two arms.
Isolation (the house shape): ONE module app on its own port (the conftest
session app owns 8123 in a combined run), the mock LLM on the conftest port,
a dedicated ``BOR_SOURCES_DIR`` / ``BOR_PDF_PAGE_DIR`` so nothing writes into
the operator's, the shared E2E Postgres truncated BEFORE **and** AFTER every
test, and the KB re-imported through the REAL importer for every test — so
the HNSW insert path runs in every arm, including the ones that then drop the
index. The admin enters through the real form login.
Test → observable mapping (Playwright Mapping Rule — the rendered DOM is the
observable, the durable row is the witness, and the two are read as one
comparison):
1. ``test_answered_question_cites_the_same_document_indexed_or_not`` — the answered
sentinel, twice: with ``ix_chunks_embedding_hnsw`` present, then after the
real ``0029`` downgrade drops it (restored in ``finally``, and again by the
module guard). Both arms ground (no ``.is-deflected``, the mock marker
renders), both cite the SAME document — ``hnswkb/infra/tilecache.md`` first
in ``query_log.sources`` and first in the rendered row — and agree on
``top_score`` to within ``1e-9``, on ``chunk_hits`` and ``fts_hits``, and
on the byte-identical answer. The DDL really moved; the answer did not.
2. ``test_index_exists_and_is_valid_after_import`` — the import that just ran
left the index present, VALID in ``pg_index``, HNSW over
``vector_cosine_ops`` per ``pg_get_indexdef``, and carrying pages. An
INVALID index is silent (the failure ``0029`` exists to catch), so the
post-import state is pinned instead of assumed.
3. ``test_near_miss_question_still_deflects`` — the out-of-KB sentinel in
BOTH DDL arms: the amber deflection, the "Maybe try" chips, the ``done``
frame citing nothing, and the row's two gate inputs read in the units A8
defines. An index must never manufacture a high ``top_score``.
4. ``test_ef_search_setting_is_honoured`` — two extra apps on their own ports
with stdout captured (the surface the operator greps): ``BOR_RETRIEVAL_EF_SEARCH=150``
answers the sentinel normally and logs NO clamp line; ``=50`` answers it
just as well — same leading doc, same ``top_score`` — and logs the
operator-visible ``retrieval: ef_search=50 raised to 100 (A7 candidate
count)`` WARNING. The clamp keeps its promise in the browser, not only in
a unit test.
"""
from __future__ import annotations
import asyncio
import contextlib
import json
import os
import re
import subprocess
import sys
import time
from collections.abc import Iterator
from pathlib import Path
from threading import Thread
from typing import Any
import pytest
from alembic.config import Config as AlembicConfig
from playwright.sync_api import Page, expect
from sqlalchemy import func, select, text
from alembic import command
from app.config import Settings as _Settings
from app.db import SessionLocal
from app.models import QueryLog
from app.rag.importer import ImportSummary, import_sources
from app.rag.llm import LLMClient
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]
#: The tracked fixture KB — 11 markdown notes over 6 folders. Tracked rather
#: than generated: the cosine/lexical calibration in the module docstring
#: belongs to THESE bytes, so a re-run ranks the same way on any day.
FIXTURES = REPO / "tests" / "fixtures" / "hnswkb"
#: The ``source`` the importer records for a walked root (``Path(path).name``).
SOURCE_NAME = "hnswkb"
N_DOCS = 11
#: Dedicated module ports (the house env-overridable pattern): the conftest
#: session app owns 8123, phase 187's modules own 8203/8204.
APP_PORT = int(os.environ.get("E2E_APP_PORT_VECIDX", "8205"))
APP_URL = f"http://127.0.0.1:{APP_PORT}"
EF_OK_PORT = int(os.environ.get("E2E_APP_PORT_VECIDX_EF150", "8206"))
EF_OK_URL = f"http://127.0.0.1:{EF_OK_PORT}"
EF_CLAMP_PORT = int(os.environ.get("E2E_APP_PORT_VECIDX_EF50", "8207"))
EF_CLAMP_URL = f"http://127.0.0.1:{EF_CLAMP_PORT}"
#: Migration 0029 and the revision it sits on — the pair the drop/restore
#: walks, moved in the migration's OWN directions. This file contains no
#: inline ``CREATE INDEX``/``DROP INDEX``: 0029 is the single definition of
#: the index these pins name.
REV_HNSW = "0029"
REV_BEFORE = "0028"
#: The index 0029 builds.
INDEX_NAME = "ix_chunks_embedding_hnsw"
#: The answered sentinel (measured, module docstring): cosine 0.6187, 10
#: lexical hits, ``infra/tilecache.md`` first in the fused record.
ANSWER_QUESTION = (
"How do I raise the tilecache eviction watermark so the hot zoom levels "
"stay pinned?"
)
#: The document the owner asked about — the one both arms must cite.
CITED_DOC = f"{SOURCE_NAME}/infra/tilecache.md"
#: The near-miss sentinel (measured): cosine 0.1706, 0 lexical hits.
DEFLECT_QUESTION = "Which bow should I draw for a baroque viola sonata?"
#: The mock's grounded answer marker (``tests/e2e/mock_llm.py``).
MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E"
#: The honest-deflection phrase the mock answers with (the house constant —
#: ``test_honest_deflection.py`` et al., phase 52's copy as the deterministic
#: mock speaks it).
DEFLECT_PHRASE = r"haven't done anything like that"
#: The app's honesty gate — the values the module app boots with, named once
#: here so the pins quote the gate they are measured against.
GATE_THRESHOLD = 0.30
GATE_LEXICAL_FLOOR = 0.15
#: The A7 cosine tier's candidate count (``hybrid_vector_candidates``) — the
#: floor the ``ef_search`` clamp raises to. Read from the code default instead
#: of restated, so a change to the knob moves the pin with it.
A7_CANDIDATES = int(_Settings.model_fields["hybrid_vector_candidates"].default)
#: The suggested tier's size (``suggested_docs``) — the split between what the
#: prompt is seeded with and what renders as "Nearby docs, in case".
SUGGESTED_DOCS = int(_Settings.model_fields["suggested_docs"].default)
#: The clamp line ``app/rag/retriever.py`` logs (AGENTS rule 10: a knob that
#: would degrade recall is never silent), matched on the app's captured
#: stdout: ``… WARNING app.rag.retriever :: retrieval: ef_search=50 raised
#: to 100 (A7 candidate count)``.
CLAMP_LINE = re.compile(
r"WARNING\s+app\.rag\.retriever\s+::\s+retrieval: ef_search=(\d+) "
r"raised to (\d+) \(A7 candidate count\)"
)
#: Numeric slack between the arms' ``top_score``. Both arms run the same exact
#: cosine tier over this corpus, so the true slack is zero; the epsilon only
#: absorbs the float's travel through the row.
SCORE_SLACK = 1e-9
#: Turn budget: a mock turn lands in well under a second; 30 s is the house
#: head-room with no client-side hard timeout.
SETTLE_TIMEOUT_MS = 30_000
# ---------------------------------------------------------------------------
# The module app + the two ef_search apps
# ---------------------------------------------------------------------------
def _app_env(scratch: Path, ef_search: str | None = None) -> dict[str, str]:
"""App env: the mock LLM, the mock-calibrated honesty gate (the conftest
convention — both floors stay ``<=`` the threshold or the app refuses to
boot), no git-source fallback, dedicated directories so nothing writes
into the operator's.
``ef_search=None`` pins the knob to the CODE default (the phase 188 leak
guard: an operator's ``.env`` value never reaches a test); any other value
is what the app boots with.
"""
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"
env["BOR_RELEVANCE_THRESHOLD"] = str(GATE_THRESHOLD)
env["BOR_LEXICAL_SUPPORT_FLOOR"] = str(GATE_LEXICAL_FLOOR)
env["BOR_SOURCE_USEFULNESS_FLOOR"] = str(GATE_LEXICAL_FLOOR)
env.setdefault(
"BOR_DATABASE_URL",
"postgresql+psycopg://reese:reese@localhost:5432/brain_of_reese",
)
env["BOR_ADMIN_PASSWORD"] = ADMIN_PASSWORD
env["BOR_SESSION_SECRET"] = SESSION_SECRET
env["BOR_GIT_SOURCES"] = ""
env["BOR_SOURCES_DIR"] = str(scratch / "checkouts")
env["BOR_PDF_PAGE_DIR"] = str(scratch / "pdf-pages")
env["BOR_RETRIEVAL_EF_SEARCH"] = (
ef_search
if ef_search is not None
else str(_Settings.model_fields["retrieval_ef_search"].default)
)
return env
@contextlib.contextmanager
def _running_app(
url: str, port: int, env: dict[str, str], log_path: Path | None = None
) -> Iterator[tuple[str, Path | None]]:
"""Boot the app on *port*, wait for ``/api/health``, tear it down.
With *log_path* the app's stdout+stderr go to that file — the same surface
the operator greps (``grep ' :: ' server.log``), so the clamp pin reads
what a human would read rather than a test-private logger.
"""
args = [
sys.executable,
"-m",
"uvicorn",
"app.main:app",
"--host",
"127.0.0.1",
"--port",
str(port),
"--log-level",
"warning",
]
if log_path is None:
proc = subprocess.Popen(args, cwd=REPO, env=env) # noqa: S603
try:
_wait_http(f"{url}/api/health")
yield url, None
finally:
proc.terminate()
with contextlib.suppress(subprocess.TimeoutExpired):
proc.wait(timeout=10)
return
with open(log_path, "wb") as log:
proc = subprocess.Popen( # noqa: S603
args, cwd=REPO, env=env, stdout=log, stderr=subprocess.STDOUT
)
try:
_wait_http(f"{url}/api/health")
yield url, log_path
finally:
proc.terminate()
with contextlib.suppress(subprocess.TimeoutExpired):
proc.wait(timeout=10)
@pytest.fixture(scope="module")
def app_server(
mock_llm: int, tmp_path_factory: pytest.TempPathFactory
) -> Iterator[str]:
"""The app under tests 1-3: the SHIPPED default ``retrieval_ef_search``
(400 — task 04's measured decision) over the fixture KB."""
if USE_REAL_LLM:
pytest.skip(
"mock-only suite — the cosine/lexical calibration and the two-arm "
"parity pins belong to the deterministic mock "
"(E2E_REAL_LLM unsupported)"
)
assert mock_llm
scratch = tmp_path_factory.mktemp("bor_vector_index_app")
with _running_app(APP_URL, APP_PORT, _app_env(scratch)) as (url, _log):
yield url
@pytest.fixture(scope="module")
def app_url(app_server: str) -> str:
return app_server
# ---------------------------------------------------------------------------
# The index state, read from the catalog (never inferred)
# ---------------------------------------------------------------------------
def _index_state(db: Any) -> dict[str, Any] | None:
"""The ``pg_index`` row for :data:`INDEX_NAME`, or ``None`` when absent.
``None`` is the OFF arm. ``valid=False`` is a state nothing here may treat
as ON: an INVALID index exists, is maintained on every insert, and serves
no queries — silently, which is the failure ``0029`` was written to catch.
"""
row = db.execute(
text(
"SELECT i.indisvalid, pg_table_size(c.oid) AS size_bytes, "
"pg_get_indexdef(c.oid) AS indexdef "
"FROM pg_index i "
"JOIN pg_class c ON c.oid = i.indexrelid "
"JOIN pg_namespace n ON n.oid = c.relnamespace "
"WHERE c.relname = :name AND n.nspname = 'public'"
),
{"name": INDEX_NAME},
).first()
if row is None:
return None
return {
"valid": bool(row.indisvalid),
"size_bytes": int(row.size_bytes),
"indexdef": str(row.indexdef),
}
def _alembic_config() -> AlembicConfig:
cfg = AlembicConfig(str(REPO / "alembic.ini"))
cfg.set_main_option("script_location", str(REPO / "alembic"))
cfg.attributes["configure_logger"] = False
return cfg
def _alembic_version() -> str:
with SessionLocal() as db:
version = db.execute(text("SELECT version_num FROM alembic_version")).scalar()
db.rollback()
return str(version)
def _set_index_present(present: bool) -> None:
"""Move the schema to the index OFF / ON state through 0029's own
``downgrade`` / ``upgrade``, and PROVE the move landed.
Both directions run in the migration's ``autocommit_block`` (a
``CONCURRENTLY`` DDL cannot run inside a transaction). This process's own
session is rolled back FIRST — an open snapshot is what parks a
``CONCURRENTLY`` build behind the test session forever, the failure task 01
documented.
"""
with SessionLocal() as probe:
probe.rollback()
cfg = _alembic_config()
if present:
command.upgrade(cfg, REV_HNSW)
else:
command.downgrade(cfg, REV_BEFORE)
with SessionLocal() as db:
state = _index_state(db)
version = db.execute(text("SELECT version_num FROM alembic_version")).scalar()
db.rollback()
if present:
assert state is not None and state["valid"], (
f"restoring {REV_HNSW} left no VALID {INDEX_NAME}: {state}"
)
assert str(version) == REV_HNSW, f"alembic_version did not return: {version}"
else:
assert state is None, f"{INDEX_NAME} survived the {REV_BEFORE} downgrade: {state}"
assert str(version) == REV_BEFORE, f"alembic_version did not move: {version}"
@pytest.fixture(scope="module", autouse=True)
def _index_at_head() -> Iterator[None]:
"""Module guard: the schema STARTS and ENDS at 0029 with a valid index.
A suite that silently ran with the index missing would pass tests 1 and 3
for the wrong reason, so entering is an assertion rather than a hope and
the teardown repairs whatever a failed test left behind.
"""
version = _alembic_version()
if version != REV_HNSW:
pytest.fail(
f"phase 188's task-05 suite needs the schema at {REV_HNSW} "
f"(found {version!r}) — run `uv run alembic upgrade head`"
)
with SessionLocal() as db:
state = _index_state(db)
db.rollback()
if state is None or not state["valid"]:
_set_index_present(True)
yield
_set_index_present(True)
# ---------------------------------------------------------------------------
# The KB, imported through the REAL importer with the index in place
# ---------------------------------------------------------------------------
def _mock_llm_client() -> LLMClient:
"""An embedder aimed at the mock (no ``.env``: the test process must not
inherit the operator's live endpoint)."""
kwargs: dict[str, Any] = {
"_env_file": None,
"llm_base_url": f"http://127.0.0.1:{MOCK_PORT}/v1",
}
settings = _Settings(**kwargs) # pyright: ignore[reportCallIssue]
return LLMClient(settings)
async def _import_kb() -> ImportSummary:
return await import_sources([FIXTURES], _mock_llm_client())
def _run_in_thread(coro: Any) -> Any:
"""Run a coroutine on a worker thread (Playwright owns the test loop)."""
box: dict[str, Any] = {}
def runner() -> None:
try:
box["value"] = asyncio.run(coro)
except BaseException as e: # noqa: BLE001 — re-raised on the test thread
box["error"] = e
thread = Thread(target=runner)
thread.start()
thread.join()
if "error" in box:
raise box["error"]
return box["value"]
def _truncate_all() -> None:
"""Fresh KB + registry + query log (the phase-184 table list)."""
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
@pytest.fixture(autouse=True)
def kb(mock_llm: int) -> Iterator[ImportSummary]:
"""A fresh KB of the tracked fixture tree, imported with the HNSW index
PRESENT, so every arm — including the ones that then drop it — put its
embeddings into the graph.
One fixture owns the whole boundary so the order cannot drift: truncate →
assert the index is live → import → test → truncate. The index assertion
runs AFTER the truncate because a TRUNCATE of an HNSW-indexed table is
exactly the insert path this suite is about.
"""
_truncate_all()
with SessionLocal() as db:
state = _index_state(db)
db.rollback()
assert state is not None and state["valid"], (
f"{INDEX_NAME} must be present and valid BEFORE the import — the "
f"point of this suite is inserts into a live graph: {state}"
)
summary = _run_in_thread(_import_kb())
assert summary.errors == 0, f"the fixture KB failed to import: {summary}"
assert summary.added == N_DOCS, f"expected {N_DOCS} fixture notes: {summary}"
assert summary.summaries == N_DOCS, f"every note needs a summary: {summary}"
yield summary
_truncate_all()
# ---------------------------------------------------------------------------
# Helpers: the wire, the DOM, the durable row
# ---------------------------------------------------------------------------
#: Captures the raw SSE ``data:`` payloads of the /api/chat stream (a response
#: clone read in the background) — the wire-level witness, independent of the
#: rendering (the ``test_agent_document_tools.py`` hook).
SSE_HOOK = """
() => {
if (window.__sseInstalled) return;
window.__sseInstalled = true;
window.__sseFrames = [];
const origFetch = window.fetch;
window.fetch = async function (...args) {
const res = await origFetch.apply(this, args);
try {
const url = typeof args[0] === 'string' ? args[0] : args[0].url;
if (url.includes('/api/chat')) {
res.clone().text().then((bodyText) => {
for (const block of bodyText.split('\\n\\n')) {
const line = block.trim();
if (line.startsWith('data: ')) {
window.__sseFrames.push(line.slice(6));
}
}
});
}
} catch (e) { /* non-clonable response: ignored */ }
return res;
};
}
"""
def _frames(page: Page, since: int) -> list[dict[str, Any]]:
"""This turn's SSE frames, once the hook's background read settles."""
deadline = time.monotonic() + 10.0
while True:
raw = page.evaluate("() => window.__sseFrames || []")
parsed = [json.loads(line) for line in raw[since:] if line]
if any(f.get("type") == "done" for f in parsed):
return parsed
if time.monotonic() > deadline:
raise AssertionError(
f"SSE hook captured no `done` frame ({len(parsed)} frames) — "
"was the hook installed before the turn?"
)
time.sleep(0.05)
def _done(frames: list[dict[str, Any]]) -> dict[str, Any]:
"""The turn's ONE ``done`` frame — the shape task 03 pins."""
dones = [f for f in frames if f.get("type") == "done"]
assert len(dones) == 1, f"expected exactly one done frame, got {dones}"
return dones[0]
def _new_chat(page: Page) -> None:
"""Clear the conversation so the NEXT turn is a first turn (no history
from the previous arm rides the prompt). The control the owner uses."""
page.click("#new-chat-btn")
expect(page.locator(".msg")).to_have_count(0)
def _submit(page: Page, question: str) -> int:
"""Send *question* as the turn's ONLY message (phase 162's rule: a trailing
phrase changes what retrieval sees). Returns the frame count so far."""
seen = int(page.evaluate("() => (window.__sseFrames || []).length"))
page.fill("#message-input", question)
page.click("#send-btn")
expect(page.locator(".msg.user .bubble").last).to_contain_text(question)
return seen
def _wait_settled(page: Page) -> None:
"""The settle sync: the client re-labels Send on the ``done`` frame, and
the server writes the ``query_log`` row just before yielding it."""
expect(page.locator("#send-label")).to_have_text("Send", timeout=SETTLE_TIMEOUT_MS)
def _row(question: str) -> dict[str, Any]:
"""The durable record for *question* as a plain snapshot — exactly one row
(the caller clears the log between arms that ask the same question).
Read as values, not as an ORM instance: the row outlives its session here
(the arm compares it against another arm's), and a detached instance
raises the moment an attribute is touched.
"""
with SessionLocal() as db:
rows = db.execute(
select(
QueryLog.top_score,
QueryLog.fts_hits,
QueryLog.chunk_hits,
QueryLog.deflected,
QueryLog.sources,
).where(QueryLog.question == question)
).all()
assert len(rows) == 1, f"expected one query_log row for {question!r}, got {len(rows)}"
row = rows[0]
return {
"top_score": float(row.top_score),
"fts_hits": row.fts_hits,
"chunk_hits": int(row.chunk_hits),
"deflected": bool(row.deflected),
"sources": str(row.sources),
}
def _recorded(row: dict[str, Any]) -> list[str]:
"""``query_log.sources`` as the ordered ``source/path`` list the server
wrote: the retrieval tier that reached the model (LOCKED A3)."""
return [part.strip() for part in row["sources"].split(",") if part.strip()]
def _arm(page: Page, question: str, *, expect_index: bool) -> dict[str, Any]:
"""Ask *question* once and collect everything that must not move between
the arms — the DOM, the wire, the durable row, and the DDL state the arm
ACTUALLY ran under."""
with SessionLocal() as db:
state = _index_state(db)
db.rollback()
if expect_index:
assert state is not None and state["valid"], (
f"the ON arm ran WITHOUT a valid {INDEX_NAME}: {state}"
)
else:
assert state is None, f"the OFF arm still had {INDEX_NAME}: {state}"
since = _submit(page, question)
_wait_settled(page)
done = _done(_frames(page, since))
brain = page.locator(".msg.brain").last
row = _row(question)
recorded = _recorded(row)
return {
"answer": brain.locator(".bubble").first.inner_text(),
"nearby": brain.locator(".related-docs .related-doc").all_inner_texts(),
"chips": brain.locator(".source-chip").count(),
"deflected_bubbles": page.locator(".msg.brain.is-deflected").count(),
"done": done,
"index_state": state,
"row_deflected": row["deflected"],
"top_score": float(row["top_score"]),
"fts_hits": row["fts_hits"],
"chunk_hits": row["chunk_hits"],
"recorded": recorded,
"suggested": recorded[:SUGGESTED_DOCS],
"nearby_recorded": recorded[SUGGESTED_DOCS:],
}
# ---------------------------------------------------------------------------
# 1 — the answered sentinel: the SAME document, index ON and index OFF
# ---------------------------------------------------------------------------
def test_answered_question_cites_the_same_document_indexed_or_not(
page: Page, app_url: str
) -> None:
"""The A7/A8 contract in the UI: an index is an ACCESS PATH, so the same
question must reach the same document whether or not the graph exists."""
page.set_default_timeout(SETTLE_TIMEOUT_MS)
login(page, app_url, next="/") # phase 79: the chat is login-gated
page.evaluate(SSE_HOOK)
# --- ON: the shipped state (0029 applied, index valid) ------------------
on = _arm(page, ANSWER_QUESTION, expect_index=True)
assert on["row_deflected"] is False
assert on["deflected_bubbles"] == 0
assert MOCK_ANSWER_MARKER in on["answer"]
# The measured margin, not just the direction.
assert on["top_score"] >= GATE_THRESHOLD
assert on["recorded"][0] == CITED_DOC, on["recorded"]
assert on["chunk_hits"] > 0
# Phase 119 (LOCKED A1): a zero-read grounded turn chips nothing — the
# index changes nothing about that either.
assert on["chips"] == 0
# The tier that reached the model, and the tier the page rendered.
assert CITED_DOC in on["suggested"]
assert on["nearby"], "the fixture KB must overflow into the rendered row"
assert on["nearby"] == on["nearby_recorded"]
assert on["done"]["sources"] == []
assert [f"{s['source']}/{s['path']}" for s in on["done"]["related"]] == (
on["nearby_recorded"]
)
# --- OFF: the real 0029 downgrade, restored in `finally` ----------------
with SessionLocal() as db:
db.execute(text("TRUNCATE query_log"))
db.commit()
_new_chat(page)
_set_index_present(False)
try:
off = _arm(page, ANSWER_QUESTION, expect_index=False)
finally:
_set_index_present(True)
# Same verdict, same document, same numbers: the DDL moved, the answer
# did not.
assert off["row_deflected"] is False
assert off["deflected_bubbles"] == 0
assert MOCK_ANSWER_MARKER in off["answer"]
assert off["recorded"] == on["recorded"], (
f"retrieval order moved with the index:\n ON={on['recorded']}\n"
f" OFF={off['recorded']}"
)
assert off["recorded"][0] == CITED_DOC
assert off["suggested"] == on["suggested"]
assert off["nearby"] == on["nearby"]
assert abs(off["top_score"] - on["top_score"]) <= SCORE_SLACK, (
f"top_score moved with the index: {on['top_score']} vs "
f"{off['top_score']}"
)
assert off["chunk_hits"] == on["chunk_hits"]
assert off["fts_hits"] == on["fts_hits"]
assert off["answer"] == on["answer"]
for key in ("deflected", "sources", "related", "suggestions"):
assert off["done"].get(key) == on["done"].get(key), (
f"the done frame's {key} moved with the index: "
f"{on['done'].get(key)!r} vs {off['done'].get(key)!r}"
)
# ---------------------------------------------------------------------------
# 2 — the index exists and is VALID after a real import
# ---------------------------------------------------------------------------
def test_index_exists_and_is_valid_after_import(kb: ImportSummary) -> None:
"""The import that just ran left a USABLE graph, not a silent INVALID one.
``0029`` post-validates its own build because a ``CONCURRENTLY`` build that
dies leaves the index INVALID — costing insert time on every sync and
serving nothing, with no error anywhere. The same check belongs after a
real import: chunks go into the graph on every sync, and an index that
drifted INVALID is invisible to every other surface the app has.
"""
with SessionLocal() as db:
state = _index_state(db)
counts = db.execute(
text("SELECT count(*) AS n, count(embedding) AS with_vec FROM chunks")
).one()
stored = db.scalar(select(func.count()).select_from(QueryLog))
db.rollback()
assert stored == 0, "this pin reads the catalog only — no turn has run"
assert state is not None, f"{INDEX_NAME} is MISSING after the import"
assert state["valid"] is True, (
f"{INDEX_NAME} exists but is INVALID after the import — every sync now "
f"pays for a graph that answers nothing: {state['indexdef']}"
)
definition = state["indexdef"]
assert INDEX_NAME in definition, definition
assert "hnsw" in definition, definition
assert "vector_cosine_ops" in definition, definition
assert "embedding" in definition, definition
# Not an empty shell over an empty table: the import put real embeddings
# in, and the graph carries pages for them.
assert int(counts.n) > 0, "the fixture import produced no chunks"
assert int(counts.with_vec) == int(counts.n), (
f"chunks without an embedding would be invisible to the tier: {counts}"
)
# Content chunks + one summary chunk per document (phase 30/118) — the
# importer's own two counts, so a row that never reached the graph shows
# up here instead of as an invisible hole.
assert int(counts.n) == kb.chunks + kb.summaries, (
f"the graph holds {counts.n} rows, the import reported "
f"{kb.chunks} content + {kb.summaries} summary chunks"
)
assert state["size_bytes"] > 0, f"{INDEX_NAME} carries no pages: {state}"
# ---------------------------------------------------------------------------
# 3 — the near-miss question still deflects (both arms)
# ---------------------------------------------------------------------------
def test_near_miss_question_still_deflects(page: Page, app_url: str) -> None:
"""A8 in the browser, index ON and OFF: weak cosine AND zero lexical hits
still deflects, and an index never manufactures a high ``top_score``."""
page.set_default_timeout(SETTLE_TIMEOUT_MS)
login(page, app_url, next="/")
page.evaluate(SSE_HOOK)
on = _arm(page, DEFLECT_QUESTION, expect_index=True)
assert on["row_deflected"] is True
assert on["deflected_bubbles"] == 1
# The gate's two inputs, in the units A8 defines them.
assert 0.0 < on["top_score"] < GATE_THRESHOLD
assert on["fts_hits"] == 0, "deflection is only reached with zero hits"
# The owner-visible shape of the deflection (phase 52/118).
expect(page.locator(".msg.brain.is-deflected .bubble")).to_contain_text(
DEFLECT_PHRASE, timeout=SETTLE_TIMEOUT_MS
)
chips = page.locator(".msg.brain.is-deflected .maybe-try .suggestion-chip")
expect(chips.first).to_be_visible(timeout=SETTLE_TIMEOUT_MS)
assert chips.count() >= 2, "a deflection must offer 2-3 alternatives"
assert all(text.strip() for text in chips.all_inner_texts())
assert on["done"]["deflected"] is True
assert on["done"]["sources"] == [], "a deflected answer cites nothing"
# OFF arm: the same verdict, for the same measured reason.
with SessionLocal() as db:
db.execute(text("TRUNCATE query_log"))
db.commit()
_new_chat(page)
_set_index_present(False)
try:
off = _arm(page, DEFLECT_QUESTION, expect_index=False)
finally:
_set_index_present(True)
assert off["row_deflected"] is True
assert off["deflected_bubbles"] == 1
assert 0.0 < off["top_score"] < GATE_THRESHOLD
assert off["fts_hits"] == 0
assert abs(off["top_score"] - on["top_score"]) <= SCORE_SLACK
assert off["recorded"] == on["recorded"]
assert MOCK_ANSWER_MARKER not in off["answer"]
assert off["done"]["deflected"] is True
# ---------------------------------------------------------------------------
# 4 — the ef_search knob on a real turn, and the clamp it cannot bypass
# ---------------------------------------------------------------------------
def _log_lines(log_path: Path, pattern: re.Pattern[str]) -> list[str]:
"""Every clamp line the app logged — read off the operator's surface."""
return [
line
for line in log_path.read_text(encoding="utf-8", errors="replace").splitlines()
if pattern.search(line)
]
def test_ef_search_setting_is_honoured(
page: Page, tmp_path_factory: pytest.TempPathFactory
) -> None:
"""``BOR_RETRIEVAL_EF_SEARCH`` reaches the turn, and a value BELOW the A7
candidate count is neither silent nor harmful."""
page.set_default_timeout(SETTLE_TIMEOUT_MS)
# --- 150 (>= the 100-candidate tier): a normal turn, no clamp line -----
ok_dir = tmp_path_factory.mktemp("bor_ef_ok")
with _running_app(
EF_OK_URL, EF_OK_PORT, _app_env(ok_dir, "150"), ok_dir / "ef150.log"
) as (ok_url, ok_log):
assert ok_log is not None
login(page, ok_url, next="/")
page.evaluate(SSE_HOOK)
on_ok = _arm(page, ANSWER_QUESTION, expect_index=True)
assert on_ok["row_deflected"] is False
assert MOCK_ANSWER_MARKER in on_ok["answer"]
assert on_ok["recorded"][0] == CITED_DOC
assert _log_lines(ok_log, CLAMP_LINE) == [], (
"ef_search=150 sits ABOVE the A7 candidate count and must not "
f"clamp: {_log_lines(ok_log, CLAMP_LINE)}"
)
# --- 50 (< the tier): the clamp fires and the turn is UNCHANGED --------
clamp_dir = tmp_path_factory.mktemp("bor_ef_clamp")
with _running_app(
EF_CLAMP_URL, EF_CLAMP_PORT, _app_env(clamp_dir, "50"), clamp_dir / "ef50.log"
) as (clamp_url, clamp_log):
assert clamp_log is not None
with SessionLocal() as db:
db.execute(text("TRUNCATE query_log"))
db.commit()
login(page, clamp_url, next="/")
page.evaluate(SSE_HOOK)
clamped = _arm(page, ANSWER_QUESTION, expect_index=True)
assert clamped["row_deflected"] is False
assert MOCK_ANSWER_MARKER in clamped["answer"]
assert clamped["recorded"][0] == CITED_DOC
assert clamped["recorded"] == on_ok["recorded"]
assert abs(clamped["top_score"] - on_ok["top_score"]) <= SCORE_SLACK
lines = _log_lines(clamp_log, CLAMP_LINE)
assert lines, (
"ef_search=50 is BELOW the A7 candidate count and MUST log the "
"clamp (AGENTS rule 10 — a knob that would degrade recall is never "
"silent). Tail of the captured log:\n"
f"{clamp_log.read_text(encoding='utf-8', errors='replace')[-3000:]}"
)
for line in lines:
match = CLAMP_LINE.search(line)
assert match is not None, line
assert int(match.group(1)) == 50, line
assert int(match.group(2)) == A7_CANDIDATES, line