phase: 167_api_payload_diet
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 14s

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.
This commit is contained in:
2026-10-08 21:23:49 -04:00
parent ab9391d019
commit 2ac37455f0
73 changed files with 7313 additions and 228 deletions
+33
View File
@@ -335,6 +335,39 @@ def test_chat_streams_deltas_then_done_with_sources(client, db, seeded_kb: FakeR
assert "HONESTY GATE" in system["content"]
def test_chat_turn_logs_one_budget_line_with_the_stream_flag(
client, db, seeded_kb: FakeRagLLM, caplog: pytest.LogCaptureFixture
) -> None:
"""Phase 167 (task 05, D5): the SSE turn is measured like every other
``/api/*`` response but with `` stream=1`` — ONE line, written when the
stream ends, ``bytes=`` the whole streamed total (the frames, not one
frame) and ``duration_ms`` the stream's life. The stream itself stays
uncompressed (phase 167 A1, pinned above), so ``enc=identity`` here
documents the wire, and the chat turn never bloats the log with one
line per frame."""
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: seeded_kb
try:
with caplog.at_level(logging.DEBUG, logger="app.response_budget"):
_, _, frames = _stream_chat(client, QUESTION)
finally:
fastapi_app.dependency_overrides.clear()
assert frames[-1]["type"] == "done" # the stream ran to completion
lines = [
record.getMessage()
for record in caplog.records
if record.name == "app.response_budget" and " path=/api/chat " in record.getMessage()
]
assert len(lines) == 1, lines
line = lines[0]
assert line.endswith(" stream=1")
match = re.search(r"status=(\d+) bytes=(\d+) enc=(\S+)", line)
assert match is not None
assert match.group(1) == "200"
assert int(match.group(2)) > len(b"data: ") # a real total, not a stub
assert match.group(3) == "identity"
def test_chat_streams_thinking_before_deltas(client, db, seeded_kb: FakeRagLLM) -> None:
"""Phase 17: ``thinking`` frames precede every ``delta`` frame and
reassemble to the model's reasoning; the ``done`` contract is
+675 -7
View File
@@ -15,12 +15,20 @@ qualifying row, ABSENT otherwise — never ``null``), plus the phase-163
for a stamped strip, ABSENT for a pre-phase row and for every
non-qualifying document).
Also the phase-167 (task 03) per-level drill route
``GET /api/docs/tree/node`` (the node shape, the 403/404 matrix, the
pin that its children equal the same level of the unlimited
``/api/docs/tree?depth=8`` response) and the ETag revalidation of the
three catalogue reads (200 + validator, conditional 304 with no body,
the admin-PATCH bust — the LOCKED A4 proof — coexisting with gzip).
Uses the real compose Postgres (``db`` fixture) and FastAPI's TestClient.
"""
from __future__ import annotations
import asyncio
import base64
import gzip
import hashlib
import inspect
import itertools
@@ -38,8 +46,9 @@ import app.api.docs as docs_api
import app.rag.importer as rag_importer
from app.config import Settings
from app.core import tokens as token_service
from app.core.security_headers import CSP
from app.main import app as fastapi_app
from app.models import Chunk, Document, FolderSummary, GitSource
from app.models import Chunk, Document, FolderSummary, GitSource, SourcesMeta
from app.rag import git_sources as rag_git_sources
from app.rag.folder_summaries import missing_folder_summaries
from app.rag.importer import import_sources
@@ -218,10 +227,19 @@ def test_docs_tree_populated_shape_order_counts_summaries(admin_client, db) -> N
)
db.commit()
r = admin_client.get("/api/docs/tree")
# Phase 167 (task 02): this shape pin walks the FULL tree, so it
# asks for the whole build explicitly — the endpoint's default is
# the view's first paint (depth=1), depth=8 is the entire tree.
r = admin_client.get("/api/docs/tree?depth=8")
assert r.status_code == 200
body = r.json()
assert set(body) == {"sources"}
# ``totals`` joins the top-level keys (phase 167, additive).
assert set(body) == {"sources", "totals"}
assert body["totals"] == {
"documents": 6,
"chunks": 11,
"last_indexed": base.isoformat(),
}
sources = body["sources"]
assert [s["name"] for s in sources] == ["Homelab", "Deployments"]
@@ -310,7 +328,12 @@ def test_docs_tree_empty_registry_and_catalog(
_truncate_tree_tables(db)
r = admin_client.get("/api/docs/tree")
assert r.status_code == 200
assert r.json() == {"sources": []}
# Phase 167 (task 02): ``totals`` rides even the empty tree —
# 0 documents, 0 chunks, no last-indexed stamp.
assert r.json() == {
"sources": [],
"totals": {"documents": 0, "chunks": 0, "last_indexed": None},
}
def test_docs_tree_indexed_only_source_after_registered(admin_client, db) -> None:
@@ -367,7 +390,10 @@ def test_docs_tree_summary_pending_on_source_and_folder_nodes(admin_client, db)
)
db.commit()
r = admin_client.get("/api/docs/tree")
# Phase 167 (task 02): the nested ``k8s/helm`` node needs the full
# build — depth=8 keeps this pin walking every level (the default
# is the view's first paint only).
r = admin_client.get("/api/docs/tree?depth=8")
assert r.status_code == 200
(homelab,) = r.json()["sources"]
# Phase 106 (task 05): ``updated_at`` joins the source node keys.
@@ -458,7 +484,10 @@ def test_docs_tree_pending_set_equals_missing_folder_summaries(admin_client, db)
)
db.commit()
r = admin_client.get("/api/docs/tree")
# Phase 167 (task 02): the cross-check walks EVERY node, so it
# requests the full tree (the default depth ships only the view's
# first level — truncated nodes still carry their own markers).
r = admin_client.get("/api/docs/tree?depth=8")
assert r.status_code == 200
pending = _tree_pending_keys(r.json()["sources"])
# THE cross-check: the marker set IS the gap-fill's candidate set.
@@ -828,8 +857,14 @@ def test_docs_tree_stat_walk_equivalence_with_flat_list(admin_client, db) -> Non
_seed_doc(db, "Homelab", "solo.md", "Solo", 0, base)
db.commit()
tree = admin_client.get("/api/docs/tree").json()
# Phase 167 (task 02): the walk needs EVERY file node, so it asks
# for the full tree; the server ``totals`` must equal the same walk
# over the flat list (the stat cards cannot drift either way).
tree = admin_client.get("/api/docs/tree?depth=8").json()
flat = admin_client.get("/api/docs").json()
assert tree["totals"]["documents"] == len(flat["documents"])
assert tree["totals"]["chunks"] == sum(d["chunks"] for d in flat["documents"])
assert tree["totals"]["last_indexed"] == max(d["indexed_at"] for d in flat["documents"])
files = _tree_file_nodes(tree["sources"])
assert len(files) == len(flat["documents"]) # document count
@@ -1768,3 +1803,636 @@ def test_tree_with_no_image_rows_is_byte_identical(admin_client: TestClient, db)
}
finally:
_truncate_tree_tables(db)
# ---------------------------------------------------------------------------
# Phase 167 (task 01): transport gzip on the real app — the /api/docs/tree
# payload (the TODO L5 2 MB defect) compresses, small responses stay
# identity, and the A20 security-header + A19 page contracts survive the
# new layer sitting between SecurityHeaders (outermost) and Caching.
# ---------------------------------------------------------------------------
def _seed_gzip_corpus(db, count: int = 50) -> None:
""">= 50 indexed docs — the tree response lands well above the 860-byte
floor (the production corpus that motivates the phase is ~1 k docs)."""
_truncate_tree_tables(db)
base = datetime.now(UTC)
for i in range(count):
_seed_doc(db, "gzip-corpus", f"docs/sub-{i % 5}/file-{i:02d}.md", f"Doc {i:02d}", 1, base)
db.commit() # the endpoint session reads committed data only
def test_docs_tree_ships_gzip_and_decompresses_to_the_documented_json(
admin_client: TestClient, db
) -> None:
"""Accept-Encoding: gzip → Content-Encoding: gzip + Vary, and the raw
wire bytes decompress to EXACTLY the identity bytes (the documented
tree JSON, 50 file nodes) — ~an order of magnitude smaller."""
_seed_gzip_corpus(db)
# Phase 167 (task 02): the 50-file corpus must ride the wire for
# this byte pin, so the full tree is requested explicitly (the
# endpoint default ships one level, which would fall under the
# compression floor).
try:
identity = admin_client.get(
"/api/docs/tree?depth=8", headers={"Accept-Encoding": "identity"}
)
assert identity.status_code == 200
assert "content-encoding" not in identity.headers
assert len(identity.content) >= 860 # the seed is above the floor
r = admin_client.get(
"/api/docs/tree?depth=8", headers={"Accept-Encoding": "gzip"}
)
assert r.status_code == 200
assert r.headers["content-encoding"] == "gzip"
assert "accept-encoding" in r.headers.get("vary", "").lower()
# httpx decodes transparently: same documented JSON.
sources = r.json()["sources"]
assert len(_tree_file_nodes_all(sources)) == 50
# The RAW stream is a real gzip of the identity body.
with admin_client.stream(
"GET", "/api/docs/tree?depth=8", headers={"Accept-Encoding": "gzip"}
) as streamed:
raw = b"".join(streamed.iter_raw())
assert gzip.decompress(raw) == identity.content
assert len(raw) < len(identity.content) // 2 # repetitive JSON: >> 2x
finally:
_truncate_tree_tables(db)
def test_gzipped_response_still_carries_the_a20_headers(
admin_client: TestClient, db
) -> None:
"""The outer-layer invariant (A20): gzip sits INSIDE
SecurityHeadersMiddleware, so a gzipped response still carries the
CSP, nosniff and the no-framing headers."""
_seed_gzip_corpus(db, 60)
try:
# depth=8 (phase 167, task 02): the whole tree stays above the
# 860-byte floor — the point here is a GZIPPED response.
r = admin_client.get("/api/docs/tree?depth=8", headers={"Accept-Encoding": "gzip"})
assert r.headers["content-encoding"] == "gzip"
assert r.headers["content-security-policy"] == CSP
assert r.headers["x-content-type-options"] == "nosniff"
assert r.headers["x-frame-options"] == "DENY"
finally:
_truncate_tree_tables(db)
def test_small_api_response_stays_identity(client: TestClient) -> None:
"""A tiny /api/* response (under the 860-byte floor) is NOT compressed
even when the client offers gzip — the minimum-size exemption."""
r = client.get("/api/health", headers={"Accept-Encoding": "gzip"})
assert r.status_code == 200
assert "content-encoding" not in r.headers
assert r.json()["status"] == "ok"
def test_html_page_contract_survives_gzip(client: TestClient) -> None:
"""A19 through the new layer: the rewritten HTML page (>= the floor, so
it compresses) STILL serves no-cache with the ?v= asset rewrite and no
validators — and the contract holds AFTER decompression."""
r = client.get("/", headers={"Accept-Encoding": "gzip"})
assert r.status_code == 200
assert r.headers["content-encoding"] == "gzip" # 95 KB page, far above the floor
assert r.headers["cache-control"] == "no-cache"
assert "etag" not in r.headers
assert "last-modified" not in r.headers
assert r.text.count("?v=") >= 1 # the caching rewrite ran, gzip preserved it
# ---------------------------------------------------------------------------
# Phase 167 (task 02): the depth-limited tree + server totals (D2/A2) — the
# default depth is the RAG view's first paint (sources + ONE level), an
# explicit depth builds deeper, out-of-range values are rejected, and an
# untruncated node keeps its pre-phase-167 wire shape.
# ---------------------------------------------------------------------------
def _seed_depth_corpus(db) -> datetime:
"""Four documents nested one level deeper each (root, ``a``, ``a/b``,
``a/b/c``) with DISTINCT ``indexed_at`` stamps, plus one stored
summary on ``a`` — a fixture whose truncated nodes still carry a
recursive count, and whose level rows carry a description and a
pending marker."""
_truncate_tree_tables(db)
base = datetime.now(UTC)
db.add(GitSource(url="https://github.com/reese/Homelab.git", kind="git", added_at=base))
_seed_doc(db, "Homelab", "root.md", "Root", 1, base)
_seed_doc(db, "Homelab", "a/top.md", "Top", 2, base + timedelta(hours=1))
_seed_doc(db, "Homelab", "a/b/mid.md", "Mid", 3, base + timedelta(hours=2))
_seed_doc(db, "Homelab", "a/b/c/deep.md", "Deep", 4, base + timedelta(hours=3))
db.add(FolderSummary(source="Homelab", folder_path="a", summary="A desc."))
db.commit() # the app's endpoint session reads committed data only
return base
def test_docs_tree_default_depth_ships_one_level_with_totals(admin_client, db) -> None:
"""``GET /api/docs/tree`` (no param) is the view's first paint:
sources + ONE level. The folder with unloaded children carries
``truncated: true`` + ``children: []`` AND its recursive count;
``totals`` matches ``GET /api/docs`` (documents, chunks, the newest
``indexed_at``) — the stat cards stay honest on a partial tree."""
base = _seed_depth_corpus(db)
try:
r = admin_client.get("/api/docs/tree")
assert r.status_code == 200
body = r.json()
assert set(body) == {"sources", "totals"}
(homelab,) = body["sources"]
assert homelab["documents"] == 4 # the whole recursive source count
assert "truncated" not in homelab # depth >= 1: never truncated
folders = [c for c in homelab["children"] if c["kind"] == "folder"]
files = [c for c in homelab["children"] if c["kind"] == "file"]
assert [f["path"] for f in files] == ["root.md"] # direct files ship
(a,) = folders
assert a["path"] == "a"
assert a["truncated"] is True
assert a["children"] == []
assert a["documents"] == 3 # the recursive count, no children built
assert a["summary"] == "A desc." # the level row shows the description
assert a["summary_pending"] is False # the stored row covers it
# Nothing deeper is on the wire (the 2 MB payload stays home).
assert [f["path"] for f in _tree_file_nodes_all(body["sources"])] == ["root.md"]
# Totals from the SAME catalogue rows (no extra query).
assert body["totals"] == {
"documents": 4,
"chunks": 10,
"last_indexed": (base + timedelta(hours=3)).isoformat(),
}
flat = admin_client.get("/api/docs").json()
assert body["totals"]["documents"] == len(flat["documents"])
assert body["totals"]["chunks"] == sum(d["chunks"] for d in flat["documents"])
assert body["totals"]["last_indexed"] == max(
d["indexed_at"] for d in flat["documents"]
)
finally:
_truncate_tree_tables(db)
def test_docs_tree_depth_three_builds_deeper_and_truncates_the_rest(
admin_client, db
) -> None:
"""``?depth=3``: ``a`` and ``a/b`` are built (no ``truncated`` key),
``a/b``'s subfolder ``a/b/c`` is truncated — while ``a/b``'s OWN
direct files still ship: the budget limits DEPTH, never the row set
of the levels it does send. The phase-98 pending rule is unchanged
on truncated nodes (``a/b/c``: 1 doc, no stored row → pending)."""
_seed_depth_corpus(db)
try:
body = admin_client.get("/api/docs/tree?depth=3").json()
(homelab,) = body["sources"]
(a,) = [c for c in homelab["children"] if c["kind"] == "folder"]
assert "truncated" not in a # expanded at this depth
a_children = {c["path"]: c for c in a["children"]}
assert set(a_children) == {"a/b", "a/top.md"}
a_b = a_children["a/b"]
assert "truncated" not in a_b
a_b_c, mid = a_b["children"] # subfolder first, then the direct file
assert (a_b_c["kind"], a_b_c["path"], a_b_c["truncated"]) == (
"folder",
"a/b/c",
True,
)
assert (a_b_c["documents"], a_b_c["children"]) == (1, [])
assert a_b_c["summary"] is None
assert a_b_c["summary_pending"] is True # 1 doc, no stored row
assert (mid["kind"], mid["path"]) == ("file", "a/b/mid.md")
assert {f["path"] for f in _tree_file_nodes_all(body["sources"])} == {
"root.md",
"a/top.md",
"a/b/mid.md",
} # deep.md never reaches the wire at this depth
finally:
_truncate_tree_tables(db)
def test_docs_tree_depth_validation_matrix(admin_client, db) -> None:
"""``depth`` is bounded (1..8) and typed: ``0`` and ``9`` are out of
range and ``abc`` is not an int — all 422 before any build runs;
the bounds themselves answer 200."""
_seed_depth_corpus(db)
try:
for bad in ("0", "9", "abc"):
r = admin_client.get(f"/api/docs/tree?depth={bad}")
assert r.status_code == 422, bad
assert admin_client.get("/api/docs/tree?depth=1").status_code == 200
assert admin_client.get("/api/docs/tree?depth=8").status_code == 200
finally:
_truncate_tree_tables(db)
def test_docs_tree_full_build_keeps_the_pre_phase_node_wire_shape(
admin_client, db
) -> None:
"""The wire-shape pin: on a FULL (``depth=8``) build nothing is
truncated, so NO node carries the ``truncated`` key — the source,
folder and file key sets are exactly the pre-phase-167 ones (the
omission serializer keeps every existing consumer byte-identical);
the additive top-level ``totals`` is the only body change."""
_seed_depth_corpus(db)
try:
body = admin_client.get("/api/docs/tree?depth=8").json()
assert set(body) == {"sources", "totals"}
(homelab,) = body["sources"]
assert set(homelab) == {
"name", "documents", "updated_at", "summary", "summary_pending", "children"
}
seen_folders = seen_files = 0
stack = list(homelab["children"])
while stack:
node = stack.pop()
if node["kind"] == "folder":
seen_folders += 1
assert set(node) == {
"kind", "path", "documents", "updated_at", "summary",
"summary_pending", "children",
}
stack.extend(node["children"])
else:
seen_files += 1
assert set(node) == {
"kind", "path", "title", "chunks", "created_at", "indexed_at"
}
assert (seen_folders, seen_files) == (3, 4)
finally:
_truncate_tree_tables(db)
# ---------------------------------------------------------------------------
# Phase 167 (task 03): GET /api/docs/tree/node — the drill's per-level
# fetch — and the ETag revalidation of the three catalogue reads
# (D3, LOCKED A3/A4).
# ---------------------------------------------------------------------------
def _find_child(node: dict, path: str) -> dict:
"""The child of *node* whose ``path`` equals *path* (the level walk
the drill does client-side, expressed once for the assertions)."""
return next(child for child in node["children"] if child["path"] == path)
def test_tree_node_source_root_is_the_kb_tree_source_shape(
admin_client: TestClient, db
) -> None:
"""``?source=Homelab`` (``path`` omitted = ``""``; the explicit
``path=`` answers the identical node): the SOURCE shape — the full
key set, never ``truncated``, the whole recursive source count —
with the root level only: the direct file plus the subfolder
flagged for the next drill."""
_seed_depth_corpus(db)
try:
r = admin_client.get("/api/docs/tree/node", params={"source": "Homelab"})
assert r.status_code == 200
node = r.json()
assert "truncated" not in node # the node's OWN children are always built
assert set(node) == {
"name",
"documents",
"updated_at",
"summary",
"summary_pending",
"children",
}
assert node["name"] == "Homelab"
assert node["documents"] == 4 # the whole recursive source count
assert node["summary_pending"] is True # no stored (Homelab, "") row
(a,) = [c for c in node["children"] if c["kind"] == "folder"]
(root,) = [c for c in node["children"] if c["kind"] == "file"]
assert (a["path"], a["truncated"], a["children"], a["documents"]) == (
"a",
True,
[],
3,
)
assert root["path"] == "root.md"
# The explicit empty path is the SAME node as the omitted one.
r2 = admin_client.get(
"/api/docs/tree/node", params={"source": "Homelab", "path": ""}
)
assert r2.status_code == 200
assert r2.json() == node
finally:
_truncate_tree_tables(db)
def test_tree_node_nested_folder_fields_and_depth_one_children(
admin_client: TestClient, db
) -> None:
"""``?source=Homelab&path=a&depth=1``: the KbTreeFolder shape with
every field the level row renders — recursive count, recursive
``updated_at``, ``summary``, ``summary_pending`` (phase 98 D3) — and
its DIRECT children only: the deeper subfolder flagged
``truncated`` (the next drill's target), the direct file present."""
_seed_depth_corpus(db)
try:
r = admin_client.get(
"/api/docs/tree/node", params={"source": "Homelab", "path": "a"}
)
assert r.status_code == 200
node = r.json()
assert node["kind"] == "folder"
assert node["path"] == "a"
assert "truncated" not in node # this node's children WERE built
assert node["documents"] == 3 # recursive — the phase-94 count rule
assert node["updated_at"] is not None # recursive max (phase 106 D9)
assert node["summary"] == "A desc."
assert node["summary_pending"] is False # the stored row covers it
a_b, top = node["children"] # subfolder first, then the direct file
assert (a_b["kind"], a_b["path"], a_b["truncated"], a_b["children"]) == (
"folder",
"a/b",
True,
[],
)
assert a_b["documents"] == 2 # mid.md + deep.md, recursive
assert a_b["summary_pending"] is True # docs but no stored row
assert (top["kind"], top["path"]) == ("file", "a/top.md")
finally:
_truncate_tree_tables(db)
def test_tree_node_depth_two_builds_one_level_deeper(
admin_client: TestClient, db
) -> None:
"""``&depth=2`` spends one more level: ``a/b`` arrives BUILT (no
``truncated`` key, its direct file present) and only its own
subfolder stays truncated — the same budget semantics the full tree
applies, answered for one node."""
_seed_depth_corpus(db)
try:
r = admin_client.get(
"/api/docs/tree/node",
params={"source": "Homelab", "path": "a", "depth": 2},
)
assert r.status_code == 200
a_b = _find_child(r.json(), "a/b")
assert "truncated" not in a_b # expanded at this depth
a_b_c, mid = a_b["children"]
assert (a_b_c["path"], a_b_c["truncated"], a_b_c["documents"]) == (
"a/b/c",
True,
1,
)
assert (mid["kind"], mid["path"]) == ("file", "a/b/mid.md")
finally:
_truncate_tree_tables(db)
def test_tree_node_404_unknown_source_folder_and_file_path(
admin_client: TestClient, db
) -> None:
"""404 ``{"detail": "unknown node"}`` for a source that is neither
registered nor indexed, a folder failing the phase-94 existence
rule, a FILE's path (a document's OWN path is not a folder), and
traversal strings — which have no filesystem surface to hit: the
node is derived from catalogue ROWS alone (the row-only rule, the
phase-10 traversal argument reused)."""
_seed_depth_corpus(db)
try:
for params in (
{"source": "nope"}, # neither registered nor indexed
{"source": "Homelab", "path": "nope"}, # no indexed path starts with it
{"source": "Homelab", "path": "a/top.md"}, # a FILE's own path
{"source": "Homelab", "path": "../Homelab"}, # traversal is not a folder
{"source": "..", "path": ""}, # nor is it a source
):
r = admin_client.get("/api/docs/tree/node", params=params)
assert r.status_code == 404, params
assert r.json() == {"detail": "unknown node"}
finally:
_truncate_tree_tables(db)
def test_tree_node_403_anonymous(client: TestClient, db) -> None:
"""The same gate as ``/api/docs/tree`` — anonymous 403
``{"detail": "admin only"}`` (the RAG view never fetches for guests,
the endpoint must not lean on that)."""
_seed_depth_corpus(db)
try:
r = client.get("/api/docs/tree/node", params={"source": "Homelab"})
assert r.status_code == 403
assert r.json() == {"detail": "admin only"}
finally:
_truncate_tree_tables(db)
def test_tree_node_validation_matrix(admin_client: TestClient, db) -> None:
"""``source`` is required and ``depth`` keeps the tree route's
bounds (``ge=1, le=8``): a missing source, ``depth=0``, ``depth=9``
and ``depth=abc`` all 422 before anything is built."""
_seed_depth_corpus(db)
try:
assert admin_client.get("/api/docs/tree/node").status_code == 422
for bad in ("0", "9", "abc"):
r = admin_client.get(
"/api/docs/tree/node", params={"source": "Homelab", "depth": bad}
)
assert r.status_code == 422, bad
finally:
_truncate_tree_tables(db)
def test_tree_node_children_equal_the_full_tree_level(
admin_client: TestClient, db
) -> None:
"""ONE concept, two routes, pinned equal: at the same budget the
node route answers EXACTLY what ``GET /api/docs/tree`` carries at
the same level — the source root at the default budget AND the full
one, and the nested ``a`` folder on the full build — the JSON
compared whole, so the drill can never paint a node the tree would
not have produced."""
_seed_depth_corpus(db)
try:
for depth in (1, 8):
tree = admin_client.get(f"/api/docs/tree?depth={depth}").json()
(source_tree,) = tree["sources"]
node_root = admin_client.get(
"/api/docs/tree/node", params={"source": "Homelab", "depth": depth}
)
assert node_root.status_code == 200
assert node_root.json() == source_tree
full = admin_client.get("/api/docs/tree?depth=8").json()
a_tree = _find_child(full["sources"][0], "a")
node_a = admin_client.get(
"/api/docs/tree/node",
params={"source": "Homelab", "path": "a", "depth": 8},
)
assert node_a.status_code == 200
assert node_a.json() == a_tree
finally:
_truncate_tree_tables(db)
_CATALOGUE_READ_URLS = (
"/api/docs",
"/api/docs/tree",
"/api/docs/tree/node?source=Homelab&path=a",
)
def test_catalogue_reads_carry_etag_and_answer_304(
admin_client: TestClient, db
) -> None:
"""The ETag matrix on all three catalogue reads (A4): the first GET
→ 200 + weak ``ETag`` + ``Cache-Control: private, must-revalidate``;
the re-send with ``If-None-Match`` → 304 with an EMPTY body and
BOTH headers (RFC 9110 §8.8.3 — a 304 repeats the validator); the
strong form of the weak tag, a comma-separated list and ``*`` all
revalidate; a wrong tag is a plain 200 carrying the CURRENT tag."""
_seed_depth_corpus(db)
try:
for url in _CATALOGUE_READ_URLS:
first = admin_client.get(url)
assert first.status_code == 200, url
tag = first.headers["etag"]
assert tag.startswith('W/"'), url
assert first.headers["cache-control"] == "private, must-revalidate", url
hit = admin_client.get(url, headers={"If-None-Match": tag})
assert hit.status_code == 304, url
assert hit.content == b"", url
assert hit.headers["etag"] == tag, url
assert hit.headers["cache-control"] == "private, must-revalidate", url
strong = tag[2:] # the same tag WITHOUT the weakness marker
assert admin_client.get(
url, headers={"If-None-Match": strong}
).status_code == 304, url
assert admin_client.get(
url, headers={"If-None-Match": f'W/"stale-tag", {strong}'}
).status_code == 304, url
assert admin_client.get(url, headers={"If-None-Match": "*"}).status_code == (
304
), url
miss = admin_client.get(url, headers={"If-None-Match": 'W/"not-the-tag"'})
assert miss.status_code == 200, url
assert miss.headers["etag"] == tag, url
assert miss.json() == first.json(), url
finally:
_truncate_tree_tables(db)
def _seed_bust_corpus(db) -> datetime:
"""The depth corpus with DETERMINISTIC, distinct ``created_at``
stamps (the default seeding's subtree max is insert-time
microseconds): ``a/b/c/deep.md`` IS the max of ``a``, so editing its
date provably MOVES every answer that carries ``a``'s
``updated_at`` — which is what the bust test below asserts."""
_truncate_tree_tables(db)
base = datetime.now(UTC)
dates = {
"root.md": base - timedelta(days=30),
"a/top.md": base - timedelta(days=20),
"a/b/mid.md": base - timedelta(days=10),
"a/b/c/deep.md": base - timedelta(days=1), # the subtree max of "a"
}
db.add(GitSource(url="https://github.com/reese/Homelab.git", kind="git", added_at=base))
for i, (path, created) in enumerate(dates.items()):
doc = Document(
source="Homelab",
path=path,
full_path=f"/tmp/Homelab/{path}",
title=path,
content=f"# {path}\n\nBody.",
content_hash=f"{next(_counter):064d}", # unique per row
indexed_at=base,
created_at=created,
)
db.add(doc)
db.flush()
db.add(
Chunk(document_id=doc.id, position=0, content=f"chunk {i}", embedding=[0.01] * 768)
)
db.add(FolderSummary(source="Homelab", folder_path="a", summary="A desc."))
db.commit() # the app's endpoint session reads committed data only
return base
def test_etag_busts_on_folder_summary_and_document_date(
admin_client: TestClient, db
) -> None:
"""The LOCKED A4 proof: the admin PATCHes mutate the tree WITHOUT
bumping ``sources_meta.version`` (asserted around the edits — a
version-derived tag would keep answering 304 here). After
``PATCH /api/folders/summary`` AND after ``PATCH
/api/documents/date`` the tag CHANGES and the conditional GET with
the OLD tag answers a fresh 200 carrying the new data."""
_seed_bust_corpus(db)
try:
version_before = db.scalar(select(func.max(SourcesMeta.version)))
tree_url = "/api/docs/tree"
node_url = "/api/docs/tree/node?source=Homelab&path=a"
old_tree = admin_client.get(tree_url).headers["etag"]
old_node = admin_client.get(node_url).headers["etag"]
r = admin_client.patch(
"/api/folders/summary",
json={"source": "Homelab", "folder_path": "a", "summary": "Handwritten."},
)
assert r.status_code == 200
t = admin_client.get(tree_url, headers={"If-None-Match": old_tree})
assert t.status_code == 200 and t.headers["etag"] != old_tree
assert _find_child(t.json()["sources"][0], "a")["summary"] == "Handwritten."
n = admin_client.get(node_url, headers={"If-None-Match": old_node})
assert n.status_code == 200 and n.headers["etag"] != old_node
assert n.json()["summary"] == "Handwritten."
# The date edit moves the subtree max (deep.md is "a"'s max in
# this corpus) — the tree, the node AND /api/docs all change.
old_tree2 = admin_client.get(tree_url).headers["etag"]
old_docs = admin_client.get("/api/docs").headers["etag"]
deep_max_before = _find_child(
admin_client.get(tree_url).json()["sources"][0], "a"
)["updated_at"]
r = admin_client.patch(
"/api/documents/date",
json={"source": "Homelab", "path": "a/b/c/deep.md", "date": "2020-01-01"},
)
assert r.status_code == 200
t2 = admin_client.get(tree_url, headers={"If-None-Match": old_tree2})
assert t2.status_code == 200 and t2.headers["etag"] != old_tree2
deep_max_after = _find_child(t2.json()["sources"][0], "a")["updated_at"]
assert deep_max_after != deep_max_before # the body really moved
d2 = admin_client.get("/api/docs", headers={"If-None-Match": old_docs})
assert d2.status_code == 200 and d2.headers["etag"] != old_docs
deep = next(d for d in d2.json()["documents"] if d["path"] == "a/b/c/deep.md")
assert deep["created_at"].startswith("2020-01-01")
# A4's other half: NEITHER PATCH bumped the KB generation (that
# would mark every saved chat stale — phase 53).
assert db.scalar(select(func.max(SourcesMeta.version))) == version_before
finally:
_truncate_tree_tables(db)
def test_gzipped_catalogue_200_carries_etag_and_conditional_304(
admin_client: TestClient, db
) -> None:
"""The compression layer and the validator coexist: the gzipped 200
of the tree still carries the ``ETag`` (the header survives the
encoding), and the conditional request answers a body-less 304 —
no ``Content-Encoding`` on a response with no body."""
_seed_gzip_corpus(db)
try:
# depth=8: the whole 50-doc tree stays above the 860-byte floor
# (the default depth-1 body would ride under it, identity).
r = admin_client.get("/api/docs/tree?depth=8", headers={"Accept-Encoding": "gzip"})
assert r.status_code == 200
assert r.headers["content-encoding"] == "gzip"
tag = r.headers["etag"]
assert tag.startswith('W/"')
assert r.headers["cache-control"] == "private, must-revalidate"
hit = admin_client.get(
"/api/docs/tree?depth=8",
headers={"Accept-Encoding": "gzip", "If-None-Match": tag},
)
assert hit.status_code == 304
assert hit.content == b""
assert "content-encoding" not in hit.headers
assert hit.headers["etag"] == tag
assert hit.headers["cache-control"] == "private, must-revalidate"
finally:
_truncate_tree_tables(db)
+47 -1
View File
@@ -154,7 +154,10 @@ def test_tree_file_dates_and_derived_subtree_max(admin_client, db) -> None:
_seed_doc(db, "Homelab", "a/top.md", "Top", 1, base, C2)
_seed_doc(db, "Homelab", "root.md", "Root", 0, base, C0)
try:
r = admin_client.get("/api/docs/tree")
# Phase 167 (task 02): this D9 walk visits ``a`` → ``a/b`` →
# ``a/b/c``, so it asks for the WHOLE tree (depth=8); the
# endpoint's default (depth=1) is the RAG view's first paint.
r = admin_client.get("/api/docs/tree?depth=8")
assert r.status_code == 200
sources = r.json()["sources"]
# Registry order leads; the 0-document registered source lists.
@@ -198,6 +201,49 @@ def test_tree_file_dates_and_derived_subtree_max(admin_client, db) -> None:
_truncate(db)
def test_tree_truncated_folder_keeps_the_recursive_date_at_default_depth(
admin_client, db
) -> None:
"""Phase 167 (task 02, D2/A2) — the truncation-proof counterpart of
the D9 walk above: at the DEFAULT depth (the view's first paint)
the deep levels are not sent, but the truncated folder node still
carries the phase-106 D9 contract — ``updated_at`` = the subtree's
recursive MAX (deep C3) and ``documents`` = the recursive count —
alongside ``truncated: true`` and ``children: []`` (the builder's
max-date map is what buys this: the payload diet never loses the
date column). The server ``totals`` ride the same response.
"""
_truncate(db)
base = datetime.now(UTC)
db.add(GitSource(url="https://github.com/reese/Homelab.git", kind="git", added_at=base))
_seed_doc(db, "Homelab", "a/b/c/deep.md", "Deep", 1, base, C3)
_seed_doc(db, "Homelab", "a/b/shallow.md", "Shallow", 0, base, C1)
_seed_doc(db, "Homelab", "a/top.md", "Top", 1, base, C2)
_seed_doc(db, "Homelab", "root.md", "Root", 0, base, C0)
try:
r = admin_client.get("/api/docs/tree") # the DEFAULT depth = 1
assert r.status_code == 200
body = r.json()
homelab = _tree_node(body["sources"], "Homelab")
# One level only: the folder ``a`` is present but NOT built out.
(a,) = [c for c in homelab["children"] if c["kind"] == "folder"]
assert (a["path"], a["truncated"], a["children"]) == ("a", True, [])
# The truncation-proof fields: recursive count + recursive date.
assert a["documents"] == 3
assert a["updated_at"] == C3.isoformat() # the DEEP max, unbuilt children
# The source node's own max still spans the whole (unsent) subtree.
assert homelab["updated_at"] == C3.isoformat()
assert "truncated" not in homelab # depth >= 1: never truncated, key omitted
# Server totals beside the depth-limited tree (the stat cards).
assert body["totals"] == {
"documents": 4,
"chunks": 2,
"last_indexed": base.isoformat(),
}
finally:
_truncate(db)
# ---------------------------------------------------------------------------
# PATCH /api/documents/date (D7) — the admin document-date editor.
# ---------------------------------------------------------------------------
@@ -0,0 +1,238 @@
"""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