Files
brain-of-reese/tests/integration/test_import_extensions_env.py
T
ducoterra 9820c361b0
Build and Push Containers / build-and-push-app (push) Successful in 2m2s
Build and Push Containers / build-and-push-db (push) Successful in 14s
phase: 118_summary_seed_context
**Phase 118 final verification pass — complete.** All criteria verified; 4 pre-existing defects found and fixed.

- **Verified:** summary-seed wiring (`select_suggested` top-5 no-floor → summary blocks, no full text in HIGH prompt), all-doc markdown summaries + NULL backfill (`summary_backfilled`, no `sources_meta` bump), `read` adds full text with `read_docs`-only dedupe, `done.sources` = suggested+read / durable record = suggested+related+read + `suggested=N` log line (seen live in E2E), byte-locked PERSONA/LOW/TOOLS_SECTION, battery gate PASS recorded in `TOOL_CALLING_TESTING.md` §10 (turbo 2026-09-16: 1/2/4 GREEN, cond-3 reported 9/10 per A7, contract 21/21, caps 0).
- **Defects fixed (all pre-existing, none phase-118):** ① `ChatMessage` schema missing the phase-113 `related` key → `extra="forbid"` 422'd every done-time auto-save of grounded turns with a related tier, leaving `message_count=1` (root cause of `test_share_chat` 3F; browser-level instrumentation proved the PUT 422) — added the field + unit/integration pins; ② `test_theme_semantic_completion` pins stale vs phase-117 debox (border/chip removed) — re-targeted to assert border/chip *absence*; ③ `test_header_consistency` `<26`px pin red on 26.125px native date-input line — bound relaxed to `<34` (wrap-detection intent kept); ④ `test_navbar_refresh` bor.chat.v1 key set updated for `related`.
- **Test/lint/coverage:** `uv run pytest --cov=app --cov-report=term-missing` → **2506 passed, app/ 99%** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors.
- **E2E:** new story suite in isolation → **2 passed**; full 103-suite matrix sweep (each isolated) → **all 103 green** after the fixes; `test_share_chat` 4 passed, `test_theme_semantic_completion` 8 passed, `test_header_consistency` 3 passed, `test_navbar_refresh` 7 passed.
- **Deviations:** none from LOCKED decisions. Note: orphaned diagnostic uvicorn processes briefly made E2E sessions exercise stale code — killed and re-verified; a sweep-regenerated tracked screenshot was restored. No commits made (harness commits).
- **Completion criteria:** all 7 ✅ (commit/phase-move is the harness's step).
- **Next pending phase:** none — `todo/` holds only this phase's overview pending the harness move.
2026-09-16 06:57:49 -04:00

198 lines
7.6 KiB
Python

"""Integration test: phase 56 — ``BOR_IMPORT_EXTENSIONS`` is user-extensible.
Proves a NOVEL (non-A9) extension flows through the import machinery
(config → walk → delta → chunk → mock ``SUMMARY_MODE`` digest) against
the story-dedicated fixture directory ``tests/fixtures/extension_kb/``
(the shared ``docs`` / ``summary_kb`` fixtures stay pinned by their own
suites). The LLM is the deterministic mock server (``tests/e2e/mock_llm.py``)
on a scratch port — the integration analogue of the e2e ``mock_llm``
fixture — so the summary is the byte-stable ``SUMMARY_MODE`` digest and
the vectors are genuine token-overlap embeddings. Runs against the local
compose Postgres (the ``db`` fixture from ``tests/conftest.py``); only
the distinctive ``extension_kb`` source rows are created and deleted, so
the rest of the shared KB is untouched.
Runs (DB must be up: ``podman compose up -d db``):
uv run pytest tests/integration/test_import_extensions_env.py -v
"""
from __future__ import annotations
import asyncio
import os
import socket
import subprocess
import sys
import time
from collections.abc import Iterator
from pathlib import Path
from typing import Any
import httpx
import pytest
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.config import Settings
from app.models import Document
from app.rag.importer import import_sources
from app.rag.llm import LLMClient
REPO = Path(__file__).resolve().parents[2]
FIXTURES = REPO / "tests" / "fixtures" / "extension_kb"
SOURCE = FIXTURES.name # "extension_kb" — distinctive, never asserted by count
SH_REL = "homelab/scripts/uptime.sh"
MD_REL = "homelab/notes/note.md"
SENTINEL = "UPTIME-PROBE-SENTINEL-9c2f"
#: The mock's SUMMARY_MODE tokenizes the document (``[a-z0-9]+``) — the
#: hyphenated sentinel lands in the digest in its tokenized form.
SENTINEL_TOKENS = "uptime probe sentinel 9c2f"
def _wait_http(url: str, timeout: float = 30.0) -> None:
deadline = time.monotonic() + timeout
last_err = "unknown"
while time.monotonic() < deadline:
try:
httpx.get(url, timeout=2.0)
return
except Exception as e: # noqa: BLE001 — retry until deadline
last_err = str(e)
time.sleep(0.2)
raise RuntimeError(f"mock LLM at {url} did not come up: {last_err}")
@pytest.fixture(scope="module")
def mock_llm_port() -> Iterator[int]:
"""The deterministic mock LLM (``tests/e2e/mock_llm.py``) on a free
scratch port — same server as the e2e ``mock_llm`` fixture, but
private to this file (integration tests otherwise run network-free)."""
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
env = dict(os.environ)
env.pop("DEBUGPY", None)
proc = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "tests.e2e.mock_llm:app",
"--host", "127.0.0.1", "--port", str(port), "--log-level", "warning"],
cwd=REPO,
env=env,
)
try:
_wait_http(f"http://127.0.0.1:{port}/v1/models")
yield port
finally:
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
def _settings(mock_port: int, extensions: str) -> Settings:
kwargs: dict[str, Any] = {
"_env_file": None,
"llm_base_url": f"http://127.0.0.1:{mock_port}/v1",
"import_extensions": extensions,
}
return Settings(**kwargs) # pyright: ignore[reportCallIssue]
def _cleanup_source(db: Session, source: str) -> None:
for doc in db.scalars(select(Document).where(Document.source == source)).all():
db.delete(doc)
db.commit()
def test_novel_extension_imports_end_to_end(mock_llm_port: int, db: Session) -> None:
"""``md,sh`` (a novel extension) imports the ``.sh`` file end to
end: row + plain-text chunks + the mock ``SUMMARY_MODE`` digest,
with the markdown control doc imported as well."""
settings = _settings(mock_llm_port, "md,sh")
assert settings.import_extension_set == {".md", ".sh"}
summary = asyncio.run(import_sources([FIXTURES], LLMClient(settings), session=db))
try:
assert (
summary.files, summary.added, summary.unchanged, summary.updated, summary.errors
) == (2, 2, 0, 0, 0)
assert summary.formats == {"sh": 1, "md": 1}
# Phase 118 (A2): EVERY doc gets a lite summary — the novel
# ``.sh`` file AND the markdown control.
assert (summary.summaries, summary.summary_errors) == (2, 0)
sh = db.scalar(
select(Document).where(
Document.source == SOURCE, Document.path == SH_REL
)
)
assert sh is not None, "the novel .sh extension was not imported"
# Non-markdown: the title comes from the file stem (a ``#`` line
# is a comment, not a heading).
assert sh.title == "uptime"
# Plain-text chunking: the content (incl. the sentinel) is
# chunked and every content chunk is embedded at the 768-dim
# contract.
content = [c for c in sh.chunks if not c.is_summary]
assert content, "the .sh file has no content chunks"
assert all(
c.embedding is not None and len(c.embedding) == 768 for c in content
)
assert any(SENTINEL in c.content for c in content)
# Mock SUMMARY_MODE digest: byte-stable, the tokenized sentinel
# inside it, plus the code-appended pointer line.
assert sh.summary is not None
assert sh.summary.startswith("This document covers")
assert SENTINEL_TOKENS in sh.summary
assert f"Source: {SOURCE}/{SH_REL}" in sh.summary
schunks = [c for c in sh.chunks if c.is_summary]
assert len(schunks) == 1 and schunks[0].position == -1
assert schunks[0].embedding is not None
# The markdown control doc imported too — AND got the mock
# ``SUMMARY_MODE`` digest (phase 118, A2: markdown summarized).
note = db.scalar(
select(Document).where(
Document.source == SOURCE, Document.path == MD_REL
)
)
assert note is not None
assert note.summary is not None
assert note.summary.startswith("This document covers extension fixture note")
assert f"Source: {SOURCE}/{MD_REL}" in note.summary
note_schunks = [c for c in note.chunks if c.is_summary]
assert len(note_schunks) == 1 and note_schunks[0].position == -1
assert note_schunks[0].embedding is not None
assert [c for c in note.chunks if not c.is_summary]
finally:
_cleanup_source(db, SOURCE)
def test_narrowing_to_md_still_excludes_the_novel_extension(
mock_llm_port: int, db: Session
) -> None:
"""``md`` (the A9-era narrowing, preserved as a special case): the
``.sh`` file is out of scope, only the control note imports."""
settings = _settings(mock_llm_port, "md")
assert settings.import_extension_set == {".md"}
summary = asyncio.run(import_sources([FIXTURES], LLMClient(settings), session=db))
try:
assert (
summary.files, summary.added, summary.unchanged, summary.updated, summary.errors
) == (1, 1, 0, 0, 0)
assert summary.formats == {"md": 1}
# Phase 118 (A2): the md-only run still summarizes the note.
assert summary.summaries == 1
assert db.scalar(
select(Document).where(
Document.source == SOURCE, Document.path == SH_REL
)
) is None
assert db.scalar(
select(Document).where(
Document.source == SOURCE, Document.path == MD_REL
)
) is not None
finally:
_cleanup_source(db, SOURCE)