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
|
/* 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
|
two token-only pages (the shell + the document viewer, one shared
|
||||||
.auth-gate markup each). A body-level FIXED overlay (the
|
.auth-gate markup each). A body-level FIXED overlay (the
|
||||||
body-level doc-modal precedent): while visible it is the ONLY
|
body-level doc-modal precedent): while visible it is the only
|
||||||
interactive surface — assets/token-gate.js locks the root (#main,
|
CONTENT-level interactive surface — assets/token-gate.js locks the
|
||||||
inert) so focus and keyboard traversal stay inside the gate
|
root (#main, inert) so focus and keyboard traversal stay inside
|
||||||
(WCAG, the inert-pair contract).
|
the gate (WCAG, the inert-pair contract).
|
||||||
Stacking: z-index 500 — above the app content (the sticky header is
|
Stacking (phase 85, TODO.md L3): z-index 15 — ABOVE all app
|
||||||
20, the skip-link 100) but BELOW the doc-modal (1000), which can
|
content (static) but BELOW the sticky header (20) and its mobile
|
||||||
only be opened from the unlocked app anyway. Solid --bg: the canvas
|
dropdown (21, inside the header's stacking context): the
|
||||||
+ grid texture live on <html> (the body stays transparent), so the
|
unauthenticated visitor keeps a reachable hamburger + menu while
|
||||||
overlay reads as the app's own surface — no blur (the phase-08
|
the gate still covers and locks the app content (#main stays
|
||||||
perf anchor).
|
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
|
The centered card reuses the #sources-gate visual language
|
||||||
(surface card + hairline + glyph + heading + sub + action) in a
|
(surface card + hairline + glyph + heading + sub + action) in a
|
||||||
tighter column. */
|
tighter column. */
|
||||||
.auth-gate {
|
.auth-gate {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 500;
|
z-index: 15;
|
||||||
display: flex; /* the card is the only in-flow child — margin: auto centers it */
|
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 */
|
overflow-y: auto; /* short viewports: the card scrolls fully into view */
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
|||||||
@@ -193,9 +193,13 @@ export async function mountGate(lockRoot, onAuthed) {
|
|||||||
|
|
||||||
/* Anonymous: reveal the gate (drop hidden AND inert — the inert-pair
|
/* Anonymous: reveal the gate (drop hidden AND inert — the inert-pair
|
||||||
contract), lock the app, focus the token input. The gate is the
|
contract), lock the app, focus the token input. The gate is the
|
||||||
only interactive surface while visible: the lock root is inert,
|
only CONTENT-level interactive surface while visible: the lock
|
||||||
so Tab never reaches the locked app (the composer on the shell,
|
root is inert, so Tab never reaches the locked app (the composer
|
||||||
the content on the viewer). */
|
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) {
|
if (gate) {
|
||||||
gate.hidden = false;
|
gate.hidden = false;
|
||||||
gate.inert = false;
|
gate.inert = false;
|
||||||
|
|||||||
@@ -162,7 +162,10 @@
|
|||||||
root; the existing boot sequence (whoami → load content) runs
|
root; the existing boot sequence (whoami → load content) runs
|
||||||
as onAuthed — an admin (or a validly cached token user) gets
|
as onAuthed — an admin (or a validly cached token user) gets
|
||||||
it immediately, the gate never shows. SHIPS hidden + inert
|
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">
|
<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-card">
|
||||||
<div class="auth-gate-glyph" aria-hidden="true">
|
<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
|
<!-- Phase 79 (task 05): the in-app token gate — a body-level
|
||||||
position:fixed; inset:0 overlay (the body-level doc-modal
|
position:fixed; inset:0 overlay (the body-level doc-modal
|
||||||
precedent) that is the ONLY interactive surface while visible:
|
precedent) that is the only CONTENT-level interactive surface
|
||||||
assets/token-gate.js locks #main (inert) so focus and keyboard
|
while visible: assets/token-gate.js locks #main (inert) so
|
||||||
traversal stay in the gate (WCAG, the inert-pair contract). It
|
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
|
SHIPS hidden + inert (the phase-16 ship-hidden pattern) — an
|
||||||
authenticated boot (admin, or a valid cached token re-authed
|
authenticated boot (admin, or a valid cached token re-authed
|
||||||
silently at mount) never shows it for a frame. The gate is the
|
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
|
byte-identical to pre-phase-46 (hamburger absent, inline pills). No
|
||||||
document is ever needed — the suite exercises the shared header only.
|
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
|
The conftest ``page`` fixture is 1280×800, so the mobile tests create
|
||||||
fresh 375×812 pages via the session ``browser`` fixture (one page per
|
fresh 375×812 pages via the session ``browser`` fixture (one page per
|
||||||
test; the reduced-motion test gets its own context).
|
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
|
2. ``test_anonymous_menu_contents`` — anonymous at 375px: the menu
|
||||||
shows EXACTLY one visible link ("Chat"); the three admin-only links
|
shows EXACTLY one visible link ("Chat"); the three admin-only links
|
||||||
stay ``hidden`` inside the menu; the open flips ``aria-expanded``.
|
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).
|
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
|
"RAG" navigates to /sources.html and the menu on the arrival
|
||||||
page ships closed.
|
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
|
toggle; an outside click does NOT close (accepted — see the test
|
||||||
docstring for why).
|
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
|
180ms opacity/transform transition pair is live and the open flips
|
||||||
class + aria; ``reducedMotion: "reduce"``: no transition in EITHER
|
class + aria; ``reducedMotion: "reduce"``: no transition in EITHER
|
||||||
state (the .is-open state included — the specificity trap) and
|
state (the .is-open state included — the specificity trap) and
|
||||||
open/close still works.
|
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
|
``display: none`` and the inline nav renders in the bar exactly as
|
||||||
before (admin: all four links, all inside the header band).
|
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:
|
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")
|
page.click("#nav-toggle")
|
||||||
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
|
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
|
||||||
# to_have_class(string) is an EXACT match on the class attribute —
|
# 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")
|
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:
|
def _assert_menu_closed(page: Page) -> None:
|
||||||
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "false")
|
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "false")
|
||||||
assert "is-open" not in (page.locator("#app-nav").get_attribute("class") or ""), (
|
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
|
preserved by reusing the same <nav> element); opening flips
|
||||||
aria-expanded true.
|
aria-expanded true.
|
||||||
|
|
||||||
Phase 79 (task 05): the anonymous visitor meets the token gate — a
|
Phase 85 (TODO.md L3): the anonymous visitor meets the token gate
|
||||||
full-viewport overlay that covers the header — so the toggle is
|
— but the gate (z 15) now sits BELOW the header (z 20), so the
|
||||||
driven programmatically (the binding is identical; see
|
toggle is a real, tappable target and the menu is driven with a
|
||||||
_js_open_menu)."""
|
real click exactly as for admin."""
|
||||||
page = _mobile_page(browser)
|
page = _mobile_page(browser)
|
||||||
try:
|
try:
|
||||||
page.goto(app_url)
|
page.goto(app_url)
|
||||||
_wait_settled_anonymous(page)
|
_wait_settled_anonymous(page)
|
||||||
_assert_menu_closed(page)
|
_assert_menu_closed(page)
|
||||||
|
|
||||||
_js_open_menu(page)
|
_open_menu(page)
|
||||||
assert _visible_nav_links(page) == ["Chat"], (
|
assert _visible_nav_links(page) == ["Chat"], (
|
||||||
"anonymous: the menu must show exactly one visible link (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()
|
expect(page.locator(sel)).to_be_hidden()
|
||||||
|
|
||||||
# A second click closes it again — aria-expanded round-trips.
|
# 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)
|
_assert_menu_closed(page)
|
||||||
finally:
|
finally:
|
||||||
page.close()
|
page.close()
|
||||||
@@ -303,11 +397,10 @@ def test_esc_and_outside_close(
|
|||||||
page.goto(app_url)
|
page.goto(app_url)
|
||||||
_wait_settled_anonymous(page)
|
_wait_settled_anonymous(page)
|
||||||
|
|
||||||
# Phase 79 (task 05): the anonymous visitor's toggle click is
|
# Phase 85 (TODO.md L3): the gate (z 15) sits below the header
|
||||||
# intercepted by the gate overlay — drive the identical binding
|
# (z 20) — the real click reaches the anonymous toggle too.
|
||||||
# programmatically (see _js_open_menu).
|
|
||||||
# Esc closes + refocuses the opener.
|
# Esc closes + refocuses the opener.
|
||||||
_js_open_menu(page)
|
_open_menu(page)
|
||||||
page.keyboard.press("Escape")
|
page.keyboard.press("Escape")
|
||||||
_assert_menu_closed(page)
|
_assert_menu_closed(page)
|
||||||
assert page.evaluate("() => document.activeElement.id") == "nav-toggle", (
|
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
|
# Outside click: the menu STAYS open (accepted behavior — the
|
||||||
# locked close set is Esc + link + resize, not backdrop click).
|
# locked close set is Esc + link + resize, not backdrop click).
|
||||||
# Phase 79 (task 05): for the anonymous visitor the "outside"
|
# Phase 85: for the anonymous visitor the "outside" point is
|
||||||
# point is the gate overlay itself — a REAL mouse click below
|
# the gate overlay itself — a REAL mouse click below the
|
||||||
# the centered card (outside the nav, intercepted by the gate).
|
# centered card, outside the nav (the gate covers the content
|
||||||
_js_open_menu(page)
|
# 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
|
page.mouse.click(10, 780) # the gate overlay — a neutral, non-nav point
|
||||||
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
|
expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
|
||||||
assert "is-open" in (page.locator("#app-nav").get_attribute("class") or ""), (
|
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}"
|
f"got {report['property']!r}"
|
||||||
)
|
)
|
||||||
# Opening flips class + aria together (the animated state).
|
# Opening flips class + aria together (the animated state).
|
||||||
# Phase 79 (task 05): the anonymous toggle click is intercepted
|
# Phase 85: the gate sits below the header — the real click
|
||||||
# by the gate overlay — programmatic drive, same binding.
|
# reaches the anonymous toggle.
|
||||||
_js_open_menu(page)
|
_open_menu(page)
|
||||||
page.keyboard.press("Escape")
|
page.keyboard.press("Escape")
|
||||||
_assert_menu_closed(page)
|
_assert_menu_closed(page)
|
||||||
finally:
|
finally:
|
||||||
@@ -389,9 +483,9 @@ def test_animation_and_reduced_motion(
|
|||||||
assert _stilled("#app-nav") == "0s", (
|
assert _stilled("#app-nav") == "0s", (
|
||||||
f"reduced motion: closed state must not transition, got {_stilled('#app-nav')!r}"
|
f"reduced motion: closed state must not transition, got {_stilled('#app-nav')!r}"
|
||||||
)
|
)
|
||||||
# Phase 79 (task 05): programmatic drive (the gate overlay
|
# Phase 85: real drive (the gate no longer intercepts the
|
||||||
# intercepts the anonymous real click — same binding).
|
# anonymous click — it sits below the header).
|
||||||
_js_open_menu(rpage)
|
_open_menu(rpage)
|
||||||
expect(rpage.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
|
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"))
|
expect(rpage.locator("#app-nav")).to_have_class(re.compile(r"\bis-open\b"))
|
||||||
assert _stilled("#app-nav") == "0s", (
|
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