phase: 85_mobile_menu_gate_overlap
All verification complete. Final report: **Phase 85 final verification pass — all green** (tasks 01–03 already complete; independently re-verified from scratch) - Verified fix in tree: `styles.css` `.auth-gate` z-index 500→15 + rewritten stacking comment (no `z-index: 500` left); `index.html`/`document.html` comment-only; `token-gate.js` docstring-only (logic byte-identical); `test_mobile_hamburger_nav.py` real-click conversion + new `test_anonymous_toggle_tappable_with_gate_up`; new `tests/unit/test_gate_header_stacking.py` (3 pins); `test_api_tokens.py` untouched - `uv run pytest` → 1717 passed, 1 warning (exit 0) - `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90%) - E2E in isolation: `test_mobile_hamburger_nav.py` **8 passed**; `test_api_tokens.py` **9 passed** (unchanged); `test_smoke.py` **3 passed**; `test_document_viewer.py` **7 passed** - `uv run ruff check . && uv run pyright` → clean / 0 errors - Live probe (375×812, anonymous, fresh server): on `/` and `/history.html` `elementFromPoint` at toggle → SVG `path`, never `#auth-gate`; real click opens menu (`aria-expanded=true`); exactly one visible nav link (Chat) + sign-in copy; Chat link topmost over gate; `#main` inert — criteria 1, 2, 3 confirmed directly - Criteria: (1) real-click menu on / + /history ✅ (2) dropdown above gate ✅ (3) anonymous contents + #main inert ✅ (4) admin byte-identical (phase-46 tests green) ✅ (5) doc-viewer gate under bar (CSS pin + doc-viewer suite) ✅ (6) full gate green ✅ (7) diff scoped to the 5 code files + new unit test + phase files, nothing in `app/` ✅ (8) commit + todo→complete move left to harness per executor rules (task files already in `complete/`) - Note: `.agents/remediation_plan.md` (untracked) is a pre-existing artifact of the earlier security audit — left untouched; a missing "N passed" line seen mid-pass was my own `-q`+addopts `-q` = `-qq` quirk, not a defect - Next pending phase: `86_history_page_width`
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
All verification complete. Final report:
|
||||
|
||||
**Phase 85 final verification pass — all green** (tasks 01–03 already complete; independently re-verified from scratch)
|
||||
- Verified fix in tree: `styles.css` `.auth-gate` z-index 500→15 + rewritten stacking comment (no `z-index: 500` left); `index.html`/`document.html` comment-only; `token-gate.js` docstring-only (logic byte-identical); `test_mobile_hamburger_nav.py` real-click conversion + new `test_anonymous_toggle_tappable_with_gate_up`; new `tests/unit/test_gate_header_stacking.py` (3 pins); `test_api_tokens.py` untouched
|
||||
- `uv run pytest` → 1717 passed, 1 warning (exit 0)
|
||||
- `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90%)
|
||||
- E2E in isolation: `test_mobile_hamburger_nav.py` **8 passed**; `test_api_tokens.py` **9 passed** (unchanged); `test_smoke.py` **3 passed**; `test_document_viewer.py` **7 passed**
|
||||
- `uv run ruff check . && uv run pyright` → clean / 0 errors
|
||||
- Live probe (375×812, anonymous, fresh server): on `/` and `/history.html` `elementFromPoint` at toggle → SVG `path`, never `#auth-gate`; real click opens menu (`aria-expanded=true`); exactly one visible nav link (Chat) + sign-in copy; Chat link topmost over gate; `#main` inert — criteria 1, 2, 3 confirmed directly
|
||||
- Criteria: (1) real-click menu on / + /history ✅ (2) dropdown above gate ✅ (3) anonymous contents + #main inert ✅ (4) admin byte-identical (phase-46 tests green) ✅ (5) doc-viewer gate under bar (CSS pin + doc-viewer suite) ✅ (6) full gate green ✅ (7) diff scoped to the 5 code files + new unit test + phase files, nothing in `app/` ✅ (8) commit + todo→complete move left to harness per executor rules (task files already in `complete/`)
|
||||
- Note: `.agents/remediation_plan.md` (untracked) is a pre-existing artifact of the earlier security audit — left untouched; a missing "N passed" line seen mid-pass was my own `-q`+addopts `-q` = `-qq` quirk, not a defect
|
||||
- Next pending phase: `86_history_page_width`
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
........................................................................ [ 4%]
|
||||
........................................................................ [ 8%]
|
||||
........................................................................ [ 12%]
|
||||
........................................................................ [ 16%]
|
||||
........................................................................ [ 20%]
|
||||
........................................................................ [ 25%]
|
||||
........................................................................ [ 29%]
|
||||
........................................................................ [ 33%]
|
||||
........................................................................ [ 37%]
|
||||
........................................................................ [ 41%]
|
||||
........................................................................ [ 46%]
|
||||
........................................................................ [ 50%]
|
||||
........................................................................ [ 54%]
|
||||
........................................................................ [ 58%]
|
||||
........................................................................ [ 62%]
|
||||
........................................................................ [ 67%]
|
||||
........................................................................ [ 71%]
|
||||
........................................................................ [ 75%]
|
||||
........................................................................ [ 79%]
|
||||
........................................................................ [ 83%]
|
||||
........................................................................ [ 88%]
|
||||
........................................................................ [ 92%]
|
||||
........................................................................ [ 96%]
|
||||
............................................................. [100%]
|
||||
=============================== warnings summary ===============================
|
||||
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||
from starlette.testclient import TestClient as TestClient # noqa
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
================================ tests coverage ================================
|
||||
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||
|
||||
Name Stmts Miss Cover
|
||||
--------------------------------------------------
|
||||
app/__init__.py 1 0 100%
|
||||
app/api/__init__.py 0 0 100%
|
||||
app/api/auth.py 52 0 100%
|
||||
app/api/chat.py 178 0 100%
|
||||
app/api/chats.py 110 0 100%
|
||||
app/api/config.py 7 0 100%
|
||||
app/api/doc_drafts.py 94 0 100%
|
||||
app/api/docs.py 50 0 100%
|
||||
app/api/git_sources.py 212 0 100%
|
||||
app/api/health.py 10 0 100%
|
||||
app/api/steering.py 42 0 100%
|
||||
app/api/suggestions.py 29 0 100%
|
||||
app/api/sync.py 98 0 100%
|
||||
app/api/tokens.py 28 0 100%
|
||||
app/config.py 141 0 100%
|
||||
app/core/__init__.py 0 0 100%
|
||||
app/core/auth.py 45 0 100%
|
||||
app/core/caching.py 108 0 100%
|
||||
app/core/debugging.py 29 2 93%
|
||||
app/core/docs_push.py 39 0 100%
|
||||
app/core/errors.py 5 0 100%
|
||||
app/core/logging.py 13 0 100%
|
||||
app/core/rate_limit.py 44 0 100%
|
||||
app/core/security_headers.py 19 0 100%
|
||||
app/core/tokens.py 33 0 100%
|
||||
app/db.py 21 0 100%
|
||||
app/main.py 64 0 100%
|
||||
app/models.py 94 0 100%
|
||||
app/rag/__init__.py 0 0 100%
|
||||
app/rag/agent.py 222 0 100%
|
||||
app/rag/archive_upload.py 128 0 100%
|
||||
app/rag/chunker.py 206 4 98%
|
||||
app/rag/git_sources.py 14 0 100%
|
||||
app/rag/importer.py 180 3 98%
|
||||
app/rag/llm.py 217 0 100%
|
||||
app/rag/overview.py 71 0 100%
|
||||
app/rag/prompts.py 88 0 100%
|
||||
app/rag/retriever.py 150 3 98%
|
||||
app/rag/scaffolding.py 55 0 100%
|
||||
app/rag/source_removal.py 41 0 100%
|
||||
app/rag/sources_meta.py 16 0 100%
|
||||
app/rag/suggestions.py 27 0 100%
|
||||
app/rag/summarizer.py 24 0 100%
|
||||
app/schemas.py 232 0 100%
|
||||
--------------------------------------------------
|
||||
TOTAL 3237 12 99%
|
||||
coverage gate: app/ 99% (>90%) OK
|
||||
All checks passed!
|
||||
0 errors, 0 warnings, 0 informations
|
||||
validation OK
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
Task 01 complete — all green.
|
||||
|
||||
**Implemented**
|
||||
- `frontend/assets/styles.css`: `.auth-gate` `z-index: 500 → 15` (the one-value fix, A1) + stacking comment rewritten to the phase-85 contract (15 gate < 20 header < 21 dropdown; skip-link 100 / 1000-family overlays unaffected; `#main` lock is JS, not z-order)
|
||||
- `frontend/index.html` / `frontend/document.html`: gate-section comments reworded (header stays reachable; `#main` stays inert) — markup byte-identical, comment-only
|
||||
- `frontend/assets/token-gate.js`: docstring sweep only — "only interactive surface" → "only CONTENT-level interactive surface" + phase-85 note; logic byte-identical
|
||||
- New `tests/unit/test_gate_header_stacking.py`: 3 source-level z-order pins (gate=15, header=20 with gate<header, mobile `.app-nav` keeps 21>15) + the gate still covers content (fixed/inset:0, `[hidden]` display:none)
|
||||
|
||||
**Test / lint results**
|
||||
- `uv run pytest tests/unit/test_gate_header_stacking.py -v` → 3 passed
|
||||
- `uv run pytest --cov=app --cov-report=term-missing` → 1717 passed, `app/` TOTAL **99%** (>90% ✓)
|
||||
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings
|
||||
- `git diff --stat` limited to the 4 frontend files (CSS value + comments only) + the new test; `grep "z-index: 500"` no longer matches anything
|
||||
|
||||
**Decisions**: the dropdown's `z-index: 21` lives on the base `.app-nav` rule (shared with `.is-open`), so the pin targets `.app-nav` in the ≤640px block — same as the existing phase-46 pin; value 15 kept as owner-decided.
|
||||
|
||||
**Next pending task**: `02_e2e_pin_updates.md` (same phase — real-click conversion + TODO-regression E2E pin).
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
........................................................................ [ 4%]
|
||||
........................................................................ [ 8%]
|
||||
........................................................................ [ 12%]
|
||||
........................................................................ [ 16%]
|
||||
........................................................................ [ 20%]
|
||||
........................................................................ [ 25%]
|
||||
........................................................................ [ 29%]
|
||||
........................................................................ [ 33%]
|
||||
........................................................................ [ 37%]
|
||||
........................................................................ [ 41%]
|
||||
........................................................................ [ 46%]
|
||||
........................................................................ [ 50%]
|
||||
........................................................................ [ 54%]
|
||||
........................................................................ [ 58%]
|
||||
........................................................................ [ 62%]
|
||||
........................................................................ [ 67%]
|
||||
........................................................................ [ 71%]
|
||||
........................................................................ [ 75%]
|
||||
........................................................................ [ 79%]
|
||||
........................................................................ [ 83%]
|
||||
........................................................................ [ 88%]
|
||||
........................................................................ [ 92%]
|
||||
........................................................................ [ 96%]
|
||||
............................................................. [100%]
|
||||
=============================== warnings summary ===============================
|
||||
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||
from starlette.testclient import TestClient as TestClient # noqa
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
================================ tests coverage ================================
|
||||
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||
|
||||
Name Stmts Miss Cover
|
||||
--------------------------------------------------
|
||||
app/__init__.py 1 0 100%
|
||||
app/api/__init__.py 0 0 100%
|
||||
app/api/auth.py 52 0 100%
|
||||
app/api/chat.py 178 0 100%
|
||||
app/api/chats.py 110 0 100%
|
||||
app/api/config.py 7 0 100%
|
||||
app/api/doc_drafts.py 94 0 100%
|
||||
app/api/docs.py 50 0 100%
|
||||
app/api/git_sources.py 212 0 100%
|
||||
app/api/health.py 10 0 100%
|
||||
app/api/steering.py 42 0 100%
|
||||
app/api/suggestions.py 29 0 100%
|
||||
app/api/sync.py 98 0 100%
|
||||
app/api/tokens.py 28 0 100%
|
||||
app/config.py 141 0 100%
|
||||
app/core/__init__.py 0 0 100%
|
||||
app/core/auth.py 45 0 100%
|
||||
app/core/caching.py 108 0 100%
|
||||
app/core/debugging.py 29 2 93%
|
||||
app/core/docs_push.py 39 0 100%
|
||||
app/core/errors.py 5 0 100%
|
||||
app/core/logging.py 13 0 100%
|
||||
app/core/rate_limit.py 44 0 100%
|
||||
app/core/security_headers.py 19 0 100%
|
||||
app/core/tokens.py 33 0 100%
|
||||
app/db.py 21 0 100%
|
||||
app/main.py 64 0 100%
|
||||
app/models.py 94 0 100%
|
||||
app/rag/__init__.py 0 0 100%
|
||||
app/rag/agent.py 222 0 100%
|
||||
app/rag/archive_upload.py 128 0 100%
|
||||
app/rag/chunker.py 206 4 98%
|
||||
app/rag/git_sources.py 14 0 100%
|
||||
app/rag/importer.py 180 3 98%
|
||||
app/rag/llm.py 217 0 100%
|
||||
app/rag/overview.py 71 0 100%
|
||||
app/rag/prompts.py 88 0 100%
|
||||
app/rag/retriever.py 150 3 98%
|
||||
app/rag/scaffolding.py 55 0 100%
|
||||
app/rag/source_removal.py 41 0 100%
|
||||
app/rag/sources_meta.py 16 0 100%
|
||||
app/rag/suggestions.py 27 0 100%
|
||||
app/rag/summarizer.py 24 0 100%
|
||||
app/schemas.py 232 0 100%
|
||||
--------------------------------------------------
|
||||
TOTAL 3237 12 99%
|
||||
coverage gate: app/ 99% (>90%) OK
|
||||
All checks passed!
|
||||
0 errors, 0 warnings, 0 informations
|
||||
validation OK
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
All green. Task 02 is complete.
|
||||
|
||||
**Implemented**
|
||||
- `tests/e2e/test_mobile_hamburger_nav.py`: retired `_js_open_menu` (JS-dispatch drive) — anonymous tests now use the real-click helper `_open_menu` (`page.click("#nav-toggle")`), docstring rewritten to the phase-85 contract (gate z 15 below header z 20)
|
||||
- New regression pin `test_anonymous_toggle_tappable_with_gate_up`: with the gate up (`#main` inert), `elementFromPoint` at the toggle center resolves to the toggle/SVG child, never `#auth-gate`; real click opens the menu above the gate (Chat link visible + topmost at its center, gate still up); Esc settles closed
|
||||
- Module docstring: phase-85 stacking paragraph + numbered mapping entry; all phase-79 "intercepted by the gate" test docstrings/comments swept (assertions kept exactly: one visible "Chat" link, Esc refocus, outside-click at (10,780) doesn't close, 180ms pins)
|
||||
- `test_api_tokens.py`: verified no hamburger pins (grep clean); run unchanged, green
|
||||
|
||||
**Results**
|
||||
- `uv run pytest tests/e2e/test_mobile_hamburger_nav.py -v --no-cov` → 8 passed (isolation)
|
||||
- `uv run pytest tests/e2e/test_api_tokens.py -v --no-cov` → 9 passed, file unchanged (isolation)
|
||||
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → 3 passed
|
||||
- `uv run pytest --cov=app --cov-report=term-missing` → 1717 passed, coverage 99% (>90%)
|
||||
- `uv run ruff check . && uv run pyright` → clean
|
||||
- Criterion grep `querySelector('#nav-toggle').click()` → no matches; my diff touches only the one E2E file (frontend changes in tree are task-01's, committed atomically by the harness at phase end)
|
||||
|
||||
**Deviations** — none; the JS helper was folded into `_open_menu` rather than duplicated (identical real-click body + same 3 assertions).
|
||||
|
||||
**Next pending task:** `.agents/phases/todo/85_mobile_menu_gate_overlap/03_verify_and_commit.md`
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
........................................................................ [ 4%]
|
||||
........................................................................ [ 8%]
|
||||
........................................................................ [ 12%]
|
||||
........................................................................ [ 16%]
|
||||
........................................................................ [ 20%]
|
||||
........................................................................ [ 25%]
|
||||
........................................................................ [ 29%]
|
||||
........................................................................ [ 33%]
|
||||
........................................................................ [ 37%]
|
||||
........................................................................ [ 41%]
|
||||
........................................................................ [ 46%]
|
||||
........................................................................ [ 50%]
|
||||
........................................................................ [ 54%]
|
||||
........................................................................ [ 58%]
|
||||
........................................................................ [ 62%]
|
||||
........................................................................ [ 67%]
|
||||
........................................................................ [ 71%]
|
||||
........................................................................ [ 75%]
|
||||
........................................................................ [ 79%]
|
||||
........................................................................ [ 83%]
|
||||
........................................................................ [ 88%]
|
||||
........................................................................ [ 92%]
|
||||
........................................................................ [ 96%]
|
||||
............................................................. [100%]
|
||||
=============================== warnings summary ===============================
|
||||
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||
from starlette.testclient import TestClient as TestClient # noqa
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
================================ tests coverage ================================
|
||||
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||
|
||||
Name Stmts Miss Cover
|
||||
--------------------------------------------------
|
||||
app/__init__.py 1 0 100%
|
||||
app/api/__init__.py 0 0 100%
|
||||
app/api/auth.py 52 0 100%
|
||||
app/api/chat.py 178 0 100%
|
||||
app/api/chats.py 110 0 100%
|
||||
app/api/config.py 7 0 100%
|
||||
app/api/doc_drafts.py 94 0 100%
|
||||
app/api/docs.py 50 0 100%
|
||||
app/api/git_sources.py 212 0 100%
|
||||
app/api/health.py 10 0 100%
|
||||
app/api/steering.py 42 0 100%
|
||||
app/api/suggestions.py 29 0 100%
|
||||
app/api/sync.py 98 0 100%
|
||||
app/api/tokens.py 28 0 100%
|
||||
app/config.py 141 0 100%
|
||||
app/core/__init__.py 0 0 100%
|
||||
app/core/auth.py 45 0 100%
|
||||
app/core/caching.py 108 0 100%
|
||||
app/core/debugging.py 29 2 93%
|
||||
app/core/docs_push.py 39 0 100%
|
||||
app/core/errors.py 5 0 100%
|
||||
app/core/logging.py 13 0 100%
|
||||
app/core/rate_limit.py 44 0 100%
|
||||
app/core/security_headers.py 19 0 100%
|
||||
app/core/tokens.py 33 0 100%
|
||||
app/db.py 21 0 100%
|
||||
app/main.py 64 0 100%
|
||||
app/models.py 94 0 100%
|
||||
app/rag/__init__.py 0 0 100%
|
||||
app/rag/agent.py 222 0 100%
|
||||
app/rag/archive_upload.py 128 0 100%
|
||||
app/rag/chunker.py 206 4 98%
|
||||
app/rag/git_sources.py 14 0 100%
|
||||
app/rag/importer.py 180 3 98%
|
||||
app/rag/llm.py 217 0 100%
|
||||
app/rag/overview.py 71 0 100%
|
||||
app/rag/prompts.py 88 0 100%
|
||||
app/rag/retriever.py 150 3 98%
|
||||
app/rag/scaffolding.py 55 0 100%
|
||||
app/rag/source_removal.py 41 0 100%
|
||||
app/rag/sources_meta.py 16 0 100%
|
||||
app/rag/suggestions.py 27 0 100%
|
||||
app/rag/summarizer.py 24 0 100%
|
||||
app/schemas.py 232 0 100%
|
||||
--------------------------------------------------
|
||||
TOTAL 3237 12 99%
|
||||
coverage gate: app/ 99% (>90%) OK
|
||||
All checks passed!
|
||||
0 errors, 0 warnings, 0 informations
|
||||
validation OK
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
Screenshot confirms the fix visually: the anonymous menu (Chat + Sign in) renders **above** the gate card. Phase 85 is fully gated.
|
||||
|
||||
**Report — Phase 85, task 03 (verify & commit)**
|
||||
|
||||
- Full gate green: `uv run pytest` → 1717 passed; `--cov=app` → TOTAL **99%** (>90% floor, no `app/` change)
|
||||
- E2E in isolation: `test_mobile_hamburger_nav.py` → 8 passed (real-click contract + new `elementFromPoint` pin); `test_api_tokens.py` → 9 passed, **file unchanged** (phase-79 lock contract intact); `test_smoke.py` → 3 passed; `test_document_viewer.py` → 7 passed
|
||||
- `uv run ruff check . && uv run pyright` → clean (0 errors/warnings)
|
||||
- Live check (dev server :8000, 375×812, not signed in): 12/12 checks passed — real tap opens the menu above the gate (Chat + sign-in visible, token card behind/below), `elementFromPoint` at toggle resolves to the SVG (never `#auth-gate`), wrong-token error line + menu independence (Esc closes, gate stays), admin menu unchanged; log + 2 screenshots in `.agents/reports/85_mobile_menu_gate_overlap/`
|
||||
- Working tree verified: only `styles.css` (z 500→15 + comment), `index.html`/`document.html` (comments only), `token-gate.js` (docstring only), `test_mobile_hamburger_nav.py`, new `tests/unit/test_gate_header_stacking.py` + phase files — nothing in `app/`, `pyproject.toml`, `uv.lock`
|
||||
- Deviation (per executor rules, overrides task steps 3–4): **no `git commit` made and phase dir not moved** — all changes left in the working tree for the harness's single atomic phase commit and the `todo/ → complete/` move
|
||||
- Next pending task: none in this phase (task 03 was the last); following phases: `86_history_page_width`, `87_big_read_progress`
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
........................................................................ [ 4%]
|
||||
........................................................................ [ 8%]
|
||||
........................................................................ [ 12%]
|
||||
........................................................................ [ 16%]
|
||||
........................................................................ [ 20%]
|
||||
........................................................................ [ 25%]
|
||||
........................................................................ [ 29%]
|
||||
........................................................................ [ 33%]
|
||||
........................................................................ [ 37%]
|
||||
........................................................................ [ 41%]
|
||||
........................................................................ [ 46%]
|
||||
........................................................................ [ 50%]
|
||||
........................................................................ [ 54%]
|
||||
........................................................................ [ 58%]
|
||||
........................................................................ [ 62%]
|
||||
........................................................................ [ 67%]
|
||||
........................................................................ [ 71%]
|
||||
........................................................................ [ 75%]
|
||||
........................................................................ [ 79%]
|
||||
........................................................................ [ 83%]
|
||||
........................................................................ [ 88%]
|
||||
........................................................................ [ 92%]
|
||||
........................................................................ [ 96%]
|
||||
............................................................. [100%]
|
||||
=============================== warnings summary ===============================
|
||||
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||
from starlette.testclient import TestClient as TestClient # noqa
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
================================ tests coverage ================================
|
||||
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||
|
||||
Name Stmts Miss Cover
|
||||
--------------------------------------------------
|
||||
app/__init__.py 1 0 100%
|
||||
app/api/__init__.py 0 0 100%
|
||||
app/api/auth.py 52 0 100%
|
||||
app/api/chat.py 178 0 100%
|
||||
app/api/chats.py 110 0 100%
|
||||
app/api/config.py 7 0 100%
|
||||
app/api/doc_drafts.py 94 0 100%
|
||||
app/api/docs.py 50 0 100%
|
||||
app/api/git_sources.py 212 0 100%
|
||||
app/api/health.py 10 0 100%
|
||||
app/api/steering.py 42 0 100%
|
||||
app/api/suggestions.py 29 0 100%
|
||||
app/api/sync.py 98 0 100%
|
||||
app/api/tokens.py 28 0 100%
|
||||
app/config.py 141 0 100%
|
||||
app/core/__init__.py 0 0 100%
|
||||
app/core/auth.py 45 0 100%
|
||||
app/core/caching.py 108 0 100%
|
||||
app/core/debugging.py 29 2 93%
|
||||
app/core/docs_push.py 39 0 100%
|
||||
app/core/errors.py 5 0 100%
|
||||
app/core/logging.py 13 0 100%
|
||||
app/core/rate_limit.py 44 0 100%
|
||||
app/core/security_headers.py 19 0 100%
|
||||
app/core/tokens.py 33 0 100%
|
||||
app/db.py 21 0 100%
|
||||
app/main.py 64 0 100%
|
||||
app/models.py 94 0 100%
|
||||
app/rag/__init__.py 0 0 100%
|
||||
app/rag/agent.py 222 0 100%
|
||||
app/rag/archive_upload.py 128 0 100%
|
||||
app/rag/chunker.py 206 4 98%
|
||||
app/rag/git_sources.py 14 0 100%
|
||||
app/rag/importer.py 180 3 98%
|
||||
app/rag/llm.py 217 0 100%
|
||||
app/rag/overview.py 71 0 100%
|
||||
app/rag/prompts.py 88 0 100%
|
||||
app/rag/retriever.py 150 3 98%
|
||||
app/rag/scaffolding.py 55 0 100%
|
||||
app/rag/source_removal.py 41 0 100%
|
||||
app/rag/sources_meta.py 16 0 100%
|
||||
app/rag/suggestions.py 27 0 100%
|
||||
app/rag/summarizer.py 24 0 100%
|
||||
app/schemas.py 232 0 100%
|
||||
--------------------------------------------------
|
||||
TOTAL 3237 12 99%
|
||||
coverage gate: app/ 99% (>90%) OK
|
||||
All checks passed!
|
||||
0 errors, 0 warnings, 0 informations
|
||||
validation OK
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
+16
-11
@@ -1631,23 +1631,28 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
/* Phase 79 (task 05): the in-app token gate — the gate surface of the
|
||||
two token-only pages (the shell + the document viewer, one shared
|
||||
.auth-gate markup each). A body-level FIXED overlay (the
|
||||
body-level doc-modal precedent): while visible it is the ONLY
|
||||
interactive surface — assets/token-gate.js locks the root (#main,
|
||||
inert) so focus and keyboard traversal stay inside the gate
|
||||
(WCAG, the inert-pair contract).
|
||||
Stacking: z-index 500 — above the app content (the sticky header is
|
||||
20, the skip-link 100) but BELOW the doc-modal (1000), which can
|
||||
only be opened from the unlocked app anyway. Solid --bg: the canvas
|
||||
+ grid texture live on <html> (the body stays transparent), so the
|
||||
overlay reads as the app's own surface — no blur (the phase-08
|
||||
perf anchor).
|
||||
body-level doc-modal precedent): while visible it is the only
|
||||
CONTENT-level interactive surface — assets/token-gate.js locks the
|
||||
root (#main, inert) so focus and keyboard traversal stay inside
|
||||
the gate (WCAG, the inert-pair contract).
|
||||
Stacking (phase 85, TODO.md L3): z-index 15 — ABOVE all app
|
||||
content (static) but BELOW the sticky header (20) and its mobile
|
||||
dropdown (21, inside the header's stacking context): the
|
||||
unauthenticated visitor keeps a reachable hamburger + menu while
|
||||
the gate still covers and locks the app content (#main stays
|
||||
inert — the lock is JS, not z-order — phase 79). The skip-link
|
||||
(100) and the 1000-family overlays (doc-modal, sync backdrop) are
|
||||
unaffected — all above 15; the doc-modal can only be opened from
|
||||
the unlocked app anyway. Solid --bg: the canvas + grid texture
|
||||
live on <html> (the body stays transparent), so the overlay reads
|
||||
as the app's own surface — no blur (the phase-08 perf anchor).
|
||||
The centered card reuses the #sources-gate visual language
|
||||
(surface card + hairline + glyph + heading + sub + action) in a
|
||||
tighter column. */
|
||||
.auth-gate {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 500;
|
||||
z-index: 15;
|
||||
display: flex; /* the card is the only in-flow child — margin: auto centers it */
|
||||
overflow-y: auto; /* short viewports: the card scrolls fully into view */
|
||||
background: var(--bg);
|
||||
|
||||
@@ -193,9 +193,13 @@ export async function mountGate(lockRoot, onAuthed) {
|
||||
|
||||
/* Anonymous: reveal the gate (drop hidden AND inert — the inert-pair
|
||||
contract), lock the app, focus the token input. The gate is the
|
||||
only interactive surface while visible: the lock root is inert,
|
||||
so Tab never reaches the locked app (the composer on the shell,
|
||||
the content on the viewer). */
|
||||
only CONTENT-level interactive surface while visible: the lock
|
||||
root is inert, so Tab never reaches the locked app (the composer
|
||||
on the shell, the content on the viewer). The header was never
|
||||
the lock root (and is never inert) — it was only visually
|
||||
covered pre-phase-85: the gate's z-index (15) now sits below the
|
||||
header (20), so the mobile hamburger + menu stay reachable while
|
||||
the app content stays inert-locked (TODO.md L3). */
|
||||
if (gate) {
|
||||
gate.hidden = false;
|
||||
gate.inert = false;
|
||||
|
||||
@@ -162,7 +162,10 @@
|
||||
root; the existing boot sequence (whoami → load content) runs
|
||||
as onAuthed — an admin (or a validly cached token user) gets
|
||||
it immediately, the gate never shows. SHIPS hidden + inert
|
||||
(the phase-16 ship-hidden pattern). -->
|
||||
(the phase-16 ship-hidden pattern). Phase 85 (TODO.md L3):
|
||||
same stacking as the shell gate — the overlay (z-index 15)
|
||||
sits below the viewer's sticky bar (20), so the hamburger +
|
||||
menu stay reachable while #main stays inert-locked. -->
|
||||
<section class="auth-gate" id="doc-auth-gate" hidden inert aria-labelledby="doc-auth-gate-title">
|
||||
<div class="auth-gate-card">
|
||||
<div class="auth-gate-glyph" aria-hidden="true">
|
||||
|
||||
+7
-3
@@ -896,9 +896,13 @@
|
||||
|
||||
<!-- Phase 79 (task 05): the in-app token gate — a body-level
|
||||
position:fixed; inset:0 overlay (the body-level doc-modal
|
||||
precedent) that is the ONLY interactive surface while visible:
|
||||
assets/token-gate.js locks #main (inert) so focus and keyboard
|
||||
traversal stay in the gate (WCAG, the inert-pair contract). It
|
||||
precedent) that is the only CONTENT-level interactive surface
|
||||
while visible: assets/token-gate.js locks #main (inert) so
|
||||
focus and keyboard traversal stay in the gate (WCAG, the
|
||||
inert-pair contract). Phase 85 (TODO.md L3): the overlay sits
|
||||
BELOW the sticky header (z-index 15 < 20) — it covers #main's
|
||||
content region visually, but the header (hamburger + menu)
|
||||
stays reachable for the unauthenticated visitor. It
|
||||
SHIPS hidden + inert (the phase-16 ship-hidden pattern) — an
|
||||
authenticated boot (admin, or a valid cached token re-authed
|
||||
silently at mount) never shows it for a frame. The gate is the
|
||||
|
||||
@@ -15,6 +15,14 @@ visibility contract intact INSIDE the menu; at >640px the bar is
|
||||
byte-identical to pre-phase-46 (hamburger absent, inline pills). No
|
||||
document is ever needed — the suite exercises the shared header only.
|
||||
|
||||
Phase 85 (TODO.md L3): the anonymous visitor meets the token gate —
|
||||
but the gate (z-index 15) now sits BELOW the sticky header (z 20), so
|
||||
a REAL tap on ``#nav-toggle`` reaches the toggle for anonymous
|
||||
visitors exactly as for admin: every test in this suite drives the
|
||||
menu with real clicks (the phase-79 JS-dispatched workaround for the
|
||||
gate-overlay interception is retired — the gate covers only ``#main``,
|
||||
which stays ``inert``; it no longer covers the header).
|
||||
|
||||
The conftest ``page`` fixture is 1280×800, so the mobile tests create
|
||||
fresh 375×812 pages via the session ``browser`` fixture (one page per
|
||||
test; the reduced-motion test gets its own context).
|
||||
@@ -29,20 +37,24 @@ Test → story mapping (Playwright Mapping Rule):
|
||||
2. ``test_anonymous_menu_contents`` — anonymous at 375px: the menu
|
||||
shows EXACTLY one visible link ("Chat"); the three admin-only links
|
||||
stay ``hidden`` inside the menu; the open flips ``aria-expanded``.
|
||||
3. ``test_admin_menu_contents`` — admin at 375px: the menu shows all
|
||||
3. ``test_anonymous_toggle_tappable_with_gate_up`` — the TODO.md L3
|
||||
regression pin: anonymous toggle tappable with the gate up —
|
||||
``elementFromPoint`` at the toggle center never ``#auth-gate``,
|
||||
real click opens the menu above the gate.
|
||||
4. ``test_admin_menu_contents`` — admin at 375px: the menu shows all
|
||||
four links (the whoami reveal works inside the menu).
|
||||
4. ``test_link_click_navigates_and_closes`` — admin at 375px: clicking
|
||||
5. ``test_link_click_navigates_and_closes`` — admin at 375px: clicking
|
||||
"RAG" navigates to /sources.html and the menu on the arrival
|
||||
page ships closed.
|
||||
5. ``test_esc_and_outside_close`` — Esc closes AND returns focus to the
|
||||
6. ``test_esc_and_outside_close`` — Esc closes AND returns focus to the
|
||||
toggle; an outside click does NOT close (accepted — see the test
|
||||
docstring for why).
|
||||
6. ``test_animation_and_reduced_motion`` — motion allowed: the
|
||||
7. ``test_animation_and_reduced_motion`` — motion allowed: the
|
||||
180ms opacity/transform transition pair is live and the open flips
|
||||
class + aria; ``reducedMotion: "reduce"``: no transition in EITHER
|
||||
state (the .is-open state included — the specificity trap) and
|
||||
open/close still works.
|
||||
7. ``test_desktop_unchanged`` — 1280×800 regression: the hamburger is
|
||||
8. ``test_desktop_unchanged`` — 1280×800 regression: the hamburger is
|
||||
``display: none`` and the inline nav renders in the bar exactly as
|
||||
before (admin: all four links, all inside the header band).
|
||||
"""
|
||||
@@ -105,6 +117,13 @@ def _visible_nav_links(page: Page) -> list[str]:
|
||||
|
||||
|
||||
def _open_menu(page: Page) -> None:
|
||||
"""Open the menu with a REAL click on the toggle.
|
||||
|
||||
Phase 85 (TODO.md L3): the gate (z-index 15) sits BELOW the sticky
|
||||
header (z 20), so a real tap reaches the toggle for anonymous
|
||||
visitors too — the phase-79 JS-dispatched workaround (the gate then
|
||||
covered the header at z 500 and intercepted the click) is retired,
|
||||
and every test in this suite uses this one real-click helper."""
|
||||
page.click("#nav-toggle")
|
||||
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
|
||||
# to_have_class(string) is an EXACT match on the class attribute —
|
||||
@@ -113,20 +132,6 @@ def _open_menu(page: Page) -> None:
|
||||
expect(page.locator("#app-nav")).to_have_css("opacity", "1")
|
||||
|
||||
|
||||
def _js_open_menu(page: Page) -> None:
|
||||
"""Phase 79 (task 05): the in-app token gate is a full-viewport
|
||||
overlay for ANONYMOUS visitors — it physically covers the header,
|
||||
so a real click on #nav-toggle is intercepted by the gate (the gate
|
||||
is the only interactive surface; the header is locked out with the
|
||||
rest of the page). The binding is identical, so the menu contract
|
||||
is driven programmatically: a JS-dispatched click runs the exact
|
||||
same listener a real click would."""
|
||||
page.evaluate("() => document.querySelector('#nav-toggle').click()")
|
||||
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
|
||||
expect(page.locator("#app-nav")).to_have_class(re.compile(r"\bis-open\b"))
|
||||
expect(page.locator("#app-nav")).to_have_css("opacity", "1")
|
||||
|
||||
|
||||
def _assert_menu_closed(page: Page) -> None:
|
||||
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "false")
|
||||
assert "is-open" not in (page.locator("#app-nav").get_attribute("class") or ""), (
|
||||
@@ -205,17 +210,17 @@ def test_anonymous_menu_contents(
|
||||
preserved by reusing the same <nav> element); opening flips
|
||||
aria-expanded true.
|
||||
|
||||
Phase 79 (task 05): the anonymous visitor meets the token gate — a
|
||||
full-viewport overlay that covers the header — so the toggle is
|
||||
driven programmatically (the binding is identical; see
|
||||
_js_open_menu)."""
|
||||
Phase 85 (TODO.md L3): the anonymous visitor meets the token gate
|
||||
— but the gate (z 15) now sits BELOW the header (z 20), so the
|
||||
toggle is a real, tappable target and the menu is driven with a
|
||||
real click exactly as for admin."""
|
||||
page = _mobile_page(browser)
|
||||
try:
|
||||
page.goto(app_url)
|
||||
_wait_settled_anonymous(page)
|
||||
_assert_menu_closed(page)
|
||||
|
||||
_js_open_menu(page)
|
||||
_open_menu(page)
|
||||
assert _visible_nav_links(page) == ["Chat"], (
|
||||
"anonymous: the menu must show exactly one visible link (Chat)"
|
||||
)
|
||||
@@ -223,7 +228,96 @@ def test_anonymous_menu_contents(
|
||||
expect(page.locator(sel)).to_be_hidden()
|
||||
|
||||
# A second click closes it again — aria-expanded round-trips.
|
||||
page.evaluate("() => document.querySelector('#nav-toggle').click()")
|
||||
page.click("#nav-toggle")
|
||||
_assert_menu_closed(page)
|
||||
finally:
|
||||
page.close()
|
||||
|
||||
|
||||
def test_anonymous_toggle_tappable_with_gate_up(
|
||||
browser: Browser, app_url: str, db_ready: None
|
||||
) -> None:
|
||||
"""TODO.md L3 regression pin (phase 85): the anonymous visitor meets
|
||||
the token gate, but the gate (z-index 15) sits BELOW the sticky
|
||||
header (z 20) — so the toggle is a REAL, tappable target:
|
||||
``document.elementFromPoint`` at the toggle center resolves to the
|
||||
toggle itself (or its SVG child), NEVER ``#auth-gate`` (the exact
|
||||
probe that returned the gate in the bug reproduction), and a real
|
||||
click opens the menu ABOVE the gate — the "Chat" link visible with
|
||||
the gate still up (the dropdown's z context, 21 inside the
|
||||
header's 20, paints over the gate's 15). The lock is untouched:
|
||||
``#main`` stays ``inert``. Leaves the page clean — Esc settles the
|
||||
menu closed."""
|
||||
page = _mobile_page(browser)
|
||||
try:
|
||||
page.goto(app_url)
|
||||
_wait_settled_anonymous(page)
|
||||
|
||||
# The gate is up and the app is locked (the phase-79 contract)…
|
||||
expect(page.locator("#auth-gate")).to_be_visible(timeout=30_000)
|
||||
assert page.evaluate("() => document.getElementById('main').inert === true"), (
|
||||
"the gate must keep #main inert while it is up"
|
||||
)
|
||||
|
||||
# …but the probe at the toggle center no longer resolves to the
|
||||
# gate: it hits the toggle itself or a descendant of it (the
|
||||
# SVG path) — the exact probe that returned #auth-gate in the
|
||||
# bug reproduction (TODO.md L3).
|
||||
probe = page.evaluate(
|
||||
"""() => {
|
||||
const t = document.querySelector('#nav-toggle');
|
||||
const r = t.getBoundingClientRect();
|
||||
const el = document.elementFromPoint(
|
||||
r.left + r.width / 2, r.top + r.height / 2);
|
||||
return {
|
||||
id: el ? el.id : null,
|
||||
tag: el ? el.tagName.toLowerCase() : null,
|
||||
toggle_or_descendant: el !== null && t.contains(el),
|
||||
};
|
||||
}"""
|
||||
)
|
||||
assert probe["id"] != "auth-gate", (
|
||||
f"the toggle center must not resolve to the gate, "
|
||||
f"got <{probe['tag']} id={probe['id']!r}> (the phase-79 bug)"
|
||||
)
|
||||
assert probe["toggle_or_descendant"], (
|
||||
f"the toggle center must resolve to the toggle or its SVG "
|
||||
f"child, got <{probe['tag']} id={probe['id']!r}>"
|
||||
)
|
||||
|
||||
# A REAL click opens the menu (the owner's exact scenario —
|
||||
# "clicking it … does not expand the menu" is fixed), and the
|
||||
# menu is ABOVE the gate: with the gate still up the
|
||||
# dropdown's "Chat" link is visible, and the topmost element at
|
||||
# its center is the link (or a descendant), never #auth-gate.
|
||||
_open_menu(page)
|
||||
expect(page.locator("#auth-gate")).to_be_visible()
|
||||
chat = page.locator("#app-nav a[href='/']")
|
||||
expect(chat).to_be_visible()
|
||||
link_probe = page.evaluate(
|
||||
"""() => {
|
||||
const a = document.querySelector("#app-nav a[href='/']");
|
||||
const r = a.getBoundingClientRect();
|
||||
const el = document.elementFromPoint(
|
||||
r.left + r.width / 2, r.top + r.height / 2);
|
||||
return {
|
||||
id: el ? el.id : null,
|
||||
tag: el ? el.tagName.toLowerCase() : null,
|
||||
link_or_descendant: el !== null && a.contains(el),
|
||||
};
|
||||
}"""
|
||||
)
|
||||
assert link_probe["id"] != "auth-gate", (
|
||||
f"the open dropdown must paint ABOVE the gate, the Chat "
|
||||
f"link center resolved to the gate ({link_probe!r})"
|
||||
)
|
||||
assert link_probe["link_or_descendant"], (
|
||||
f"the Chat link must be the topmost target at its center, "
|
||||
f"got <{link_probe['tag']} id={link_probe['id']!r}>"
|
||||
)
|
||||
|
||||
# Leave the page clean: Esc settles it closed.
|
||||
page.keyboard.press("Escape")
|
||||
_assert_menu_closed(page)
|
||||
finally:
|
||||
page.close()
|
||||
@@ -303,11 +397,10 @@ def test_esc_and_outside_close(
|
||||
page.goto(app_url)
|
||||
_wait_settled_anonymous(page)
|
||||
|
||||
# Phase 79 (task 05): the anonymous visitor's toggle click is
|
||||
# intercepted by the gate overlay — drive the identical binding
|
||||
# programmatically (see _js_open_menu).
|
||||
# Phase 85 (TODO.md L3): the gate (z 15) sits below the header
|
||||
# (z 20) — the real click reaches the anonymous toggle too.
|
||||
# Esc closes + refocuses the opener.
|
||||
_js_open_menu(page)
|
||||
_open_menu(page)
|
||||
page.keyboard.press("Escape")
|
||||
_assert_menu_closed(page)
|
||||
assert page.evaluate("() => document.activeElement.id") == "nav-toggle", (
|
||||
@@ -316,10 +409,11 @@ def test_esc_and_outside_close(
|
||||
|
||||
# Outside click: the menu STAYS open (accepted behavior — the
|
||||
# locked close set is Esc + link + resize, not backdrop click).
|
||||
# Phase 79 (task 05): for the anonymous visitor the "outside"
|
||||
# point is the gate overlay itself — a REAL mouse click below
|
||||
# the centered card (outside the nav, intercepted by the gate).
|
||||
_js_open_menu(page)
|
||||
# Phase 85: for the anonymous visitor the "outside" point is
|
||||
# the gate overlay itself — a REAL mouse click below the
|
||||
# centered card, outside the nav (the gate covers the content
|
||||
# but no longer the header; it carries no close listener).
|
||||
_open_menu(page)
|
||||
page.mouse.click(10, 780) # the gate overlay — a neutral, non-nav point
|
||||
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
|
||||
assert "is-open" in (page.locator("#app-nav").get_attribute("class") or ""), (
|
||||
@@ -364,9 +458,9 @@ def test_animation_and_reduced_motion(
|
||||
f"got {report['property']!r}"
|
||||
)
|
||||
# Opening flips class + aria together (the animated state).
|
||||
# Phase 79 (task 05): the anonymous toggle click is intercepted
|
||||
# by the gate overlay — programmatic drive, same binding.
|
||||
_js_open_menu(page)
|
||||
# Phase 85: the gate sits below the header — the real click
|
||||
# reaches the anonymous toggle.
|
||||
_open_menu(page)
|
||||
page.keyboard.press("Escape")
|
||||
_assert_menu_closed(page)
|
||||
finally:
|
||||
@@ -389,9 +483,9 @@ def test_animation_and_reduced_motion(
|
||||
assert _stilled("#app-nav") == "0s", (
|
||||
f"reduced motion: closed state must not transition, got {_stilled('#app-nav')!r}"
|
||||
)
|
||||
# Phase 79 (task 05): programmatic drive (the gate overlay
|
||||
# intercepts the anonymous real click — same binding).
|
||||
_js_open_menu(rpage)
|
||||
# Phase 85: real drive (the gate no longer intercepts the
|
||||
# anonymous click — it sits below the header).
|
||||
_open_menu(rpage)
|
||||
expect(rpage.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
|
||||
expect(rpage.locator("#app-nav")).to_have_class(re.compile(r"\bis-open\b"))
|
||||
assert _stilled("#app-nav") == "0s", (
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Unit: the gate-below-header z-order contract (phase 85, task 01).
|
||||
|
||||
TODO.md L3 (owner bug report, 2026-09-07): "Only on the chat page, and
|
||||
only when navigating there directly, does the hamburger menu on mobile
|
||||
not work. … This makes the menu inaccessible on mobile."
|
||||
|
||||
Bug basis (confirmed by live reproduction): the phase-79 token gate
|
||||
ships as a full-viewport overlay at ``z-index: 500`` — ABOVE the sticky
|
||||
``.app-header`` (20) — so for an unauthenticated visitor
|
||||
``document.elementFromPoint`` at the ``#nav-toggle`` center returned
|
||||
``#auth-gate`` in every shell view: a real tap on the hamburger was
|
||||
intercepted by the overlay and the menu could never open. (The chat
|
||||
page was just the entry point; the gate broke the menu on every view.
|
||||
``login.html`` has no gate, which is why the owner saw it work "when
|
||||
logging in".)
|
||||
|
||||
The fix is ONE CSS value (owner decision A1): ``.auth-gate`` moves to
|
||||
``z-index: 15`` — above all app content (static) but BELOW the sticky
|
||||
header (20) and its mobile dropdown (21, inside the header's stacking
|
||||
context). The gate still covers and locks the app content (``#main``
|
||||
stays ``inert`` — the lock is JS, not z-order — phase 79); only the
|
||||
header's VISUAL lock is lifted. This module pins the three z-order
|
||||
values at source level (the house pattern of
|
||||
``tests/unit/test_hamburger_nav.py``: read ``styles.css`` as text, no
|
||||
browser) so the regression that put the gate at 500 cannot return.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
|
||||
STYLES_CSS = FRONTEND / "assets" / "styles.css"
|
||||
|
||||
|
||||
def _text(path: Path) -> str:
|
||||
assert path.is_file(), f"missing frontend file: {path}"
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _css() -> str:
|
||||
"""styles.css with comments stripped (a comment may legally carry
|
||||
braces — the brace-matching helpers below must never see them)."""
|
||||
return re.sub(r"/\*.*?\*/", "", _text(STYLES_CSS), flags=re.S)
|
||||
|
||||
|
||||
def _media_block(css: str, query: str) -> str:
|
||||
"""The full text of the FIRST ``@media <query>`` block (brace-matched,
|
||||
nested rules included verbatim)."""
|
||||
m = re.search(re.escape(query) + r"[^{]*\{", css)
|
||||
assert m, f"missing {query!r} media query in styles.css"
|
||||
depth = 0
|
||||
for i in range(m.end() - 1, len(css)):
|
||||
if css[i] == "{":
|
||||
depth += 1
|
||||
elif css[i] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return css[m.start() : i + 1]
|
||||
raise AssertionError(f"unbalanced braces in {query!r} media block")
|
||||
|
||||
|
||||
def _rule_block(css: str, selector: str) -> str:
|
||||
"""The first rule body for ``selector`` (e.g. ``.auth-gate``). The
|
||||
``\\s*\\{`` requires the opening brace right after the selector, so a
|
||||
decorated variant (``.auth-gate[hidden]``) is never matched."""
|
||||
m = re.search(r"(?<![\w-])" + re.escape(selector) + r"\s*\{([^}]*)\}", css)
|
||||
assert m, f"missing rule for {selector!r}"
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def _z_index(block: str) -> int:
|
||||
m = re.search(r"z-index:\s*(\d+)", block)
|
||||
assert m, "the rule must carry an explicit z-index"
|
||||
return int(m.group(1))
|
||||
|
||||
|
||||
def test_auth_gate_sits_below_the_sticky_header() -> None:
|
||||
"""THE regression pin: the .auth-gate overlay carries z-index 15 —
|
||||
below the sticky .app-header (20), so taps on the bar (and the
|
||||
hamburger) reach the bar for the unauthenticated visitor instead of
|
||||
the overlay (TODO.md L3). The old 500 (above the header) must not
|
||||
return."""
|
||||
css = _css()
|
||||
gate = _z_index(_rule_block(css, ".auth-gate"))
|
||||
header = _z_index(_rule_block(css, ".app-header"))
|
||||
assert gate == 15, f".auth-gate must be pinned at z-index 15 (found {gate})"
|
||||
assert header == 20, f".app-header must keep z-index 20 (found {header})"
|
||||
assert gate < header, (
|
||||
"the gate must sit BELOW the header — otherwise the overlay "
|
||||
"intercepts the #nav-toggle again (the phase-79 bug)"
|
||||
)
|
||||
|
||||
|
||||
def test_auth_gate_still_covers_the_app_content() -> None:
|
||||
"""The lock is JS, not z-order (phase 79): the rest of the overlay
|
||||
contract is untouched — body-level fixed full-viewport cover
|
||||
(position:fixed + inset:0), and the [hidden] state stays
|
||||
display:none (the ship-hidden skeleton contract)."""
|
||||
css = _css()
|
||||
block = _rule_block(css, ".auth-gate")
|
||||
assert "position: fixed" in block, ".auth-gate must stay position:fixed"
|
||||
assert "inset: 0" in block, ".auth-gate must still cover the full viewport"
|
||||
hidden = _rule_block(css, ".auth-gate[hidden]")
|
||||
assert "display: none" in hidden, (
|
||||
".auth-gate[hidden] must stay display:none (ship-hidden)"
|
||||
)
|
||||
|
||||
|
||||
def test_mobile_dropdown_stays_above_the_gate() -> None:
|
||||
"""The mobile dropdown (.app-nav, ≤640px — the z-index lives on the
|
||||
base rule, shared by the closed and the .is-open states) keeps
|
||||
z-index 21 = header + 1 (the phase-46 relationship pinned in
|
||||
test_hamburger_nav): inside the header's stacking context the open
|
||||
menu paints above the bar content AND above the gate (15)."""
|
||||
css = _css()
|
||||
mobile = _media_block(css, "@media (max-width: 640px)")
|
||||
assert _z_index(_rule_block(mobile, ".app-nav")) == 21, (
|
||||
"the mobile .app-nav must keep z-index 21 (header 20 + 1)"
|
||||
)
|
||||
# The header (20) itself is global (outside media queries) — the
|
||||
# ≤640px block restyles it but never changes its stacking level,
|
||||
# so the dropdown's 21 stays the header's own +1 on mobile too.
|
||||
assert _z_index(_rule_block(mobile, ".app-nav")) > 15, (
|
||||
"the open menu must paint above the gate (21 > 15)"
|
||||
)
|
||||
Reference in New Issue
Block a user