Files
brain-of-reese/tests/integration/test_response_budget_api.py
ducoterra 2ac37455f0
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 14s
phase: 167_api_payload_diet
All gates green. Final report:

**Verified (phase 167 final pass — all 6 tasks already in `complete/`)**
- Live re-measure of the headline criterion on the **1,000-doc live-replica corpus** (restored `data/bor_live_kb_replica.dump.sql` into a scratch DB `bor_audit`, alembic 0011→0027, dropped after; the shared dev DB was never touched): `GET /api/docs/tree` default depth=1 = **8,474 identity → 1,128 B with `Content-Encoding: gzip`** (< 40 KB bar; ~2 MB defect fixed), repeat load **304 / 0 bytes**; depth=8 265,147→23,515; node drill 20,844→1,065; `/api/docs` 246,366→45,104; `/api/documents/content` 892,124→60,776 (`LOOK`, accepted-with-why). Reproduces `AUDIT.md` to within 1 byte.
- Transport contracts proven live: `POST /api/chat` **no** `Content-Encoding` + frames arriving live + budget line `… enc=identity duration_ms=7380 stream=1`; HTML still `no-cache` with the `?v=` rewrite (and gzipped); A20 headers on 200 **and** 304; ETag identical across identity/gzip; `depth=0|9|abc`→422; anonymous tree/node→403; unknown folder/source→404 `unknown node`.

**Gates (all 100% green)**
- `uv run pytest` → **3980 passed**; `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (`compression.py`, `http_cache.py`, `response_budget.py` 100%; `api/docs.py` 99%, one defensive dedupe line).
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors.
- Phase + regression E2E **in isolation** (`--junitxml`, exit codes): `test_kb_tree_lazy_load` 11, `test_kb_tree` 8, `test_kb_tree_nav` 4, `test_navbar_refresh` 7, `test_document_dates` 6, `test_git_source_dates` 4, `test_sync_summary_visibility` 3, `test_table_ux_polish` 6, `test_sources_midstream_bug` 6, `test_ls_tree_drilldown` 3, `test_smoke` 3 — **0 failures/errors** (the earlier `-q -q` run had simply suppressed pytest's summary line).

**Documentation** — README.md + AGENTS.md reviewed in full (1,436 / 80 lines):
- Fixed (pre-existing, phase-167-exposed): README *Caching & deploys* claimed "One Starlette middleware (`app/core/caching.py`) applies the rule" while the section now describes three transport layers → rewritten to name caching/compression/http_cache, the real outermost→innermost order, and why `bytes=` is uncompressed.
- AGENTS.md rule 5 (UI Structure Check) gained the phase-167 clause: a lazily-drilled level keeps the current level rendered, its in-flight `role="status"` / `role="alert"`+Retry row lives in the same bounded scroll panel, and stat cards read server `totals` (never a walk of a depth-limited tree).
- Cross-checked and consistent: every `BOR_*` in `.env.example` is in the README table, every `Settings` field is documented, all 15 README-cited API routes exist in the app, no stale "one fetch / drill client-side" claim remains anywhere.

**Notable decisions / deviations**
- `compression.py` adds a documented `_RebufferSmallResponses` seam beyond the D1 sketch (the caching layer's `BaseHTTPMiddleware` re-framing otherwise defeats Starlette's `minimum_size` exemption and inflates tiny responses) — integration-pinned, math still Starlette's.
- A node-route **404 = vanished folder → honest reset to top level** (per `00_phase.md` D4/D6), not task-06's "alert + Retry"; 500 shows alert+Retry with the level preserved — both pinned, nothing weakened.
- Retrieval battery not measurable (shared E2E DB holds no corpus), but `git diff --name-only -- app/rag` is empty, so retrieval is unchanged by construction. `AUDIT.md` left byte-identical (it lives under `todo/`, off-limits to this pass); no git index/history commands run.

**Next pending phase:** none — `167_api_payload_diet` is the only entry in `.agents/phases/todo/`; the harness moves it to `complete/` and commits.
2026-10-08 21:23:49 -04:00

239 lines
9.2 KiB
Python

"""Integration: the byte-budget log line on the REAL app stack (phase 167, task 05).
``tests/unit/test_response_budget.py`` pins the middleware on a bare ASGI
app; this file pins what only the real stack can prove —
* the line describes the REAL ``GET /api/docs/tree``: ``bytes=`` equals the
JSON the route produced, ``enc=identity`` for an identity client and
``enc=gzip`` through the actual ``SelectiveGZipMiddleware`` (the
reading that works because the middleware sits INSIDE the compressor),
* a conditional revalidation of that route logs ``status=304 bytes=0`` —
the phase-167 win visible in the log, and
* the level follows ``log_large_response_bytes`` (DEBUG under it, INFO at
or over it, and ``/api/health`` under the 64 KB default), while
non-``/api/`` paths (the shell page through the caching + compression
layers) write nothing.
Uses the real compose Postgres (``db`` fixture) and FastAPI's TestClient.
"""
from __future__ import annotations
import itertools
import logging
import re
from datetime import UTC, datetime
from typing import Any
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import text
import app.core.response_budget as budget_mod
from app.config import Settings
from app.main import app as fastapi_app
from app.models import Chunk, Document
_LINE_RE = re.compile(
r"^api method=(?P<method>[A-Z]+) path=(?P<path>\S+) status=(?P<status>\d+) "
r"bytes=(?P<bytes>\d+) enc=(?P<enc>\S+) duration_ms=(?P<ms>\d+)$"
)
_counter = itertools.count()
def _clean(db) -> None:
db.execute(text("TRUNCATE chunks, documents"))
db.commit()
def _seed_corpus(db, count: int = 50) -> None:
"""``count`` indexed docs — the tree response lands well above the
860-byte gzip floor (the corpus that motivates the phase is ~1 k docs)."""
_clean(db)
base = datetime.now(UTC)
for i in range(count):
doc = Document(
source="budget-corpus",
path=f"docs/sub-{i % 5}/file-{i:02d}.md",
full_path=f"/tmp/budget-corpus/docs/sub-{i % 5}/file-{i:02d}.md",
title=f"Doc {i:02d}",
content=f"# Doc {i:02d}\n\nBody of document {i:02d}.",
content_hash=f"{next(_counter):064d}",
indexed_at=base,
)
db.add(doc)
db.flush()
db.add(
Chunk(
document_id=doc.id,
position=0,
content=f"chunk of doc {i:02d}",
embedding=[0.01] * 768,
)
)
db.commit() # the endpoint session reads committed data only
def _budget_lines(
caplog: pytest.LogCaptureFixture, path: str | None = None
) -> list[tuple[int, str]]:
"""``(levelno, message)`` for the budget logger, optionally narrowed to
one request path (the ``admin_client`` fixture's own login is a real
``/api/*`` response too)."""
lines = [
(record.levelno, record.getMessage())
for record in caplog.records
if record.name == "app.response_budget"
]
if path is None:
return lines
return [(level, message) for level, message in lines if f" path={path} " in message]
def test_tree_response_logs_the_exact_line_for_the_route_bytes(
admin_client: TestClient, db, caplog: pytest.LogCaptureFixture
) -> None:
"""``bytes=`` is what the route produced — with an identity client that
is exactly the body the caller received."""
_seed_corpus(db)
try:
with caplog.at_level(logging.DEBUG, logger="app.response_budget"):
response = admin_client.get(
"/api/docs/tree?depth=8", headers={"Accept-Encoding": "identity"}
)
assert response.status_code == 200
lines = _budget_lines(caplog, "/api/docs/tree")
assert len(lines) == 1
match = _LINE_RE.match(lines[0][1])
assert match is not None, lines[0][1]
assert match.group("method") == "GET"
assert match.group("status") == "200"
assert int(match.group("bytes")) == len(response.content)
assert match.group("enc") == "identity"
assert int(match.group("ms")) >= 0
finally:
_clean(db)
def test_gzipped_tree_logs_enc_gzip_with_the_uncompressed_bytes(
admin_client: TestClient, db, caplog: pytest.LogCaptureFixture
) -> None:
"""The D5 reading, end to end: the client received gzip and the log says
``enc=gzip`` while ``bytes=`` stays the ROUTE's size — which is why the
middleware is wired inside the compression layer."""
_seed_corpus(db)
try:
with caplog.at_level(logging.DEBUG, logger="app.response_budget"):
identity = admin_client.get(
"/api/docs/tree?depth=8", headers={"Accept-Encoding": "identity"}
)
response = admin_client.get(
"/api/docs/tree?depth=8", headers={"Accept-Encoding": "gzip"}
)
assert response.status_code == 200
assert response.headers["content-encoding"] == "gzip"
assert response.content == identity.content
lines = _budget_lines(caplog, "/api/docs/tree")
assert len(lines) == 2
identity_line, gzip_line = (
_LINE_RE.match(lines[0][1]),
_LINE_RE.match(lines[1][1]),
)
assert identity_line is not None and identity_line.group("enc") == "identity"
assert gzip_line is not None and gzip_line.group("enc") == "gzip"
# Same route bytes in both lines…
assert identity_line.group("bytes") == gzip_line.group("bytes")
# …and that is the ROUTE's size, not the compressed transfer.
assert int(gzip_line.group("bytes")) == len(identity.content)
finally:
_clean(db)
def test_conditional_revalidation_logs_zero_bytes(
admin_client: TestClient, db, caplog: pytest.LogCaptureFixture
) -> None:
"""The phase-167 win is visible, not hidden: the 304 costs one line with
``bytes=0``."""
_seed_corpus(db)
try:
fresh = admin_client.get("/api/docs/tree?depth=1")
assert fresh.status_code == 200
etag = fresh.headers["etag"]
with caplog.at_level(logging.DEBUG, logger="app.response_budget"):
revalidated = admin_client.get(
"/api/docs/tree?depth=1", headers={"If-None-Match": etag}
)
assert revalidated.status_code == 304
lines = _budget_lines(caplog, "/api/docs/tree")
assert len(lines) == 1
match = _LINE_RE.match(lines[0][1])
assert match is not None
assert match.group("status") == "304"
assert match.group("bytes") == "0"
finally:
_clean(db)
def test_level_follows_the_configured_budget(
admin_client: TestClient,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""``/api/health`` sits under the 65536 default → DEBUG; the same
response with ``log_large_response_bytes=0`` is INFO (the loud mode). The
middleware reads the setting per response, so this is the operator's
runtime knob, not a boot-time one."""
with caplog.at_level(logging.DEBUG, logger="app.response_budget"):
assert admin_client.get("/api/health").status_code == 200
assert [level for level, _ in _budget_lines(caplog, "/api/health")] == [logging.DEBUG]
monkeypatch.setattr(
budget_mod,
"get_settings",
lambda: Settings(_env_file=None, log_large_response_bytes=0), # pyright: ignore[reportCallIssue]
)
caplog.clear()
with caplog.at_level(logging.DEBUG, logger="app.response_budget"):
assert admin_client.get("/api/health").status_code == 200
assert [level for level, _ in _budget_lines(caplog, "/api/health")] == [logging.INFO]
def test_html_page_writes_no_budget_line(
admin_client: TestClient, caplog: pytest.LogCaptureFixture
) -> None:
"""The shell page rides the caching rewrite AND the compression layer
and still writes nothing — the budget measures ``/api/*`` only."""
with caplog.at_level(logging.DEBUG, logger="app.response_budget"):
response = admin_client.get("/sources.html", headers={"Accept-Encoding": "gzip"})
assert response.status_code == 200
assert response.headers["content-encoding"] == "gzip"
assert not any(" path=/sources.html " in message for _, message in _budget_lines(caplog))
def test_anonymous_403_is_measured_like_any_other_response(
client: TestClient, caplog: pytest.LogCaptureFixture
) -> None:
"""The gate is a response too — an unauthenticated catalogue read logs
its status (and the byte line is what makes a chatty 403 visible)."""
with caplog.at_level(logging.DEBUG, logger="app.response_budget"):
assert client.get("/api/docs").status_code == 403
lines = _budget_lines(caplog, "/api/docs")
assert len(lines) == 1
match: Any = _LINE_RE.match(lines[0][1])
assert match is not None
assert match.group("status") == "403"
def test_the_module_level_app_carries_the_middleware() -> None:
"""Smoke: the wiring pin in ``tests/unit/test_response_budget.py`` covers
the ORDER on a fresh ``create_app()``; this proves the module-level ``app``
the whole suite (and the dev server) runs against has the layer too."""
names = [getattr(mw.cls, "__name__", "") for mw in fastapi_app.user_middleware]
assert "ResponseBudgetMiddleware" in names