---
phase: 07.2-security-jti-claim-redis-access-token-revocation-inserted
plan: 01
type: execute
wave: 0
depends_on: []
files_modified:
  - backend/tests/test_auth_deps.py
  - backend/tests/test_task2_auth_service.py
  - backend/tests/test_auth_api.py
  - backend/tests/test_admin_api.py
autonomous: true
requirements:
  - CONCERNS:JTI-CLAIM
  - CONCERNS:JTI-REVOKE-REDIS
tags:
  - security
  - jwt
  - testing
  - nyquist

must_haves:
  truths:
    - "Every Phase 7.2 behavior has a failing test scaffold before any implementation runs"
    - "test_auth_deps.py test app mounts app.state.redis = FakeRedis() so Wave 1 NBF check does not crash existing 34+ auth-dep tests"
    - "Wave 0 stubs are skipped/xfailed (strict=False); zero existing tests regress"
  artifacts:
    - path: "backend/tests/test_auth_deps.py"
      provides: "FakeRedis attached to make_test_app(); NBF-check xfail stubs (iat<nbf→401, iat>nbf→pass, fail-open)"
      contains: "FakeRedis"
    - path: "backend/tests/test_task2_auth_service.py"
      provides: "test_create_access_token_includes_jti xfail stub"
      contains: "jti"
    - path: "backend/tests/test_auth_api.py"
      provides: "NBF-write xfail stubs for change_password, enable_totp, disable_totp"
      contains: "user_nbf"
    - path: "backend/tests/test_admin_api.py"
      provides: "NBF-write xfail stub for admin deactivation handler"
      contains: "user_nbf"
  key_links:
    - from: "backend/tests/test_auth_deps.py make_test_app()"
      to: "request.app.state.redis"
      via: "test_app.state.redis = FakeRedis() set before client yield"
      pattern: "app\\.state\\.redis\\s*=\\s*FakeRedis"
    - from: "Wave 0 stubs"
      to: "Wave 1/2 implementation"
      via: "pytest.xfail(strict=False) markers promote to xpass when code lands"
      pattern: "pytest\\.xfail|xfail.*strict=False"
---

<objective>
Pre-implementation scaffolding for Phase 7.2 (Nyquist Wave 0).

Purpose: Two things must be in place before any code in `services/auth.py`,
`deps/auth.py`, `api/auth.py`, or `api/admin.py` is touched:

1. A `FakeRedis` instance attached to `app.state.redis` on the minimal test
   app in `backend/tests/test_auth_deps.py`. Without this, Wave 1's NBF
   check in `get_current_user` will raise `AttributeError: 'State' object
   has no attribute 'redis'` on every existing auth-dep test (Pitfall 4
   from RESEARCH.md).

2. Failing/xfailed test stubs that pin every Phase 7.2 behavior listed in
   `07.2-VALIDATION.md` (JTI presence, NBF write on each security event,
   NBF check accept/reject, fail-open). Wave 1 and Wave 2 implementation
   then "flips" these from xfail to passing — verifiable test signal.

Output: 4 modified test files. Test suite still green (zero new failures);
new stubs report as XFAIL with `strict=False`.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-CONTEXT.md
@.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-RESEARCH.md
@.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-VALIDATION.md
@backend/tests/test_auth_deps.py
@backend/tests/test_auth_api.py
@backend/tests/test_task2_auth_service.py
@backend/tests/test_admin_api.py

<interfaces>
<!-- Reusable FakeRedis class already exists. DO NOT redefine. -->

Existing FakeRedis class — backend/tests/test_auth_api.py lines 47–86:
  - class FakeRedis with async get/set/incr/expire/close
  - Already used by authed_client fixture (line 102): fake_redis = FakeRedis(); app.state.redis = fake_redis
  - Supports `await redis.get(key)` returning the stored value (or None), and `await redis.set(key, value, ex=seconds)`
  - For Phase 7.2 NBF check, the .get() return type must be bytes-like (RESEARCH.md L210: `int(nbf_bytes.decode())`).
    The existing FakeRedis stores arbitrary value types; tests setting `user_nbf` must store bytes (e.g., `b"1700000000"`) for the .decode() call to work.

Existing make_test_app — backend/tests/test_auth_deps.py lines 22–37:
  - Builds a minimal FastAPI with /test/me and /test/admin endpoints
  - Does NOT currently set app.state.redis
  - Used by auth_client fixture (line 40–51)

Existing test_task2_auth_service.py — backend/tests/test_task2_auth_service.py:
  - test_create_access_token_jwt_format (line 28): existing positive test for create_access_token
  - test_decode_access_token_valid (line 35): decodes and asserts payload contents — TEMPLATE for jti assertion

Existing test_auth_api.py change-password test pattern:
  - Uses `authed_client` fixture (line 89) which sets `app.state.redis = FakeRedis()`
  - change_password handler at backend/api/auth.py:455 — already uses `request.cookies.get("refresh_token")` and writes audit log

Existing test_admin_api.py deactivation tests:
  - test_deactivate_user at line 191 — PATCH /api/admin/users/{id}/status with is_active=False
  - admin_client fixture (line 72) does NOT currently set app.state.redis — Wave 2 (Plan 03) write site uses request.app.state.redis, so admin tests need FakeRedis too (Task 4 below)

Phase 7.2 behaviors to stub (from 07.2-VALIDATION.md Per-Task Verification Map):
  - jti-claim: create_access_token payload contains "jti" key with UUID-format string value
  - test-fakeredis: get_current_user does not raise AttributeError when app.state.redis is FakeRedis
  - nbf-write-change-password: POST /api/auth/change-password sets user_nbf:{user_id} in Redis
  - nbf-write-enable-totp: POST /api/auth/totp/enable sets user_nbf:{user_id} in Redis
  - nbf-write-disable-totp: DELETE /api/auth/totp sets user_nbf:{user_id} in Redis
  - nbf-write-deactivation: PATCH /api/admin/users/{id}/status (is_active=False) sets user_nbf:{user_id} in Redis
  - nbf-check-reject: token with iat < nbf returns 401 "Session invalidated"
  - nbf-check-allow: token with iat > nbf returns 200
  - nbf-fail-open: redis.get raises Exception → request still succeeds (200)

Wave 0 stub convention (from STATE.md key decisions: "Wave 0 stubs: single-line body only"):
  - Body is ONLY `pytest.xfail("not implemented yet — Phase 7.2 Wave 1/2", strict=False)` or test marker `@pytest.mark.xfail(strict=False, reason="...")` with placeholder assertion
  - No real assertion logic in Wave 0 — that comes in Wave 1/2 when the same stubs are promoted by replacing the xfail with real assertions
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Mount FakeRedis on test_auth_deps test app and add NBF-check stubs</name>
  <files>backend/tests/test_auth_deps.py</files>
  <read_first>
    - backend/tests/test_auth_deps.py (current make_test_app + auth_client fixture — lines 22–51)
    - backend/tests/test_auth_api.py lines 47–86 (FakeRedis class definition — import this, do not redefine)
    - backend/tests/test_auth_api.py lines 89–123 (authed_client fixture — pattern for app.state.redis assignment)
    - .planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-RESEARCH.md "Pitfall 4" section (lines 296–301)
  </read_first>
  <behavior>
    - Test 1 (positive guard): existing test_get_current_user_returns_user still passes after FakeRedis is attached (no regression).
    - Test 2 (xfail): test_get_current_user_rejects_token_when_iat_before_user_nbf — pre-populates fake_redis["user_nbf:{uid}"] = b"<future_ts>", calls /test/me with a token whose iat < future_ts, expects 401 with detail containing "Session invalidated".
    - Test 3 (xfail): test_get_current_user_allows_token_when_iat_after_user_nbf — pre-populates fake_redis["user_nbf:{uid}"] = b"<past_ts>", calls /test/me with a token whose iat > past_ts, expects 200.
    - Test 4 (xfail): test_get_current_user_failopen_on_redis_error — sets app.state.redis to an object whose get() raises Exception, calls /test/me, expects 200 (fail-open per D-04).
    - All three new tests marked `@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 1 — NBF check not yet implemented")`.
  </behavior>
  <action>
    Import FakeRedis from tests.test_auth_api at top of file (use relative-style import: `from tests.test_auth_api import FakeRedis` — verify by reading existing `from ...` statements in other Phase tests for the correct module path; conftest uses `from main import app` so absolute `from tests.test_auth_api` is the project convention).

    Modify `make_test_app()` (line 22): before returning test_app, set `test_app.state.redis = FakeRedis()`. Keep the existing route definitions unchanged.

    Modify `auth_client` fixture (line 40): no signature change; ensure each invocation creates a fresh FakeRedis (because make_test_app() instantiates it). Add a fixture-scope comment noting that Phase 7.2 NBF check reads `request.app.state.redis`.

    Append three new test functions after the existing last test:
      - `test_get_current_user_rejects_token_when_iat_before_user_nbf(auth_client, db_session)`
      - `test_get_current_user_allows_token_when_iat_after_user_nbf(auth_client, db_session)`
      - `test_get_current_user_failopen_on_redis_error(auth_client, db_session)`

    Each new test:
      - decorated with `@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 1 — NBF check not yet implemented in get_current_user")`
      - decorated with `@pytest.mark.asyncio`
      - Uses _create_user helper (already present, line 54) to insert a user
      - Uses services.auth.create_access_token to mint the token
      - Body: 1–5 lines of placeholder assertions (e.g., `assert False, "stub"` after the prep), so when Wave 1 implementation lands and the assertion logic is filled in, the xfail flips to xpass. Keep Wave 0 body minimal per STATE.md convention.

    For Task 4 (failopen), define a small inline `class _BrokenRedis` whose `async def get(self, key)` raises `RuntimeError("simulated redis down")`. Set `auth_client._transport.app.state.redis = _BrokenRedis()` inside the test (or override via a fixture-local app instance) — this is acceptable in Wave 0 because Wave 1 implementation will read the assignment back out.

    Do NOT modify any existing test in the file. Do NOT add real assertion logic for the NBF check beyond placeholder stubs.
  </action>
  <verify>
    <automated>cd backend &amp;&amp; pytest tests/test_auth_deps.py -v</automated>
  </verify>
  <acceptance_criteria>
    - `cd backend &amp;&amp; pytest tests/test_auth_deps.py -v` exits 0
    - `grep -c "FakeRedis" backend/tests/test_auth_deps.py` returns &gt;= 2 (import + assignment)
    - `grep -c "test_app.state.redis" backend/tests/test_auth_deps.py` returns &gt;= 1
    - `grep -c "test_get_current_user_rejects_token_when_iat_before_user_nbf\|test_get_current_user_allows_token_when_iat_after_user_nbf\|test_get_current_user_failopen_on_redis_error" backend/tests/test_auth_deps.py` returns 3
    - `grep -E -c "pytest\\.mark\\.xfail|pytest\\.xfail" backend/tests/test_auth_deps.py` returns &gt;= 3
    - Test output lists at least 3 XFAIL results (or XPASS for any whose body already trivially passes — both acceptable in strict=False)
    - All previously-existing tests in test_auth_deps.py still PASS (no regressions in the 34+ baseline)
  </acceptance_criteria>
  <done>FakeRedis mounted on test app; three NBF-check stubs added with xfail(strict=False); zero existing test regressions.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Add JTI presence stub in test_task2_auth_service.py</name>
  <files>backend/tests/test_task2_auth_service.py</files>
  <read_first>
    - backend/tests/test_task2_auth_service.py lines 28–58 (existing create/decode_access_token tests — TEMPLATE)
    - backend/services/auth.py lines 86–117 (current create_access_token + decode_access_token implementations — note absence of jti)
  </read_first>
  <behavior>
    - Test (xfail): test_create_access_token_includes_jti_claim — calls `create_access_token("test-uid", "user")`, decodes via `decode_access_token`, asserts the returned payload dict has key `"jti"` whose value is a non-empty string parseable as a UUID (uuid.UUID(payload["jti"]) does not raise).
  </behavior>
  <action>
    Append a single new test function after the existing last create/decode_access_token test in the file:

      `def test_create_access_token_includes_jti_claim():` decorated with `@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 1 — jti claim not yet added to create_access_token")`.

    Body: import create_access_token and decode_access_token from services.auth (follow the import style of the surrounding tests — they use function-local imports per test). Mint a token. Decode it. Assert `"jti" in payload` and `uuid.UUID(payload["jti"])` succeeds. Import `uuid` at function scope (matches surrounding style) if not already module-level imported.

    Do NOT modify create_access_token in services/auth.py — that is Wave 1 (Plan 02). Do NOT modify any existing test.
  </action>
  <verify>
    <automated>cd backend &amp;&amp; pytest tests/test_task2_auth_service.py -v -k jti</automated>
  </verify>
  <acceptance_criteria>
    - `cd backend &amp;&amp; pytest tests/test_task2_auth_service.py -v -k jti` exits 0
    - `grep -c "test_create_access_token_includes_jti_claim" backend/tests/test_task2_auth_service.py` returns 1
    - `grep -E -c "xfail.*strict=False" backend/tests/test_task2_auth_service.py` returns &gt;= 1 (new stub)
    - The new test reports as XFAIL (or XPASS if the placeholder body trivially passes; both acceptable in strict=False)
    - All existing tests in test_task2_auth_service.py still PASS (zero regressions)
  </acceptance_criteria>
  <done>JTI presence test stub added with xfail(strict=False); existing tests unaffected.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 3: Add NBF-write stubs for change_password / enable_totp / disable_totp in test_auth_api.py</name>
  <files>backend/tests/test_auth_api.py</files>
  <read_first>
    - backend/tests/test_auth_api.py lines 45–123 (FakeRedis class + authed_client fixture — already mounts app.state.redis)
    - backend/api/auth.py lines 452–509 (change_password handler — Wave 2 will add the user_nbf write here)
    - backend/api/auth.py lines 548–601 (enable_totp handler — Wave 2 write site)
    - backend/api/auth.py lines 606–641 (disable_totp handler — Wave 2 write site)
    - Existing test that exercises change_password in test_auth_api.py (grep for `change-password` and copy its login+TOTP setup pattern)
  </read_first>
  <behavior>
    - Test (xfail) 1: test_change_password_writes_user_nbf_to_redis — completes register + login (using existing helpers), then POST /api/auth/change-password with valid current+new passwords. After response, asserts `await app.state.redis.get(f"user_nbf:{user_id}")` returns a non-None bytes value parseable as `int(...)`.
    - Test (xfail) 2: test_enable_totp_writes_user_nbf_to_redis — registers, logs in, calls /totp/setup, supplies a generated TOTP code via /totp/enable, asserts `user_nbf:{user_id}` is set in Redis.
    - Test (xfail) 3: test_disable_totp_writes_user_nbf_to_redis — same setup as Test 2 plus enable, then DELETE /api/auth/totp, asserts `user_nbf:{user_id}` is set in Redis.
  </behavior>
  <action>
    Append three new async test functions (after the existing change_password / TOTP tests) named exactly as listed in behavior above. All three:
      - decorated `@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 2 — user_nbf write not yet added to handler")`
      - decorated `@pytest.mark.asyncio`
      - Use the `authed_client` fixture (already mounts FakeRedis at app.state.redis)
      - Reuse the existing `_register` / `_login` helpers (lines 31–43) and the in-file TOTP test helpers (grep for existing TOTP-enable test pattern in the file; copy fixture/imports)
      - Body keeps Wave 0 minimal: prep + a single `assert False, "stub — Wave 2 will fill in"` or call `pytest.xfail("Phase 7.2 Wave 2 stub")` inside the body. The xfail strict=False allows both.

    Implementation hint (kept in test comments for Wave 2): after the API call returns success, fetch the user_id from the login response payload, then `nbf_bytes = await authed_client._transport.app.state.redis.get(f"user_nbf:{user_id}")` and assert `nbf_bytes is not None` plus `int(nbf_bytes.decode() if isinstance(nbf_bytes, (bytes, bytearray)) else nbf_bytes) > 0`.

    Do NOT modify any handler in backend/api/auth.py — that is Wave 2 (Plan 03). Do NOT modify the FakeRedis class.
  </action>
  <verify>
    <automated>cd backend &amp;&amp; pytest tests/test_auth_api.py -v -k "nbf or user_nbf"</automated>
  </verify>
  <acceptance_criteria>
    - `cd backend &amp;&amp; pytest tests/test_auth_api.py -v -k "nbf or user_nbf"` exits 0
    - `grep -c "test_change_password_writes_user_nbf_to_redis\|test_enable_totp_writes_user_nbf_to_redis\|test_disable_totp_writes_user_nbf_to_redis" backend/tests/test_auth_api.py` returns 3
    - `grep -E -c "user_nbf" backend/tests/test_auth_api.py` returns &gt;= 3 (one per new test)
    - `grep -E -c "xfail.*strict=False" backend/tests/test_auth_api.py` returns &gt;= 3 (new stubs)
    - All three new tests report as XFAIL (or XPASS — both acceptable in strict=False)
    - All existing tests in test_auth_api.py still PASS (zero regressions to register/login/totp/change-password baseline)
  </acceptance_criteria>
  <done>Three NBF-write stubs added covering change_password, enable_totp, disable_totp; existing auth API tests unaffected.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 4: Add NBF-write stub + FakeRedis for admin deactivation in test_admin_api.py</name>
  <files>backend/tests/test_admin_api.py</files>
  <read_first>
    - backend/tests/test_admin_api.py lines 71–84 (admin_client fixture — does NOT currently set app.state.redis)
    - backend/tests/test_admin_api.py lines 191–220 (test_deactivate_user — TEMPLATE for the new test)
    - backend/tests/test_auth_api.py lines 47–86 (FakeRedis class — import target)
    - backend/api/admin.py lines 340–380 (deactivation handler — Wave 2 write site)
  </read_first>
  <behavior>
    - admin_client fixture mounts `app.state.redis = FakeRedis()` so the deactivation handler (post-Wave 2) can call `request.app.state.redis.set(...)` without AttributeError.
    - Test (xfail): test_deactivate_user_writes_user_nbf_to_redis — creates a regular user via make_regular_user, sends PATCH /api/admin/users/{id}/status with `{"is_active": false}`, asserts `await app.state.redis.get(f"user_nbf:{user_id}")` returns a non-None bytes value.
    - Test (negative guard, can be passing immediately): test_activate_user_does_NOT_write_user_nbf — sends PATCH with `{"is_active": true}` on an already-deactivated user, asserts Redis key is NOT set. Marked `@pytest.mark.xfail(strict=False)` because the handler does not yet write the key in either branch — Wave 2 must preserve this invariant. (Mirrors RESEARCH.md Anti-pattern: "Do not write user_nbf for successful activation".)
  </behavior>
  <action>
    Import FakeRedis at top of file: `from tests.test_auth_api import FakeRedis` (matches project convention for cross-test imports).

    Modify `admin_client` fixture (line 72): before `async with AsyncClient(...)`, add `app.state.redis = FakeRedis()`. In the teardown after fixture yield (after `app.dependency_overrides.clear()`), add `app.state.redis = None` (mirrors authed_client teardown at test_auth_api.py:123).

    Append two new test functions at end of file (after the last existing test):
      - `test_deactivate_user_writes_user_nbf_to_redis(admin_client)` — async, marked `@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 2 — user_nbf write not yet added to admin deactivation handler")`. Body: unpack `client, _admin, session = admin_client`; create a regular user via `make_regular_user(session)`; PATCH /api/admin/users/{user.id}/status with `{"is_active": false}`; assert response 200; access `client._transport.app.state.redis` (the FakeRedis instance) and `await` its `.get(f"user_nbf:{user.id}")`; assert the value is not None. Placeholder body acceptable in Wave 0 (single `assert False, "stub"` line is fine).
      - `test_activate_user_does_not_write_user_nbf(admin_client)` — same xfail decorator; deactivate first, then PATCH with `{"is_active": true}`, assert `await app.state.redis.get(f"user_nbf:{user.id}")` is None at the end. Placeholder body acceptable.

    Verify no existing admin test regresses: pre-existing admin tests do not call `request.app.state.redis` so the mount is additive and safe.

    Do NOT modify any handler in backend/api/admin.py — that is Wave 2 (Plan 03).
  </action>
  <verify>
    <automated>cd backend &amp;&amp; pytest tests/test_admin_api.py -v</automated>
  </verify>
  <acceptance_criteria>
    - `cd backend &amp;&amp; pytest tests/test_admin_api.py -v` exits 0
    - `grep -c "from tests.test_auth_api import FakeRedis\|from tests\\.test_auth_api import FakeRedis" backend/tests/test_admin_api.py` returns &gt;= 1
    - `grep -c "app.state.redis = FakeRedis" backend/tests/test_admin_api.py` returns &gt;= 1
    - `grep -c "test_deactivate_user_writes_user_nbf_to_redis\|test_activate_user_does_not_write_user_nbf" backend/tests/test_admin_api.py` returns 2
    - `grep -E -c "xfail.*strict=False" backend/tests/test_admin_api.py` returns &gt;= 2
    - All previously-existing admin tests (including test_deactivate_user, test_reactivate_user) still PASS
    - Full pytest -v reports zero NEW failures vs baseline (XFAILs and XPASSes acceptable)
  </acceptance_criteria>
  <done>FakeRedis attached to admin_client fixture; deactivation NBF-write stubs (positive + negative-guard) added; existing admin tests unaffected.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| test → app.state | Test fixtures inject fake infrastructure (FakeRedis) onto the live FastAPI app; tests must not leak shared state between test runs |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-7.2-W0-01 | Tampering | test_admin_api.py admin_client fixture | mitigate | Reset `app.state.redis = None` in teardown after yield (mirrors authed_client convention at test_auth_api.py:123) — prevents stale FakeRedis from one test file bleeding into another |
| T-7.2-W0-02 | Repudiation | xfail stubs with `strict=False` | accept | Stubs are deliberately permissive in Wave 0 — they document expected behavior and act as failing gates when Wave 1/2 lands. The strict=False convention is established across this project (STATE.md). |
| T-7.2-W0-SC | Tampering | npm/pip/cargo installs | n/a | No packages installed in this plan — Phase 7.2 adds zero dependencies (RESEARCH.md "Package Legitimacy Audit" section is intentionally empty) |
</threat_model>

<verification>
- `cd backend && pytest tests/test_auth_deps.py tests/test_auth_api.py tests/test_task2_auth_service.py tests/test_admin_api.py -v` — zero new failures vs current baseline (373 passed in Phase 7.1)
- `grep -c "FakeRedis" backend/tests/test_auth_deps.py` returns >= 2 (import + assignment)
- `grep -c "FakeRedis" backend/tests/test_admin_api.py` returns >= 2 (import + assignment)
- All four new stub test sets report as XFAIL (or XPASS — both acceptable under strict=False)
</verification>

<success_criteria>
- Wave 0 stubs cover every behavior in 07.2-VALIDATION.md Per-Task Verification Map (jti-claim, test-fakeredis, nbf-write × 4, nbf-check × 2, nbf-fail-open)
- FakeRedis is reachable via `request.app.state.redis` in both test_auth_deps and test_admin_api fixtures
- Wave 1 (Plan 02) and Wave 2 (Plan 03) can promote each stub to a passing assertion by editing only the test body — no new fixtures, no new files
- Zero regressions: existing 373-test baseline from Phase 7.1 passes unchanged
</success_criteria>

<output>
Create `.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-01-SUMMARY.md` when done.

Required fields in SUMMARY: artifacts (4 test files), patterns_established (FakeRedis on test_auth_deps app.state, FakeRedis on admin_client fixture), patterns_to_avoid (do NOT redefine FakeRedis — import from tests.test_auth_api), provides (Wave 0 scaffolds for jti, NBF-check, NBF-write × 4, fail-open).
</output>
