Files
brain-of-reese/tests/unit/test_response_budget.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

703 lines
25 KiB
Python

"""Unit tests: ``app.core.response_budget`` — the byte-budget log line
(phase 167, task 05, D5, LOCKED A5).
Pins the contract of the line, the level cut, and the pass-through rules
on a tiny ASGI app (the raw-ASGI interleaving harness of
``test_compression.py`` where ordering matters, ``TestClient`` where the
composed stack matters):
* the EXACT shape ``api method=… path=… status=… bytes=… enc=…
duration_ms=…`` for a large 200 at INFO and a small 200 at DEBUG;
* a 304 logs ``bytes=0`` (the revalidation win stays visible);
* a non-``/api/`` path and a non-HTTP scope log NOTHING;
* a stream logs ONE line with ``stream=1``, ``bytes`` = the sum of the
chunks, emitted when the stream COMPLETES (never mid-flight) — and the
frames are never buffered;
* ``enc`` reads the WIRE encoding (``gzip`` through the real compression
layer, ``identity`` for a client that does not accept it) while
``bytes`` stays the size the ROUTE produced;
* an app exception is logged (finally-style, bytes as counted) and
re-raised UNCHANGED;
* the config knob: ``0`` logs every ``/api/*`` response at INFO, a
negative value refuses startup naming the field, and the middleware sits
INSIDE ``SelectiveGZipMiddleware`` so ``bytes=`` is the route's size.
"""
from __future__ import annotations
import asyncio
import gzip
import json
import logging
import re
from typing import Any
import pytest
from fastapi.testclient import TestClient
from pydantic import ValidationError
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Route
from starlette.types import Message, Receive, Scope, Send
import app.core.response_budget as budget_mod
import app.main as main_mod
from app.config import Settings
from app.core.compression import SelectiveGZipMiddleware
from app.core.response_budget import (
API_PATH_PREFIX,
UNKNOWN_STATUS,
ResponseBudgetMiddleware,
)
#: One line, fully anchored — the shape README documents and the
#: ``grep ' :: api '`` workflow parses.
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+)$"
)
#: ~28 KB of tree-shaped JSON.
_BODY = json.dumps(
{
"sources": [
{
"name": "src",
"documents": i,
"children": [{"path": f"d/{i}/{n}.md"} for n in range(20)],
}
for i in range(200)
]
}
).encode()
_SMALL_BODY = b'{"ok":true}'
#: Four SSE-class frames, each comfortably above the gzip floor.
_FRAMES = [f"data: {'x' * 500}\n\n".encode() for _ in range(4)]
_LOGGER_NAME = "app.response_budget"
def _settings(**kwargs: Any) -> Settings:
"""Build Settings without reading a .env file (the test_config pattern)."""
kwargs.setdefault("_env_file", None)
return Settings(**kwargs) # pyright: ignore[reportCallIssue] (kwarg exists at runtime)
def _api_lines(caplog: pytest.LogCaptureFixture) -> list[tuple[int, str]]:
"""``(levelno, message)`` for this module's logger only, in order."""
return [
(record.levelno, record.getMessage())
for record in caplog.records
if record.name == _LOGGER_NAME
]
class _Counter(logging.Handler):
"""Counts emitted lines live, so an app can observe "has the line been
written yet?" from INSIDE the response — the ordering proof."""
def __init__(self) -> None:
super().__init__()
self.count = 0
def emit(self, record: logging.LogRecord) -> None:
self.count += 1
def _http_scope(path: str, method: str = "GET") -> Scope:
return {"type": "http", "method": method, "path": path, "headers": []}
async def _receive() -> Message:
return {"type": "http.disconnect"}
async def _send(message: Message) -> None:
return None
def _raw_json_app(body: bytes, *, status: int = 200, media_type: str = "application/json") -> Any:
"""The smallest ASGI app that answers one buffered body — the raw-ASGI
level is where the WIRE bytes (post-compression) are observable."""
async def app(scope: Scope, receive: Receive, send: Send) -> None:
await send(
{
"type": "http.response.start",
"status": status,
"headers": [(b"content-type", media_type.encode())],
}
)
await send({"type": "http.response.body", "body": body, "more_body": False})
return app
def _raw_harness(middleware_app: Any, *, path: str, accept: str) -> list[Message]:
"""Drive a middleware stack at the raw ASGI level; return the exact
messages that reached the client's ``send``."""
sent: list[Message] = []
async def recording_send(message: Message) -> None:
sent.append(message)
asyncio.run(
middleware_app(
{
"type": "http",
"method": "GET",
"path": path,
"headers": [(b"accept-encoding", accept.encode())],
},
_receive,
recording_send,
)
)
return sent
def _inner_app(
*,
body: bytes = _BODY,
status: int = 200,
path: str = "/api/docs/tree",
media_type: str = "application/json",
) -> Starlette:
async def respond(request: Any) -> Response:
return Response(body, status_code=status, media_type=media_type)
async def boom(request: Any) -> Response:
raise RuntimeError("kaboom")
return Starlette(
routes=[
Route(path, respond, methods=["GET", "POST"]),
Route("/api/boom", boom, methods=["GET"]),
Route("/sources.html", respond, methods=["GET"]),
]
)
def _client(app: Any, *, threshold: int = 1024) -> TestClient:
return TestClient(ResponseBudgetMiddleware(app, threshold=threshold))
# ---------------------------------------------------------------------------
# The line's exact shape, and the level the budget decides
# ---------------------------------------------------------------------------
def test_large_response_logs_one_info_line_in_the_exact_shape(
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
client = _client(_inner_app(), threshold=1024)
assert client.get("/api/docs/tree").status_code == 200
lines = _api_lines(caplog)
assert len(lines) == 1
level, message = lines[0]
assert level == logging.INFO
match = LINE_RE.match(message)
assert match is not None, message
assert match.group("method") == "GET"
assert match.group("path") == "/api/docs/tree"
assert match.group("status") == "200"
assert int(match.group("bytes")) == len(_BODY)
assert match.group("enc") == "identity"
assert int(match.group("ms")) >= 0
def test_small_response_logs_the_same_line_at_debug(
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
client = _client(_inner_app(body=_SMALL_BODY), threshold=1024)
assert client.get("/api/docs/tree").status_code == 200
lines = _api_lines(caplog)
assert len(lines) == 1
level, message = lines[0]
assert level == logging.DEBUG
assert f"bytes={len(_SMALL_BODY)}" in message
def test_budget_is_the_inclusive_cut(caplog: pytest.LogCaptureFixture) -> None:
"""``bytes >= budget`` is INFO, one byte less is DEBUG — the boundary the
README and ``.env.example`` state."""
async def exactly(request: Any) -> Response:
return Response(b"x" * 100)
async def one_less(request: Any) -> Response:
return Response(b"x" * 99)
app = Starlette(
routes=[
Route("/api/a", exactly, methods=["GET"]),
Route("/api/b", one_less, methods=["GET"]),
]
)
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
client = _client(app, threshold=100)
client.get("/api/a")
client.get("/api/b")
assert [level for level, _ in _api_lines(caplog)] == [logging.INFO, logging.DEBUG]
def test_threshold_defaults_to_the_settings_value(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
"""No ``threshold`` argument — the way ``create_app`` wires it — so the
live ``log_large_response_bytes`` decides per response."""
monkeypatch.setattr(
budget_mod, "get_settings", lambda: _settings(log_large_response_bytes=5)
)
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
TestClient(ResponseBudgetMiddleware(_inner_app(body=_SMALL_BODY))).get(
"/api/docs/tree"
)
assert [level for level, _ in _api_lines(caplog)] == [logging.INFO]
def test_zero_budget_logs_every_api_response_at_info(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
"""``BOR_LOG_LARGE_RESPONSE_BYTES=0`` is the loud mode: even an 11-byte
body is INFO (the operator's one-off investigation setting)."""
monkeypatch.setattr(
budget_mod, "get_settings", lambda: _settings(log_large_response_bytes=0)
)
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
TestClient(ResponseBudgetMiddleware(_inner_app(body=_SMALL_BODY))).get(
"/api/docs/tree"
)
# 11 bytes, budget 0 -> INFO (the same body is DEBUG at any positive
# budget: see test_small_response_logs_the_same_line_at_debug).
assert [level for level, _ in _api_lines(caplog)] == [logging.INFO]
# ---------------------------------------------------------------------------
# Statuses: a 304 logs 0 bytes, an error is counted like any other response
# ---------------------------------------------------------------------------
def test_304_logs_zero_bytes(caplog: pytest.LogCaptureFixture) -> None:
"""The revalidation win stays in the log — a bodiless 304 is a line with
``bytes=0``, not a missing line."""
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
client = _client(_inner_app(body=b"", status=304), threshold=1024)
assert client.get("/api/docs/tree").status_code == 304
lines = _api_lines(caplog)
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"
assert lines[0][0] == logging.DEBUG
def test_error_status_is_logged_like_any_other_response(
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
client = _client(_inner_app(body=b'{"detail":"admin only"}', status=403))
assert client.get("/api/docs/tree").status_code == 403
match = LINE_RE.match(_api_lines(caplog)[0][1])
assert match is not None and match.group("status") == "403"
# ---------------------------------------------------------------------------
# What is NOT measured
# ---------------------------------------------------------------------------
def test_non_api_path_logs_nothing(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
# budget 0: ANY /api line would show, so silence is the whole pin.
client = _client(_inner_app(), threshold=0)
assert client.get("/sources.html").status_code == 200
assert _api_lines(caplog) == []
def test_non_http_scopes_pass_through_silently(
caplog: pytest.LogCaptureFixture,
) -> None:
"""``lifespan`` / ``websocket`` scopes reach the app untouched and write
no line — there is no response to measure."""
seen: list[str] = []
async def app(scope: Scope, receive: Receive, send: Send) -> None:
seen.append(scope["type"])
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
for scope_type in ("lifespan", "websocket"):
asyncio.run(
ResponseBudgetMiddleware(app, threshold=0)(
{"type": scope_type, "path": "/api/x"}, _receive, _send
)
)
assert seen == ["lifespan", "websocket"]
assert _api_lines(caplog) == []
# ---------------------------------------------------------------------------
# The stream: one line, stream=1, counted total, emitted at completion
# ---------------------------------------------------------------------------
def test_stream_logs_once_at_completion_with_stream_flag_and_total_bytes(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The SSE contract of the line: nothing while frames are in flight, one
``stream=1`` line when the final chunk completes, ``bytes`` = the sum of
the frames (NOT one frame)."""
counter = _Counter()
logger = logging.getLogger(_LOGGER_NAME)
logger.addHandler(counter)
in_flight: list[int] = []
async def sse_app(scope: Scope, receive: Receive, send: Send) -> None:
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [(b"content-type", b"text/event-stream")],
}
)
for frame in _FRAMES:
await send(
{"type": "http.response.body", "body": frame, "more_body": True}
)
in_flight.append(counter.count) # after each frame is delivered
await send({"type": "http.response.body", "body": b"", "more_body": False})
in_flight.append(counter.count)
try:
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
asyncio.run(
ResponseBudgetMiddleware(sse_app, threshold=1)(
_http_scope("/api/chat", "POST"), _receive, _send
)
)
finally:
logger.removeHandler(counter)
# Every in-flight probe except the last saw zero lines; the completion
# is what emitted.
assert in_flight == [0] * len(_FRAMES) + [1]
lines = _api_lines(caplog)
assert len(lines) == 1
message = lines[0][1]
assert message.endswith(" stream=1")
match = LINE_RE.match(message.removesuffix(" stream=1"))
assert match is not None
assert match.group("method") == "POST"
assert match.group("path") == "/api/chat"
assert int(match.group("bytes")) == sum(len(frame) for frame in _FRAMES)
def test_stream_frames_are_never_buffered() -> None:
"""Zero-buffering proof (the interleaving idiom of
``test_compression.py``): every frame reaches the client's ``send``
before the app produces the next one — a ``BaseHTTPMiddleware`` here
would emit all ``app:*`` events first."""
events: list[str] = []
async def sse_app(scope: Scope, receive: Receive, send: Send) -> None:
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [(b"content-type", b"text/event-stream")],
}
)
for i, frame in enumerate(_FRAMES):
events.append(f"app:frame-{i}")
await send(
{"type": "http.response.body", "body": frame, "more_body": True}
)
await send({"type": "http.response.body", "body": b"", "more_body": False})
async def recording_send(message: Message) -> None:
events.append(f"client:{message['type']}")
asyncio.run(
ResponseBudgetMiddleware(sse_app, threshold=0)(
_http_scope("/api/chat", "POST"), _receive, recording_send
)
)
expected = ["client:http.response.start"]
for i in range(len(_FRAMES)):
expected += [f"app:frame-{i}", "client:http.response.body"]
expected += ["client:http.response.body"]
assert events == expected
# ---------------------------------------------------------------------------
# enc= reads the wire, bytes= stays the route's size
# ---------------------------------------------------------------------------
def _stack(*, threshold: int = 1024) -> TestClient:
"""response-budget INSIDE the selective-gzip layer — the order
``create_app`` builds."""
return TestClient(
SelectiveGZipMiddleware(
ResponseBudgetMiddleware(_inner_app(), threshold=threshold),
minimum_size=860,
)
)
def test_enc_gzip_is_reported_through_the_compression_layer(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The two halves of the line, measured against the wire: the client
receives a gzipped body, the log says ``enc=gzip`` and still counts the
ROUTE's uncompressed bytes (the position inside the compressor is what
buys that)."""
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
sent = _raw_harness(
SelectiveGZipMiddleware(
ResponseBudgetMiddleware(_raw_json_app(_BODY), threshold=1024),
minimum_size=860,
),
path="/api/docs/tree",
accept="gzip",
)
wire = b"".join(m.get("body", b"") for m in sent if m["type"] == "http.response.body")
assert gzip.decompress(wire) == _BODY # the transfer really was gzip
match = LINE_RE.match(_api_lines(caplog)[-1][1])
assert match is not None
assert match.group("enc") == "gzip"
assert int(match.group("bytes")) == len(_BODY) > len(wire)
def test_enc_identity_when_the_client_does_not_accept_gzip(
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
response = _stack().get(
"/api/docs/tree", headers={"Accept-Encoding": "identity"}
)
assert "content-encoding" not in response.headers
match = LINE_RE.match(_api_lines(caplog)[-1][1])
assert match is not None and match.group("enc") == "identity"
assert int(match.group("bytes")) == len(_BODY)
def test_enc_reports_a_pre_encoded_response(caplog: pytest.LogCaptureFixture) -> None:
"""A route that encodes its own body (the fronting-proxy case) is
reported as such — the header on the wire is the answer."""
async def encoded(request: Any) -> Response:
return Response(
gzip.compress(_BODY),
media_type="application/json",
headers={"Content-Encoding": "gzip"},
)
app = Starlette(routes=[Route("/api/encoded", encoded, methods=["GET"])])
with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME):
TestClient(ResponseBudgetMiddleware(app, threshold=1024)).get("/api/encoded")
match = LINE_RE.match(_api_lines(caplog)[-1][1])
assert match is not None and match.group("enc") == "gzip"
# ---------------------------------------------------------------------------
# The app raising: logged, and re-raised unchanged
# ---------------------------------------------------------------------------
def test_app_exception_logs_and_reraises_unchanged(
caplog: pytest.LogCaptureFixture,
) -> None:
"""An app that dies before it can answer still writes one line (nothing
was counted, status 500 — what the outer error layer will answer) and the
exception propagates UNCHANGED."""
async def dying_app(scope: Scope, receive: Receive, send: Send) -> None:
raise RuntimeError("kaboom")
with (
caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME),
pytest.raises(RuntimeError, match="kaboom"),
):
asyncio.run(
ResponseBudgetMiddleware(dying_app, threshold=0)(
_http_scope("/api/docs/tree"), _receive, _send
)
)
lines = _api_lines(caplog)
assert len(lines) == 1
match = LINE_RE.match(lines[0][1])
assert match is not None
assert match.group("status") == str(UNKNOWN_STATUS)
assert match.group("bytes") == "0"
def test_a_500_produced_by_the_framework_is_counted_like_a_response(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Through a real app the unhandled exception becomes a 500 WITH a body
(Starlette's ``Internal Server Error`` text): the line then reports that
status and those bytes — one line either way, never zero."""
with (
caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME),
pytest.raises(RuntimeError, match="kaboom"),
):
_client(_inner_app(), threshold=0).get("/api/boom")
lines = _api_lines(caplog)
assert len(lines) == 1
match = LINE_RE.match(lines[0][1])
assert match is not None
assert match.group("status") == "500"
assert int(match.group("bytes")) == len(b"Internal Server Error")
def test_exception_mid_stream_logs_the_bytes_counted_so_far(
caplog: pytest.LogCaptureFixture,
) -> None:
"""A stream that dies after two frames still writes ONE line with the
bytes it actually got out (finally-style) — and the exception is not
swallowed."""
async def dying_stream_app(scope: Scope, receive: Receive, send: Send) -> None:
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [(b"content-type", b"text/event-stream")],
}
)
await send({"type": "http.response.body", "body": _FRAMES[0], "more_body": True})
await send({"type": "http.response.body", "body": _FRAMES[1], "more_body": True})
raise RuntimeError("stream died")
with (
caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME),
pytest.raises(RuntimeError, match="stream died"),
):
asyncio.run(
ResponseBudgetMiddleware(dying_stream_app, threshold=0)(
_http_scope("/api/chat", "POST"), _receive, _send
)
)
lines = _api_lines(caplog)
assert len(lines) == 1
message = lines[0][1]
assert message.endswith(" stream=1")
assert f"bytes={len(_FRAMES[0]) + len(_FRAMES[1])}" in message
def test_a_cancelled_stream_still_logs_one_line_with_what_got_out(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The client-walked-away path (the live audit hit it: an SSE turn cut
mid-answer): the app unwinds with ``CancelledError`` and no completing
body, the ``finally`` writes ONE line with the bytes that actually got
out, and the cancellation propagates (the middleware never swallows it)."""
async def cancelled_stream_app(scope: Scope, receive: Receive, send: Send) -> None:
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [(b"content-type", b"text/event-stream")],
}
)
await send({"type": "http.response.body", "body": _FRAMES[0], "more_body": True})
raise asyncio.CancelledError # what the server does on a client disconnect
with (
caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME),
pytest.raises(asyncio.CancelledError),
):
asyncio.run(
ResponseBudgetMiddleware(cancelled_stream_app, threshold=0)(
_http_scope("/api/chat", "POST"), _receive, _send
)
)
lines = _api_lines(caplog)
assert len(lines) == 1
assert lines[0][1].endswith(" stream=1")
assert f"bytes={len(_FRAMES[0])}" in lines[0][1]
# ---------------------------------------------------------------------------
# Config + wiring
# ---------------------------------------------------------------------------
def test_log_large_response_bytes_defaults_to_64_kb() -> None:
assert _settings().log_large_response_bytes == 65536
def test_log_large_response_bytes_env_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("BOR_LOG_LARGE_RESPONSE_BYTES", "4096")
assert _settings().log_large_response_bytes == 4096
monkeypatch.setenv("BOR_LOG_LARGE_RESPONSE_BYTES", "0") # loud mode is legal
assert _settings().log_large_response_bytes == 0
def test_negative_budget_refuses_startup_naming_the_field() -> None:
with pytest.raises(ValidationError, match="log_large_response_bytes must be >= 0"):
_settings(log_large_response_bytes=-1)
def test_middleware_sits_inside_gzip_outside_caching(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The documented stack (README + ``app/main.py``): SecurityHeaders
outermost → gzip → response-budget → caching → session. That position is
what makes ``bytes=`` the ROUTE's size while ``enc=`` still names the
wire encoding."""
monkeypatch.setattr(main_mod.settings, "compress_min_bytes", 860)
names = [
getattr(mw.cls, "__name__", "") for mw in main_mod.create_app().user_middleware
]
assert names == [
"SecurityHeadersMiddleware",
"SelectiveGZipMiddleware",
"ResponseBudgetMiddleware",
"CachingMiddleware",
"SessionMiddleware",
]
def test_budget_is_wired_even_with_compression_off(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``BOR_COMPRESS_MIN_BYTES=0`` removes gzip only — the budget line is
not optional (LOCKED A5: measure everything, reject nothing), so the
measurement survives the compression kill switch."""
monkeypatch.setattr(main_mod.settings, "compress_min_bytes", 0)
names = [
getattr(mw.cls, "__name__", "") for mw in main_mod.create_app().user_middleware
]
assert "SelectiveGZipMiddleware" not in names
assert "ResponseBudgetMiddleware" in names
def test_api_path_prefix_constant() -> None:
assert API_PATH_PREFIX == "/api/"