Files
brain-of-reese/tests/integration/test_kb_overview_api.py
T
ducoterra 7fce6572d0
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 13s
feat: phases 77–80 — navbar view refresh, static background, API tokens, history suggestion chips
Single consolidated commit for four completed, validated phases (77, 78,
79, 80). The pipeline run left all work uncommitted because the harness
commits only with PHASE_COMMIT=1 while child executors are forbidden from
committing; the phases themselves all passed validation and moved to
.agents/phases/complete/.

Phase 77 — navbar view refresh
- router.js dispatches bor:view-refresh on re-show / active re-click /
  popstate (gated on wasMounted; first show and boot exempt)
- History / RAG / Sources / Tuning re-fetch on refresh (admin branch);
  Chat deliberately excluded (stream survival)
- History "Refresh" button (admin-only, in-flight disable + status line)
- New story suite tests/e2e/test_navbar_refresh.py (7 tests)

Phase 78 — static background
- Removed the animated glow layers; static 44px grid over the flat --bg
  canvas; default and reduced-motion renders byte-identical
- Updated background/theme E2E suites; removed bg-glow test pins

Phase 79 — API tokens
- api_tokens model + migration 0012; hash-only token service
- Admin tokens API + Tokens admin view; POST /api/token-auth;
  live-revoking require_user on chat / suggestions / document content
- Frontend token gate with localStorage cache; anonymous E2E suites
  migrated to token login
- New story suite tests/e2e/test_api_tokens.py (9 tests)

Phase 80 — history suggestion chips
- last_questions() endpoint with SEED fallback; startNewChat() refetch
- Seed-semantics docs (config.py, .env.example, README)
- Integration state matrix + E2E suite rewritten to the 4 chip states

Also included: phase-76 report artifacts and the repo restore-test-db
skill (previously untracked), scripts/* ruff fixes from phase 77.

Final gate state (phase 80 final pass, covers everything above):
- uv run pytest --cov=app → 1637 passed, 0 failed, app/ coverage 99%
- uv run ruff check . && uv run pyright → clean, 0 errors
- Per-phase story E2E suites green in isolation
2026-09-07 12:39:01 -04:00

262 lines
10 KiB
Python

"""Integration: KB overview (phase 31) — the ``<knowledge_base>`` section
of the chat system prompt.
Real Postgres (``podman compose up -d db``) seeded from
``tests/fixtures/docs/`` through the real importer; the chat path reuses
the deterministic capturing fake LLM from ``test_chat_api``
(token-overlap embeddings), so the stored row's journey —
``kb_overview`` row → per-turn PK lookup → ``<knowledge_base>`` section
of the **exact** captured system prompt (HIGH and LOW) — is verified
end-to-end without a network.
The byte-identity contract (phase 15 convention): with no row, the
captured system prompt equals the pre-phase construction
(``build_high_prompt`` / ``build_deflect_prompt`` with
``kb_overview=None``) — asserted with ``==``, not ``in``.
Requires: podman compose up -d db
"""
from __future__ import annotations
import asyncio
import logging
from collections.abc import Iterator
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select, text
from test_chat_api import FakeRagLLM, _stream_chat, _token_vec
from app.api import chat as chat_api
from app.main import app as fastapi_app
from app.models import Document, KbOverview
from app.rag.importer import import_sources
from app.rag.prompts import build_deflect_prompt, build_high_prompt
from app.rag.retriever import retrieve, weak_hit_titles
from tests.conftest import ADMIN_PASSWORD
FIXTURES = Path(__file__).resolve().parents[1] / "fixtures" / "docs"
QUESTION = "How is my Kubernetes cluster set up?"
OFF_TOPIC = "How do I bake sourdough bread?"
@pytest.fixture(autouse=True)
def _admin_signed_in(client: TestClient) -> None:
"""Phase 79 (task 03): ``POST /api/chat`` is user-gated — this module
reuses ``test_chat_api._stream_chat`` with the shared (module-local
to THIS file) ``client``, so it signs the admin in once per test
(the autouse in ``test_chat_api`` does not apply across the import).
"""
r = client.post("/api/login", json={"password": ADMIN_PASSWORD})
assert r.status_code == 204, f"admin login failed: {r.status_code} {r.text}"
#: A multi-line, multi-bullet outline: the section must carry it whole
#: (well within ``BOR_KB_OVERVIEW_MAX_CHARS``) and the per-turn log line
#: records its length.
OVERVIEW = (
"- Kubernetes cluster and node maintenance notes\n"
"- Backup schedules and restore runbooks\n"
"- Networking: static DNS and kafkabridge"
)
@pytest.fixture(autouse=True)
def clean_kb_overview(db) -> Iterator[None]:
"""The outline row + query log are global state: reset around every
test so no test inherits another test's row."""
db.execute(text("TRUNCATE kb_overview, query_log"))
db.commit()
yield
db.execute(text("TRUNCATE kb_overview, query_log"))
db.commit()
@pytest.fixture()
def seeded_kb(db) -> Iterator[FakeRagLLM]:
"""Fresh Postgres with the fixture docs imported (real pipeline)."""
db.execute(text("TRUNCATE chunks, documents, query_log, kb_overview"))
db.commit()
llm = FakeRagLLM()
summary = asyncio.run(import_sources([FIXTURES], llm, session=db))
assert summary.added == 13 # A9 formats (phase 47 added quadlet+j2); .hidden/ skipped
yield llm
db.execute(text("TRUNCATE chunks, documents, query_log, kb_overview"))
db.commit()
def _seed_overview(db) -> None:
db.add(KbOverview(id=1, content=OVERVIEW))
db.commit()
def _cited_docs(db, frames: list[dict]) -> list[Document]:
"""The documents the done event cited, in citation order — the same
list ``plan_turn`` passed to the prompt builder."""
docs = []
for s in frames[-1]["sources"]:
doc = db.scalar(select(Document).where(Document.path == s["path"]))
assert doc is not None, f"done source {s['path']!r} missing from the KB"
docs.append(doc)
return docs
def _turn_log_lines(caplog: pytest.LogCaptureFixture) -> list[str]:
"""The per-turn ``question=…`` log lines (PLAN §9) from this test."""
return [r.getMessage() for r in caplog.records if "question=" in r.getMessage()]
# ---------- no row → byte-identical to the pre-phase prompts ----------
def test_no_row_high_prompt_byte_identical_to_pre_phase(
client: TestClient, db, seeded_kb: FakeRagLLM, caplog: pytest.LogCaptureFixture
) -> None:
"""No ``kb_overview`` row: the captured HIGH system prompt EQUALS the
pre-phase construction exactly — the section is absent, not empty."""
caplog.set_level(logging.INFO, logger="app.chat")
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: seeded_kb
try:
_, _, frames = _stream_chat(client, QUESTION)
finally:
fastapi_app.dependency_overrides.clear()
assert frames[-1]["deflected"] is False
(system, user) = seeded_kb.seen_messages[0][0], seeded_kb.seen_messages[0][1]
assert user["content"] == QUESTION
expected = build_high_prompt(_cited_docs(db, frames), notes=[], kb_overview=None)
assert system["content"] == expected
assert "<knowledge_base>" not in system["content"]
lines = _turn_log_lines(caplog)
assert lines and "kb_chars=0" in lines[-1]
def test_no_row_low_prompt_byte_identical_to_pre_phase(
client: TestClient, db, seeded_kb: FakeRagLLM, caplog: pytest.LogCaptureFixture
) -> None:
"""No row, off-topic question: the captured LOW (deflection) prompt
EQUALS the pre-phase construction exactly."""
caplog.set_level(logging.INFO, logger="app.chat")
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: seeded_kb
try:
_, _, frames = _stream_chat(client, OFF_TOPIC)
finally:
fastapi_app.dependency_overrides.clear()
assert frames[-1]["deflected"] is True
(system, user) = seeded_kb.seen_messages[0][0], seeded_kb.seen_messages[0][1]
assert user["content"] == OFF_TOPIC
# Reconstruct the LOW prompt the way plan_turn does — the pre-phase
# construction (kb_overview=None), the same deterministic retrieval.
chunks = retrieve(db, OFF_TOPIC, _token_vec(OFF_TOPIC))
expected = build_deflect_prompt(
weak_hit_titles(chunks), notes=[], kb_overview=None
)
assert system["content"] == expected
assert "<knowledge_base>" not in system["content"]
assert "DEFLECT_MODE" in system["content"]
lines = _turn_log_lines(caplog)
assert lines and "kb_chars=0" in lines[-1]
# ---------- row present → section in BOTH prompts, exactly ----------
def test_row_high_prompt_carries_kb_section_exactly(
client: TestClient, db, seeded_kb: FakeRagLLM, caplog: pytest.LogCaptureFixture
) -> None:
"""Stored row: the captured HIGH prompt EQUALS the construction with
the outline — section present, ordered before ``<documents>``."""
_seed_overview(db)
caplog.set_level(logging.INFO, logger="app.chat")
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: seeded_kb
try:
_, _, frames = _stream_chat(client, QUESTION)
finally:
fastapi_app.dependency_overrides.clear()
assert frames[-1]["deflected"] is False
(system, _user) = seeded_kb.seen_messages[0][0], seeded_kb.seen_messages[0][1]
expected = build_high_prompt(
_cited_docs(db, frames), notes=[], kb_overview=OVERVIEW
)
assert system["content"] == expected
# Section shape + order: <relevance> → <knowledge_base> → <documents>.
prompt = system["content"]
assert (
prompt.index("<relevance>HIGH</relevance>")
< prompt.index("<knowledge_base>")
< prompt.index(OVERVIEW)
< prompt.index("</knowledge_base>")
< prompt.index("<documents>")
)
# The per-turn log line records the outline's length (PLAN §9).
lines = _turn_log_lines(caplog)
assert lines and f"kb_chars={len(OVERVIEW)}" in lines[-1]
def test_row_low_prompt_carries_kb_section_exactly(
client: TestClient, db, seeded_kb: FakeRagLLM, caplog: pytest.LogCaptureFixture
) -> None:
"""Stored row, off-topic question: the LOW prompt EQUALS the
construction with the outline — the section is in the deflection
prompt too (real alternatives, not hallucinated ones)."""
_seed_overview(db)
caplog.set_level(logging.INFO, logger="app.chat")
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: seeded_kb
try:
_, _, frames = _stream_chat(client, OFF_TOPIC)
finally:
fastapi_app.dependency_overrides.clear()
assert frames[-1]["deflected"] is True
(system, _user) = seeded_kb.seen_messages[0][0], seeded_kb.seen_messages[0][1]
chunks = retrieve(db, OFF_TOPIC, _token_vec(OFF_TOPIC))
expected = build_deflect_prompt(
weak_hit_titles(chunks), notes=[], kb_overview=OVERVIEW
)
assert system["content"] == expected
prompt = system["content"]
assert (
prompt.index("<relevance>LOW</relevance>")
< prompt.index("<knowledge_base>")
< prompt.index(OVERVIEW)
< prompt.index("</knowledge_base>")
< prompt.index("DEFLECT_MODE")
)
# Deflection still sees titles only — never document content.
assert "Talos Linux" not in prompt
lines = _turn_log_lines(caplog)
assert lines and f"kb_chars={len(OVERVIEW)}" in lines[-1]
def test_row_reread_every_turn_and_deleted_row_stops_it(
client: TestClient, db, seeded_kb: FakeRagLLM
) -> None:
"""The row is read per turn (not cached): it steers every turn until
it is deleted, and the following turn is section-free again."""
_seed_overview(db)
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: seeded_kb
try:
_stream_chat(client, QUESTION)
_stream_chat(client, QUESTION)
assert len(seeded_kb.seen_messages) == 2
for messages in seeded_kb.seen_messages:
assert "<knowledge_base>" in messages[0]["content"]
assert OVERVIEW in messages[0]["content"]
# Delete the row → the next turn's prompt drops the section.
db.execute(text("TRUNCATE kb_overview"))
db.commit()
_stream_chat(client, QUESTION)
assert len(seeded_kb.seen_messages) == 3
assert "<knowledge_base>" not in seeded_kb.seen_messages[-1][0]["content"]
finally:
fastapi_app.dependency_overrides.clear()