docs(06): create phase 6 plan — performance & production hardening

6 plans across 4 waves covering structlog/Loki observability, Locust
load testing, multi-stage Dockerfile hardening, trusted-proxy rate
limiting, and RUNBOOK.md. Verification passed (0 blockers).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-06-02 20:35:03 +02:00
co-authored by Claude Sonnet 4.6
parent b7503cdff4
commit 70c09f6cd4
10 changed files with 1778 additions and 14 deletions
@@ -0,0 +1,255 @@
---
phase: 06-performance-production-hardening
plan: 01
type: execute
wave: 0
depends_on: []
files_modified:
- backend/tests/test_logging.py
- backend/tests/test_rate_limiting.py
- backend/load_tests/__init__.py
- backend/load_tests/locustfile.py
autonomous: false
requirements:
- D-01
- D-02
- D-04
- D-05
- D-06
- D-11
- D-12
user_setup: []
must_haves:
truths:
- "pytest -v collects test_logging.py and test_rate_limiting.py without import errors"
- "All Wave 0 stubs run as xfail(strict=False) so no false-positive xpass breaks CI"
- "backend/load_tests/ is excluded from pytest discovery via empty __init__.py"
- "Package legitimacy for structlog and locust is verified by a human before any requirements file is modified"
- "An automated unit test asserts the slowapi key_func ordering assumption (A1) before per-account limiters ship"
artifacts:
- path: "backend/tests/test_logging.py"
provides: "xfail stubs for D-01/D-02 (correlation_id field, context cleared between requests, JSON renderer wired)"
contains: "pytest.mark.xfail"
- path: "backend/tests/test_rate_limiting.py"
provides: "xfail stubs for D-11 (get_client_ip trusted/untrusted) and D-12 (account_limiter key, 429 after 100 req/min, key_func ordering assumption A1)"
contains: "pytest.mark.xfail"
- path: "backend/load_tests/__init__.py"
provides: "Empty marker file that makes load_tests an importable package and stops pytest from descending into it"
- path: "backend/load_tests/locustfile.py"
provides: "Locust HttpUser skeleton with TODO body — full implementation in 06-03"
contains: "class DocuVaultUser"
key_links:
- from: "backend/tests/test_rate_limiting.py::test_account_limiter_key_ordering"
to: "Pattern 4 / Pitfall 3 in 06-RESEARCH.md (A1 verification)"
via: "ASGI request fixture with request.state.current_user pre-set"
pattern: "request\\.state\\.current_user"
- from: "backend/load_tests/__init__.py"
to: "pytest collection"
via: "package import gate"
pattern: "__init__\\.py"
---
<objective>
Establish the Nyquist Wave 0 scaffold for Phase 6: failing test stubs for structured logging (D-01/D-02), trusted-proxy rate limiting (D-11), and per-account rate limiting (D-12); a Locust load-test skeleton (D-04/D-05) excluded from pytest discovery; and a blocking package-legitimacy checkpoint for the two [ASSUMED] dependencies (structlog 25.5.0, locust 2.34.0) before any requirements file is touched.
Purpose: Lock down behavior expectations before implementation; surface the slowapi key_func ordering assumption (RESEARCH.md A1) as an explicit test that downstream plans must turn green; gate package installs behind a blocking human verification per package-legitimacy protocol.
Output: Four scaffolded files plus a verified human-approved package list ready for 06-02 / 06-03 to install.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/06-performance-production-hardening/06-CONTEXT.md
@.planning/phases/06-performance-production-hardening/06-RESEARCH.md
@.planning/phases/06-performance-production-hardening/06-PATTERNS.md
@.planning/phases/06-performance-production-hardening/06-VALIDATION.md
@CLAUDE.md
@backend/tests/conftest.py
@backend/deps/utils.py
@backend/pytest.ini
</context>
<tasks>
<task type="checkpoint:human-verify" gate="blocking-human">
<name>Task 1: Package legitimacy verification — structlog and locust</name>
<read_first>
- .planning/phases/06-performance-production-hardening/06-RESEARCH.md (sections: Package Legitimacy Audit, Standard Stack)
</read_first>
<what-built>
Two PyPI packages are flagged `[ASSUMED]` by the researcher because slopcheck was unavailable at research time. No code change has been made yet — this checkpoint exists so the user can confirm legitimacy before any requirements file is modified.
</what-built>
<how-to-verify>
1. Open https://pypi.org/project/structlog/ — confirm version 25.5.0 is published; confirm the homepage points to https://github.com/hynek/structlog; confirm author is Hynek Schlawack (a CPython core contributor).
2. Open https://pypi.org/project/locust/ — confirm version 2.34.0 is published; confirm the homepage points to https://github.com/locustio/locust; confirm the project has >10 years of release history and multi-million monthly downloads.
3. Confirm neither package name is a typosquat of a similar published name (e.g. `struct-log`, `locusts`).
</how-to-verify>
<acceptance_criteria>
- User responds with "approved" or "approved with notes" — execution may proceed.
- User responds with anything else — execution halts; planner reconsiders alternatives or aborts the phase.
</acceptance_criteria>
<resume-signal>Type "approved" to allow 06-02 to add structlog and 06-03 to add locust; otherwise describe the concern.</resume-signal>
</task>
<task type="auto">
<name>Task 2: Create test_logging.py with xfail stubs (D-01/D-02)</name>
<files>backend/tests/test_logging.py</files>
<read_first>
- backend/tests/conftest.py (fixture conventions: async_client, auth_user, pytest_asyncio.fixture, asyncio_mode=auto)
- backend/pytest.ini (asyncio_mode setting; verify xfail is a recognised marker)
- .planning/phases/06-performance-production-hardening/06-RESEARCH.md (Pattern 1, Pattern 2, Pitfall 2)
</read_first>
<action>
Create backend/tests/test_logging.py with single-line-body xfail stubs (strict=False) for these behaviours, one test function each — body is exactly `pytest.xfail("not implemented yet")`:
(1) test_setup_logging_emits_json_when_LOG_JSON_true — D-01 contract that setup_logging(json_logs=True) installs JSONRenderer through ProcessorFormatter and root logger emits JSON to stdout.
(2) test_correlation_id_middleware_binds_contextvar — CorrelationIDMiddleware binds correlation_id, path, method into structlog.contextvars per request.
(3) test_correlation_id_response_header_present — every HTTP response includes X-Correlation-ID header set to the bound correlation_id.
(4) test_contextvars_cleared_between_requests — Pitfall 2 — clear_contextvars() runs first in the middleware so user_id from a prior request never bleeds into the next.
(5) test_uvicorn_access_log_suppressed — uvicorn.access propagate=False after setup_logging() so the middleware owns request logging.
Use the same async_client fixture pattern from conftest.py for tests that need ASGI requests. Mark all five with `@pytest.mark.xfail(strict=False, reason="implementation in 06-02")`. Single-line body only — no assertion code that could xpass accidentally.
</action>
<verify>
<automated>cd backend &amp;&amp; pytest tests/test_logging.py -v --no-header 2>&amp;1 | grep -v '^#' | grep -cE 'XFAIL|xfailed'</automated>
</verify>
<acceptance_criteria>
- File backend/tests/test_logging.py exists and is importable: `cd backend &amp;&amp; python -c "import tests.test_logging"` exits 0.
- `cd backend &amp;&amp; pytest tests/test_logging.py -v --no-header 2>&amp;1` shows exactly 5 collected tests, all XFAIL, zero PASSED, zero FAILED, zero ERROR.
- Every test body is the single line `pytest.xfail("not implemented yet")``grep -c "pytest.xfail" backend/tests/test_logging.py` returns 5.
- All five tests are decorated `@pytest.mark.xfail(strict=False, reason=...)``grep -c "xfail(strict=False" backend/tests/test_logging.py` returns 5.
</acceptance_criteria>
<done>Five xfail stubs collected, all show XFAIL status, full suite still passes the same count as before this plan.</done>
</task>
<task type="auto">
<name>Task 3: Create test_rate_limiting.py with xfail stubs (D-11/D-12 + A1)</name>
<files>backend/tests/test_rate_limiting.py</files>
<read_first>
- backend/deps/utils.py (current get_client_ip body; the function that 06-04 will replace)
- backend/tests/conftest.py (async_client, auth_user, second_auth_user fixtures)
- .planning/phases/06-performance-production-hardening/06-RESEARCH.md (Pattern 3, Pattern 4, Pitfall 3, Assumption A1)
- .planning/phases/06-performance-production-hardening/06-PATTERNS.md (deps/utils.py section, account_limiter section)
</read_first>
<action>
Create backend/tests/test_rate_limiting.py with single-line-body xfail stubs (strict=False), one test function each — body is exactly `pytest.xfail("not implemented yet")`:
(1) test_get_client_ip_untrusted_returns_direct_peer — D-11 — when request.client.host is 8.8.8.8 (not in trusted CIDRs), ignore X-Forwarded-For and return "8.8.8.8".
(2) test_get_client_ip_trusted_proxy_reads_xff_leftmost — D-11 — when request.client.host is 127.0.0.1 (trusted), return the leftmost IP of X-Forwarded-For: "1.2.3.4, 5.6.7.8" → "1.2.3.4".
(3) test_get_client_ip_trusted_proxy_no_xff_falls_back — D-11 — trusted peer with no XFF header returns the direct peer IP.
(4) test_get_client_ip_invalid_peer_returns_none_or_string — D-11 — request.client is None returns None without raising.
(5) test_account_limiter_key_uses_user_id — D-12 — _account_key(request) where request.state.current_user has id=UUID(...) returns str(user.id), not request.client.host.
(6) test_account_limiter_key_falls_back_to_ip_when_no_user — D-12 — when request.state.current_user is missing, key function returns the direct peer IP (Pitfall 3 — must not crash).
(7) test_account_limiter_key_ordering_assumption — A1 verification — construct a FastAPI app with one endpoint that sets `request.state.current_user = current_user` as its first line, decorated with `@account_limiter.limit("100/minute")`; call it 101 times with the same user; assert the 101st response is 429 AND the limiter recorded the key as str(user.id), not as the IP. This test is the gate that turns A1 from assumption to fact before 06-04 wires the decorator on all endpoints.
(8) test_authenticated_endpoint_429_after_100_per_minute — D-12 — full integration: GET /api/documents/ 101 times with the same auth_user; 101st returns 429.
Mark all eight with `@pytest.mark.xfail(strict=False, reason="implementation in 06-04")`. Single-line body only.
</action>
<verify>
<automated>cd backend &amp;&amp; pytest tests/test_rate_limiting.py -v --no-header 2>&amp;1 | grep -v '^#' | grep -cE 'XFAIL|xfailed'</automated>
</verify>
<acceptance_criteria>
- File backend/tests/test_rate_limiting.py exists and is importable: `cd backend &amp;&amp; python -c "import tests.test_rate_limiting"` exits 0.
- `cd backend &amp;&amp; pytest tests/test_rate_limiting.py -v --no-header` shows exactly 8 collected tests, all XFAIL, zero PASSED, zero FAILED.
- `grep -c "pytest.xfail" backend/tests/test_rate_limiting.py` returns 8.
- `grep -c "xfail(strict=False" backend/tests/test_rate_limiting.py` returns 8.
- At least one test name contains the substring "ordering" — `grep -c "def test.*ordering" backend/tests/test_rate_limiting.py` returns 1.
</acceptance_criteria>
<done>Eight xfail stubs collected, A1 verification test present, no regressions in existing suite.</done>
</task>
<task type="auto">
<name>Task 4: Create backend/load_tests/ package skeleton with Locust stub</name>
<files>backend/load_tests/__init__.py, backend/load_tests/locustfile.py</files>
<read_first>
- .planning/phases/06-performance-production-hardening/06-RESEARCH.md (Pattern 7)
- .planning/phases/06-performance-production-hardening/06-PATTERNS.md (locustfile.py section)
- backend/pytest.ini (testpaths setting — confirm pytest does not include load_tests)
</read_first>
<action>
Create two files:
(1) backend/load_tests/__init__.py — empty file (zero bytes). This serves a single purpose: turn load_tests/ into a Python package so locust can import it cleanly, while pytest's default test discovery (which looks for test_*.py files) will not collect locustfile.py because its filename does not match the test_*.py pattern. Add a one-line comment `# Locust load-test package — not a pytest test target` if any content is needed.
(2) backend/load_tests/locustfile.py — skeleton with the import block (`from locust import HttpUser, task, between, events`), TEST_EMAIL / TEST_PASSWORD env-var pickup with safe defaults, a class `DocuVaultUser(HttpUser)` containing wait_time = between(0.5, 2.0), an empty access_token attribute, and method stubs `on_start`, `_auth_headers`, `list_documents`, `upload_document`, `refresh_token`, plus an `@events.quitting.add_listener def check_sla(...)` registered listener. Every method body is exactly `raise NotImplementedError("implementation in 06-03")`. Top of file: docstring referencing D-04/D-05/D-06 and the run command from RESEARCH.md Pattern 7. Do NOT import anything from the application code (no `from backend...` or `from api...` lines).
</action>
<verify>
<automated>test -f backend/load_tests/__init__.py &amp;&amp; test -f backend/load_tests/locustfile.py &amp;&amp; cd backend &amp;&amp; python -c "import ast; ast.parse(open('load_tests/locustfile.py').read())" &amp;&amp; cd backend &amp;&amp; pytest tests/ -v --collect-only --no-header 2>&amp;1 | grep -c "load_tests"</automated>
</verify>
<acceptance_criteria>
- File backend/load_tests/__init__.py exists; `wc -c backend/load_tests/__init__.py` returns ≤ 70 bytes (empty or one comment line).
- File backend/load_tests/locustfile.py exists and is syntactically valid Python: `python -c "import ast; ast.parse(open('backend/load_tests/locustfile.py').read())"` exits 0.
- locustfile.py defines exactly one class containing the string `class DocuVaultUser``grep -c "class DocuVaultUser" backend/load_tests/locustfile.py` returns 1.
- locustfile.py contains the SLA listener marker — `grep -c "events.quitting.add_listener" backend/load_tests/locustfile.py` returns 1.
- locustfile.py contains no application imports — `grep -cE "^from (backend|api|services|db|deps)" backend/load_tests/locustfile.py` returns 0.
- `cd backend &amp;&amp; pytest tests/ --collect-only --no-header 2>&amp;1 | grep -c "load_tests"` returns 0 (pytest must NOT discover anything in load_tests).
- Each method body raises NotImplementedError — `grep -c "raise NotImplementedError" backend/load_tests/locustfile.py` returns ≥ 5.
</acceptance_criteria>
<done>Package present, locustfile importable by `python -c "import sys; sys.path.insert(0,'backend/load_tests'); import locustfile"`, pytest does not discover the directory, no application imports.</done>
</task>
<task type="auto">
<name>Task 5: Full-suite regression check</name>
<files></files>
<read_first>
- backend/pytest.ini
- backend/tests/test_logging.py (just created)
- backend/tests/test_rate_limiting.py (just created)
</read_first>
<action>
Run the full backend pytest suite to confirm: (a) the 13 new xfail stubs (5 + 8) all collect and report XFAIL, (b) zero pre-existing tests changed status, (c) no collection errors from the new files or the new load_tests/ package. Capture the XFAIL count and the PASSED count for the SUMMARY.md.
</action>
<verify>
<automated>cd backend &amp;&amp; pytest tests/ -v --no-header --tb=short 2>&amp;1 | tail -5 | grep -vE '^#' | grep -E 'passed|xfailed'</automated>
</verify>
<acceptance_criteria>
- `cd backend &amp;&amp; pytest tests/ --no-header --tb=line 2>&amp;1 | tail -1` shows a green summary line containing "passed" with no "failed" or "error".
- Total xfailed count increased by exactly 13 versus the baseline recorded in STATE.md (Phase 6.2: 344 passed, 1 pre-existing failure).
- Zero NEW failures: the pre-existing test_extract_docx failure remains the only failure, no other tests turn red.
- Plan SUMMARY records the exact passed/xfailed numbers from the run.
</acceptance_criteria>
<done>Full suite green-with-known-pre-existing-failure, 13 new XFAIL items present, ready for implementation plans 06-02/06-03/06-04 to promote them to PASS.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Untrusted client → reverse proxy | External HTTP clients can spoof headers; proxy must overwrite or strip them |
| Reverse proxy → FastAPI | Internal-network requests; XFF is trusted only when peer matches CIDR |
| Application code → PyPI registry | Third-party packages may be malicious typosquats; legitimacy gate required |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-06-01-01 | Tampering | structlog/locust packages from PyPI | mitigate | Blocking human checkpoint (Task 1) — verify PyPI URL, author, age, downloads before any requirements file modification |
| T-06-01-02 | Tampering | A1 assumption (slowapi key_func ordering) ships untested | mitigate | Dedicated xfail test (Task 3, test 7) — 06-04 must turn it green before applying the decorator to all routes; un-promoted test blocks phase gate |
| T-06-01-03 | Information Disclosure | Locust credentials hardcoded in version-controlled file | mitigate | Skeleton reads from env vars; .env file already in .gitignore; documented in RUNBOOK.md (06-06) |
| T-06-01-SC | Tampering | npm/pip/cargo installs | mitigate | Package legitimacy gate (Task 1) is `gate="blocking-human"`; cannot auto-advance |
</threat_model>
<verification>
After all five tasks:
- `cd backend && pytest tests/ -v --no-header` shows the existing pass count plus exactly 13 new XFAIL items (5 in test_logging.py + 8 in test_rate_limiting.py).
- `cd backend && pytest tests/ --collect-only 2>&1 | grep -c "load_tests"` returns 0.
- `python -c "import ast; ast.parse(open('backend/load_tests/locustfile.py').read())"` exits 0.
- `grep -cE "^from (backend|api|services|db|deps)" backend/load_tests/locustfile.py` returns 0.
- User approved the package legitimacy checkpoint (recorded in SUMMARY).
</verification>
<success_criteria>
- All 13 xfail stubs collected and shown as XFAIL.
- backend/load_tests/ package exists, pytest skips it, locustfile.py imports cleanly under Python's stdlib only.
- Package legitimacy human-verify checkpoint passed and recorded.
- Zero regressions: the only red test in the suite is the pre-existing test_extract_docx (carried over from 06.2).
</success_criteria>
<output>
Create `.planning/phases/06-performance-production-hardening/06-01-SUMMARY.md` when done. Include: passed/xfailed/failed counts from the final pytest run, the package legitimacy decision verbatim, and any open notes for downstream plans.
</output>