phase: 171_admin_settings_layout
Expected state: only `00_phase.md` remains in `todo/` (the harness moves it on success; I must not touch it). All verification is complete. --- **Phase 171 final verification pass — report** - **Verified (all 4 tasks complete, CSS-only diff confirmed byte-exact to contract):** `#settings-content` flex column + `gap: 1.25rem` (tasks 01); `.settings-group-row th` band `--brand-soft` + 700 + `--line` top hairline (02); `#settings-save:disabled` `cursor: default` (03); new phase-dedicated Playwright suite (04). Mobile media queries and `settings.js`/`app/` byte-untouched, as pinned. - **Gates:** `uv run pytest --cov=app --cov-report=term-missing` → **4069 passed, app/ 99%** (>90%); `uv run ruff check .` → clean; `uv run pyright` → **0 errors, 0 warnings**. - **E2E (in isolation):** `test_admin_settings_layout.py` → **5 passed**; `test_admin_settings_panel.py` (phase 168) → **12 passed**; `test_wide_desktop_column.py` → **3 passed**. - **Completion criteria:** search/table no-intersection + gapped action row at 1440/375 px ✅ · distinct group bands (bg ≠ card, weight 700, ≥1px top hairline) ✅ · disabled Save `cursor: default` with §7.4 dim + aria-busy lifecycle intact ✅ · full suite + coverage + lint ✅ · all three E2E suites isolated ✅ · atomic commit + dir move = **harness's job** (task files already in `complete/`; per protocol I left every change uncommitted in the working tree). - **Documentation:** README.md (all 1487 lines) and AGENTS.md (all 80 lines) fully reviewed — every config default cross-checked vs `app/config.py`, all quick-reference commands verified against existing files, AGENTS.md rules/contracts verified vs code — **verified consistent, no changes needed** (phase 171 is CSS-only: no setup/command/env/API impact). - **Notable:** one phase-168 test failed on the first isolated run solely because an unrelated `undercooked` dev server held port 8124 (the suite's `DISABLED_PORT`); re-ran with the suite's built-in `E2E_APP_PORT=8300` override — no code change, foreign process untouched. Post-run DB state clean (`app_settings` 0, `ui_settings` 0, e2e tokens 0). - **Next pending phase:** none — phase 171 was the last `todo/` phase.
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
Expected state: only `00_phase.md` remains in `todo/` (the harness moves it on success; I must not touch it). All verification is complete.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Phase 171 final verification pass — report**
|
||||||
|
|
||||||
|
- **Verified (all 4 tasks complete, CSS-only diff confirmed byte-exact to contract):** `#settings-content` flex column + `gap: 1.25rem` (tasks 01); `.settings-group-row th` band `--brand-soft` + 700 + `--line` top hairline (02); `#settings-save:disabled` `cursor: default` (03); new phase-dedicated Playwright suite (04). Mobile media queries and `settings.js`/`app/` byte-untouched, as pinned.
|
||||||
|
- **Gates:** `uv run pytest --cov=app --cov-report=term-missing` → **4069 passed, app/ 99%** (>90%); `uv run ruff check .` → clean; `uv run pyright` → **0 errors, 0 warnings**.
|
||||||
|
- **E2E (in isolation):** `test_admin_settings_layout.py` → **5 passed**; `test_admin_settings_panel.py` (phase 168) → **12 passed**; `test_wide_desktop_column.py` → **3 passed**.
|
||||||
|
- **Completion criteria:** search/table no-intersection + gapped action row at 1440/375 px ✅ · distinct group bands (bg ≠ card, weight 700, ≥1px top hairline) ✅ · disabled Save `cursor: default` with §7.4 dim + aria-busy lifecycle intact ✅ · full suite + coverage + lint ✅ · all three E2E suites isolated ✅ · atomic commit + dir move = **harness's job** (task files already in `complete/`; per protocol I left every change uncommitted in the working tree).
|
||||||
|
- **Documentation:** README.md (all 1487 lines) and AGENTS.md (all 80 lines) fully reviewed — every config default cross-checked vs `app/config.py`, all quick-reference commands verified against existing files, AGENTS.md rules/contracts verified vs code — **verified consistent, no changes needed** (phase 171 is CSS-only: no setup/command/env/API impact).
|
||||||
|
- **Notable:** one phase-168 test failed on the first isolated run solely because an unrelated `undercooked` dev server held port 8124 (the suite's `DISABLED_PORT`); re-ran with the suite's built-in `E2E_APP_PORT=8300` override — no code change, foreign process untouched. Post-run DB state clean (`app_settings` 0, `ui_settings` 0, e2e tokens 0).
|
||||||
|
- **Next pending phase:** none — phase 171 was the last `todo/` phase.
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
ok
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
........................................................................ [ 1%]
|
||||||
|
........................................................................ [ 3%]
|
||||||
|
........................................................................ [ 5%]
|
||||||
|
........................................................................ [ 7%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 10%]
|
||||||
|
........................................................................ [ 12%]
|
||||||
|
........................................................................ [ 14%]
|
||||||
|
........................................................................ [ 15%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 19%]
|
||||||
|
........................................................................ [ 21%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
........................................................................ [ 24%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 28%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 31%]
|
||||||
|
........................................................................ [ 33%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 37%]
|
||||||
|
........................................................................ [ 38%]
|
||||||
|
........................................................................ [ 40%]
|
||||||
|
........................................................................ [ 42%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 47%]
|
||||||
|
........................................................................ [ 49%]
|
||||||
|
........................................................................ [ 51%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 54%]
|
||||||
|
........................................................................ [ 56%]
|
||||||
|
........................................................................ [ 58%]
|
||||||
|
........................................................................ [ 60%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 63%]
|
||||||
|
........................................................................ [ 65%]
|
||||||
|
........................................................................ [ 67%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 72%]
|
||||||
|
........................................................................ [ 74%]
|
||||||
|
........................................................................ [ 76%]
|
||||||
|
........................................................................ [ 77%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 81%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 84%]
|
||||||
|
........................................................................ [ 86%]
|
||||||
|
........................................................................ [ 88%]
|
||||||
|
........................................................................ [ 90%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 93%]
|
||||||
|
........................................................................ [ 95%]
|
||||||
|
........................................................................ [ 97%]
|
||||||
|
........................................................................ [ 99%]
|
||||||
|
..................................... [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/admin_settings.py 108 0 100%
|
||||||
|
app/api/auth.py 52 0 100%
|
||||||
|
app/api/chat.py 280 1 99%
|
||||||
|
app/api/chat_images.py 50 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 13 0 100%
|
||||||
|
app/api/doc_drafts.py 99 0 100%
|
||||||
|
app/api/docs.py 234 1 99%
|
||||||
|
app/api/git_sources.py 259 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 33 0 100%
|
||||||
|
app/api/sync.py 204 0 100%
|
||||||
|
app/api/tokens.py 40 0 100%
|
||||||
|
app/api/ui_settings.py 55 0 100%
|
||||||
|
app/config.py 285 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 124 0 100%
|
||||||
|
app/core/compression.py 59 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/http_cache.py 22 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/rate_limit.py 44 0 100%
|
||||||
|
app/core/response_budget.py 63 0 100%
|
||||||
|
app/core/security_headers.py 20 0 100%
|
||||||
|
app/core/settings_catalog.py 18 0 100%
|
||||||
|
app/core/settings_overlay.py 111 0 100%
|
||||||
|
app/core/theming.py 38 0 100%
|
||||||
|
app/core/tokens.py 44 0 100%
|
||||||
|
app/db.py 22 0 100%
|
||||||
|
app/main.py 86 0 100%
|
||||||
|
app/models.py 141 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 454 1 99%
|
||||||
|
app/rag/archive_upload.py 134 0 100%
|
||||||
|
app/rag/chunker.py 220 4 98%
|
||||||
|
app/rag/doc_dates.py 18 0 100%
|
||||||
|
app/rag/folder_summaries.py 134 0 100%
|
||||||
|
app/rag/git_sources.py 77 0 100%
|
||||||
|
app/rag/importer.py 841 4 99%
|
||||||
|
app/rag/llm.py 246 1 99%
|
||||||
|
app/rag/md_images.py 63 0 100%
|
||||||
|
app/rag/overview.py 78 0 100%
|
||||||
|
app/rag/pdf.py 192 0 100%
|
||||||
|
app/rag/prompts.py 156 0 100%
|
||||||
|
app/rag/retriever.py 252 3 99%
|
||||||
|
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 58 0 100%
|
||||||
|
app/schemas.py 532 0 100%
|
||||||
|
--------------------------------------------------
|
||||||
|
TOTAL 6392 17 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
|
||||||
|
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
|
||||||
|
|
||||||
|
validation OK
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
All verified. Final report:
|
||||||
|
|
||||||
|
**Task 01 — `#settings-content` vertical rhythm: complete, 100% green.**
|
||||||
|
|
||||||
|
- Added one CSS rule in `frontend/assets/styles.css` directly after `.settings-shell`: `#settings-content { display: flex; flex-direction: column; gap: 1.25rem; }` — the shell's own token (ASSUMPTION A4), comment citing phase-171 task 01 + TODO L4; no per-element margins (the `#settings-status`/`.settings-error` `min-height`s already reserve their lines)
|
||||||
|
- Added house-style pin `test_settings_content_is_a_flex_column_with_the_shell_gap` in `tests/unit/test_settings_frontend_view.py` (three declarations in one rule block + no-margin check on `.settings-search`/`.settings-actions`); module docstring pin inventory updated
|
||||||
|
- Verified safe with the `hidden` attribute (global `[hidden] { display: none !important; }` wins until `settings.js` reveals); `.visually-hidden` `#settings-restart-desc` is out-of-flow, so no phantom flex row; the 640px mobile stacking is byte-unchanged (diff touches exactly the one rule + pin)
|
||||||
|
- Tests: `uv run pytest tests/unit/test_settings_frontend_view.py -v --no-cov` → 14 passed; `uv run pytest --cov=app --cov-report=term-missing` → **4067 passed, app/ coverage 99% (>90%)**
|
||||||
|
- Lint/types: `uv run ruff check .` → all checks passed; `uv run pyright` → 0 errors, 0 warnings
|
||||||
|
- No `app/`/JS changes; no doc impact (no setup/command/env/API/convention change — README/AGENTS.md untouched)
|
||||||
|
- The search-box-overlap and smushed-action-row bounding-box proof is task 04's E2E scope
|
||||||
|
- Next pending task: `02_group_band_distinction.md` (not yet authored in `todo/` — only `00_phase.md` + task 01 present)
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
ok
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
........................................................................ [ 1%]
|
||||||
|
........................................................................ [ 3%]
|
||||||
|
........................................................................ [ 5%]
|
||||||
|
........................................................................ [ 7%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 10%]
|
||||||
|
........................................................................ [ 12%]
|
||||||
|
........................................................................ [ 14%]
|
||||||
|
........................................................................ [ 15%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 19%]
|
||||||
|
........................................................................ [ 21%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
........................................................................ [ 24%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 28%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 31%]
|
||||||
|
........................................................................ [ 33%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 37%]
|
||||||
|
........................................................................ [ 38%]
|
||||||
|
........................................................................ [ 40%]
|
||||||
|
........................................................................ [ 42%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 47%]
|
||||||
|
........................................................................ [ 49%]
|
||||||
|
........................................................................ [ 51%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 54%]
|
||||||
|
........................................................................ [ 56%]
|
||||||
|
........................................................................ [ 58%]
|
||||||
|
........................................................................ [ 60%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 63%]
|
||||||
|
........................................................................ [ 65%]
|
||||||
|
........................................................................ [ 67%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 72%]
|
||||||
|
........................................................................ [ 74%]
|
||||||
|
........................................................................ [ 76%]
|
||||||
|
........................................................................ [ 77%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 81%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 84%]
|
||||||
|
........................................................................ [ 86%]
|
||||||
|
........................................................................ [ 88%]
|
||||||
|
........................................................................ [ 90%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 93%]
|
||||||
|
........................................................................ [ 95%]
|
||||||
|
........................................................................ [ 97%]
|
||||||
|
........................................................................ [ 99%]
|
||||||
|
................................... [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/admin_settings.py 108 0 100%
|
||||||
|
app/api/auth.py 52 0 100%
|
||||||
|
app/api/chat.py 280 1 99%
|
||||||
|
app/api/chat_images.py 50 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 13 0 100%
|
||||||
|
app/api/doc_drafts.py 99 0 100%
|
||||||
|
app/api/docs.py 234 1 99%
|
||||||
|
app/api/git_sources.py 259 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 33 0 100%
|
||||||
|
app/api/sync.py 204 0 100%
|
||||||
|
app/api/tokens.py 40 0 100%
|
||||||
|
app/api/ui_settings.py 55 0 100%
|
||||||
|
app/config.py 285 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 124 0 100%
|
||||||
|
app/core/compression.py 59 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/http_cache.py 22 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/rate_limit.py 44 0 100%
|
||||||
|
app/core/response_budget.py 63 0 100%
|
||||||
|
app/core/security_headers.py 20 0 100%
|
||||||
|
app/core/settings_catalog.py 18 0 100%
|
||||||
|
app/core/settings_overlay.py 111 0 100%
|
||||||
|
app/core/theming.py 38 0 100%
|
||||||
|
app/core/tokens.py 44 0 100%
|
||||||
|
app/db.py 22 0 100%
|
||||||
|
app/main.py 86 0 100%
|
||||||
|
app/models.py 141 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 454 1 99%
|
||||||
|
app/rag/archive_upload.py 134 0 100%
|
||||||
|
app/rag/chunker.py 220 4 98%
|
||||||
|
app/rag/doc_dates.py 18 0 100%
|
||||||
|
app/rag/folder_summaries.py 134 0 100%
|
||||||
|
app/rag/git_sources.py 77 0 100%
|
||||||
|
app/rag/importer.py 841 4 99%
|
||||||
|
app/rag/llm.py 246 1 99%
|
||||||
|
app/rag/md_images.py 63 0 100%
|
||||||
|
app/rag/overview.py 78 0 100%
|
||||||
|
app/rag/pdf.py 192 0 100%
|
||||||
|
app/rag/prompts.py 156 0 100%
|
||||||
|
app/rag/retriever.py 252 3 99%
|
||||||
|
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 58 0 100%
|
||||||
|
app/schemas.py 532 0 100%
|
||||||
|
--------------------------------------------------
|
||||||
|
TOTAL 6392 17 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
|
||||||
|
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
|
||||||
|
|
||||||
|
validation OK
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
All green. Task 02 complete.
|
||||||
|
|
||||||
|
**Report**
|
||||||
|
|
||||||
|
- **Implemented**
|
||||||
|
- `frontend/assets/styles.css` — `.settings-group-row th` band: `background: var(--surface)` → `var(--brand-soft)` (the sticky-th solid tint; B3: no new palette var), added `font-weight: 700` + `border-top: 1px solid var(--line)` (delimited above AND below — the `.settings-table th` family supplies the bottom border); contrast comment restated: `--ink` on `--brand-soft` = **14.8:1** (AA, recomputed)
|
||||||
|
- `tests/unit/test_settings_frontend_view.py` — new pin `test_settings_group_band_is_branded_bold_hairlined` (band's `--brand-soft` + 700 + top hairline; old `--surface` fill gone; `--ink` pairing + 0.82rem/uppercase/letter-spacing typography survive; bottom border pinned on the `th` family; no transition/animation) + docstring entry
|
||||||
|
- **Test / lint / coverage**
|
||||||
|
- `uv run pytest tests/unit/test_settings_frontend_view.py -v --no-cov` → **15 passed**
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → **4068 passed, app/ 99%** (>90% gate)
|
||||||
|
- `uv run ruff check .` → clean; `uv run pyright` → **0 errors, 0 warnings**
|
||||||
|
- Regression: `tests/e2e/test_admin_settings_panel.py` (phase 168) isolated → **12 passed** (first run's single failure was a port-8124 collision with an *unrelated project's* dev server — not killed; re-ran on `E2E_APP_PORT=8300`)
|
||||||
|
- **Docs:** no doc impact (CSS-only, no config/API/env changes)
|
||||||
|
- **Decisions:** `settings.js` byte-unchanged (pure-CSS band, per task); no live-infrastructure touched
|
||||||
|
- **Next pending task:** `.agents/phases/todo/171_admin_settings_layout/03_disabled_save_cursor.md`
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
ok
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
........................................................................ [ 1%]
|
||||||
|
........................................................................ [ 3%]
|
||||||
|
........................................................................ [ 5%]
|
||||||
|
........................................................................ [ 7%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 10%]
|
||||||
|
........................................................................ [ 12%]
|
||||||
|
........................................................................ [ 14%]
|
||||||
|
........................................................................ [ 15%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 19%]
|
||||||
|
........................................................................ [ 21%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
........................................................................ [ 24%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 28%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 31%]
|
||||||
|
........................................................................ [ 33%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 37%]
|
||||||
|
........................................................................ [ 38%]
|
||||||
|
........................................................................ [ 40%]
|
||||||
|
........................................................................ [ 42%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 47%]
|
||||||
|
........................................................................ [ 49%]
|
||||||
|
........................................................................ [ 51%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 54%]
|
||||||
|
........................................................................ [ 56%]
|
||||||
|
........................................................................ [ 58%]
|
||||||
|
........................................................................ [ 60%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 63%]
|
||||||
|
........................................................................ [ 65%]
|
||||||
|
........................................................................ [ 67%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 72%]
|
||||||
|
........................................................................ [ 74%]
|
||||||
|
........................................................................ [ 76%]
|
||||||
|
........................................................................ [ 77%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 81%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 84%]
|
||||||
|
........................................................................ [ 86%]
|
||||||
|
........................................................................ [ 88%]
|
||||||
|
........................................................................ [ 90%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 93%]
|
||||||
|
........................................................................ [ 95%]
|
||||||
|
........................................................................ [ 97%]
|
||||||
|
........................................................................ [ 99%]
|
||||||
|
.................................... [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/admin_settings.py 108 0 100%
|
||||||
|
app/api/auth.py 52 0 100%
|
||||||
|
app/api/chat.py 280 1 99%
|
||||||
|
app/api/chat_images.py 50 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 13 0 100%
|
||||||
|
app/api/doc_drafts.py 99 0 100%
|
||||||
|
app/api/docs.py 234 1 99%
|
||||||
|
app/api/git_sources.py 259 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 33 0 100%
|
||||||
|
app/api/sync.py 204 0 100%
|
||||||
|
app/api/tokens.py 40 0 100%
|
||||||
|
app/api/ui_settings.py 55 0 100%
|
||||||
|
app/config.py 285 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 124 0 100%
|
||||||
|
app/core/compression.py 59 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/http_cache.py 22 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/rate_limit.py 44 0 100%
|
||||||
|
app/core/response_budget.py 63 0 100%
|
||||||
|
app/core/security_headers.py 20 0 100%
|
||||||
|
app/core/settings_catalog.py 18 0 100%
|
||||||
|
app/core/settings_overlay.py 111 0 100%
|
||||||
|
app/core/theming.py 38 0 100%
|
||||||
|
app/core/tokens.py 44 0 100%
|
||||||
|
app/db.py 22 0 100%
|
||||||
|
app/main.py 86 0 100%
|
||||||
|
app/models.py 141 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 454 1 99%
|
||||||
|
app/rag/archive_upload.py 134 0 100%
|
||||||
|
app/rag/chunker.py 220 4 98%
|
||||||
|
app/rag/doc_dates.py 18 0 100%
|
||||||
|
app/rag/folder_summaries.py 134 0 100%
|
||||||
|
app/rag/git_sources.py 77 0 100%
|
||||||
|
app/rag/importer.py 841 4 99%
|
||||||
|
app/rag/llm.py 246 1 99%
|
||||||
|
app/rag/md_images.py 63 0 100%
|
||||||
|
app/rag/overview.py 78 0 100%
|
||||||
|
app/rag/pdf.py 192 0 100%
|
||||||
|
app/rag/prompts.py 156 0 100%
|
||||||
|
app/rag/retriever.py 252 3 99%
|
||||||
|
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 58 0 100%
|
||||||
|
app/schemas.py 532 0 100%
|
||||||
|
--------------------------------------------------
|
||||||
|
TOTAL 6392 17 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
|
||||||
|
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
|
||||||
|
|
||||||
|
validation OK
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
Everything is green and the diff is exactly the task contract. The E2E verification of the live cursor is deferred to task 04 per the completion criterion ("in the live UI (or the task-04 E2E)").
|
||||||
|
|
||||||
|
**Report — phase 171, task 03 (disabled Save cursor)**
|
||||||
|
|
||||||
|
- `#settings-save:disabled` in `frontend/assets/styles.css`: `cursor: wait` → `cursor: default` (the `.settings-reset:disabled` house idiom); `opacity: 0.6` (§7.4 dim) and the `:hover:not(:disabled)` rule byte-unchanged
|
||||||
|
- Action-row comment now states the cursor contract explicitly (disabled = `default`; the wait cursor was never part of §7.4)
|
||||||
|
- Pin in `tests/unit/test_settings_frontend_view.py`: `#settings-save:disabled` carries `cursor: default`, `opacity: 0.6` intact, hover rule intact, reset-idiom line stands, plus a scoped negative pin — `cursor: wait` absent from **all** `.settings*` / `#settings-*` rule blocks (media-query-aware selector parse; the chat edit form's `.edit-save` keeps its own `wait` and is out of scope); module docstring pin list updated
|
||||||
|
- Tests: `uv run pytest tests/unit/test_settings_frontend_view.py -v --no-cov` → 16 passed
|
||||||
|
- Full suite: `uv run pytest --cov=app --cov-report=term-missing` → **4069 passed, 0 failed; `app/` coverage 99%** (>90% gate)
|
||||||
|
- Lint/types: `uv run ruff check .` → all checks passed; `uv run pyright` → 0 errors, 0 warnings
|
||||||
|
- Docs: no doc impact (internal CSS value; no setup/command/env/API changes)
|
||||||
|
- Decisions: negative pin implemented as a selector-scoped rule-body scan so it can't trip on non-settings controls, per the task note
|
||||||
|
- `settings.js` and `app/` byte-unchanged; only `styles.css` + the test file touched (working tree, per harness protocol)
|
||||||
|
- Next pending task: `04_e2e_settings_layout.md` (the phase's dedicated Playwright suite + final gates)
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
ok
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
........................................................................ [ 1%]
|
||||||
|
........................................................................ [ 3%]
|
||||||
|
........................................................................ [ 5%]
|
||||||
|
........................................................................ [ 7%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 10%]
|
||||||
|
........................................................................ [ 12%]
|
||||||
|
........................................................................ [ 14%]
|
||||||
|
........................................................................ [ 15%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 19%]
|
||||||
|
........................................................................ [ 21%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
........................................................................ [ 24%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 28%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 31%]
|
||||||
|
........................................................................ [ 33%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 37%]
|
||||||
|
........................................................................ [ 38%]
|
||||||
|
........................................................................ [ 40%]
|
||||||
|
........................................................................ [ 42%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 47%]
|
||||||
|
........................................................................ [ 49%]
|
||||||
|
........................................................................ [ 51%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 54%]
|
||||||
|
........................................................................ [ 56%]
|
||||||
|
........................................................................ [ 58%]
|
||||||
|
........................................................................ [ 60%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 63%]
|
||||||
|
........................................................................ [ 65%]
|
||||||
|
........................................................................ [ 67%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 72%]
|
||||||
|
........................................................................ [ 74%]
|
||||||
|
........................................................................ [ 76%]
|
||||||
|
........................................................................ [ 77%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 81%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 84%]
|
||||||
|
........................................................................ [ 86%]
|
||||||
|
........................................................................ [ 88%]
|
||||||
|
........................................................................ [ 90%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 93%]
|
||||||
|
........................................................................ [ 95%]
|
||||||
|
........................................................................ [ 97%]
|
||||||
|
........................................................................ [ 99%]
|
||||||
|
..................................... [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/admin_settings.py 108 0 100%
|
||||||
|
app/api/auth.py 52 0 100%
|
||||||
|
app/api/chat.py 280 1 99%
|
||||||
|
app/api/chat_images.py 50 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 13 0 100%
|
||||||
|
app/api/doc_drafts.py 99 0 100%
|
||||||
|
app/api/docs.py 234 1 99%
|
||||||
|
app/api/git_sources.py 259 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 33 0 100%
|
||||||
|
app/api/sync.py 204 0 100%
|
||||||
|
app/api/tokens.py 40 0 100%
|
||||||
|
app/api/ui_settings.py 55 0 100%
|
||||||
|
app/config.py 285 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 124 0 100%
|
||||||
|
app/core/compression.py 59 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/http_cache.py 22 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/rate_limit.py 44 0 100%
|
||||||
|
app/core/response_budget.py 63 0 100%
|
||||||
|
app/core/security_headers.py 20 0 100%
|
||||||
|
app/core/settings_catalog.py 18 0 100%
|
||||||
|
app/core/settings_overlay.py 111 0 100%
|
||||||
|
app/core/theming.py 38 0 100%
|
||||||
|
app/core/tokens.py 44 0 100%
|
||||||
|
app/db.py 22 0 100%
|
||||||
|
app/main.py 86 0 100%
|
||||||
|
app/models.py 141 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 454 1 99%
|
||||||
|
app/rag/archive_upload.py 134 0 100%
|
||||||
|
app/rag/chunker.py 220 4 98%
|
||||||
|
app/rag/doc_dates.py 18 0 100%
|
||||||
|
app/rag/folder_summaries.py 134 0 100%
|
||||||
|
app/rag/git_sources.py 77 0 100%
|
||||||
|
app/rag/importer.py 841 4 99%
|
||||||
|
app/rag/llm.py 246 1 99%
|
||||||
|
app/rag/md_images.py 63 0 100%
|
||||||
|
app/rag/overview.py 78 0 100%
|
||||||
|
app/rag/pdf.py 192 0 100%
|
||||||
|
app/rag/prompts.py 156 0 100%
|
||||||
|
app/rag/retriever.py 252 3 99%
|
||||||
|
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 58 0 100%
|
||||||
|
app/schemas.py 532 0 100%
|
||||||
|
--------------------------------------------------
|
||||||
|
TOTAL 6392 17 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
|
||||||
|
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
|
||||||
|
|
||||||
|
validation OK
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
Task 04 complete — all green.
|
||||||
|
|
||||||
|
**Implemented**
|
||||||
|
- `tests/e2e/test_admin_settings_layout.py` (new, phase-dedicated per AGENTS rule 4): module app fixture copied verbatim from the phase-168 suite (mock LLM, `BOR_SETTINGS_DB=1` opt-in, TRUNCATE before/after, `.env` mtime guard), docstring rewritten for the four layout contracts; the dead kill-switch fixture/constants dropped with their non-listed test.
|
||||||
|
- Five pinned tests: search row clear of the card (1440/375) + sticky-`thead` intact; action row gapped from the card + both buttons reachable/unclipped; group bands distinct (tint ≠ card, weight ≥700, ≥1px top hairline); disabled Save `cursor: default` → `pointer` when dirty → dimmed `default` (opacity 0.6, no `aria-busy`) after a settled Save; no horizontal overflow at 375/1440, table ≥80% of container, keyboard walk filter → first input → Save → Clear all, zero external requests.
|
||||||
|
|
||||||
|
**Gates (all green)**
|
||||||
|
- `uv run pytest tests/e2e/test_admin_settings_layout.py -v --no-cov` → 5 passed (isolated)
|
||||||
|
- `uv run pytest tests/e2e/test_admin_settings_panel.py -v --no-cov` (E2E_APP_PORT=8125) → 12 passed; `test_wide_desktop_column.py` → 3 passed
|
||||||
|
- `uv run pytest` → 4069 passed; `--cov=app` → 99% (>90%); ruff clean; pyright 0 errors
|
||||||
|
- `app_settings` = 0 rows after the suite
|
||||||
|
|
||||||
|
**Docs:** no doc impact — the phase is CSS-only; the README's Settings section describes the contract, not the pixels.
|
||||||
|
|
||||||
|
**Decisions:** the phase-168 suite's kill-switch app hit an unrelated owner process (`undercooked`) on port 8124 — reran with the suite's built-in `E2E_APP_PORT` override rather than touching the foreign process. Skipped the task's `git mv`/`git commit` close-out: per harness protocol the harness moves the phase dir and makes the atomic commit.
|
||||||
|
|
||||||
|
**Next pending task:** none — phase 171 is its last `todo/` phase.
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
ok
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
........................................................................ [ 1%]
|
||||||
|
........................................................................ [ 3%]
|
||||||
|
........................................................................ [ 5%]
|
||||||
|
........................................................................ [ 7%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 10%]
|
||||||
|
........................................................................ [ 12%]
|
||||||
|
........................................................................ [ 14%]
|
||||||
|
........................................................................ [ 15%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 19%]
|
||||||
|
........................................................................ [ 21%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
........................................................................ [ 24%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 28%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 31%]
|
||||||
|
........................................................................ [ 33%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 37%]
|
||||||
|
........................................................................ [ 38%]
|
||||||
|
........................................................................ [ 40%]
|
||||||
|
........................................................................ [ 42%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 47%]
|
||||||
|
........................................................................ [ 49%]
|
||||||
|
........................................................................ [ 51%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 54%]
|
||||||
|
........................................................................ [ 56%]
|
||||||
|
........................................................................ [ 58%]
|
||||||
|
........................................................................ [ 60%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 63%]
|
||||||
|
........................................................................ [ 65%]
|
||||||
|
........................................................................ [ 67%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 72%]
|
||||||
|
........................................................................ [ 74%]
|
||||||
|
........................................................................ [ 76%]
|
||||||
|
........................................................................ [ 77%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 81%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 84%]
|
||||||
|
........................................................................ [ 86%]
|
||||||
|
........................................................................ [ 88%]
|
||||||
|
........................................................................ [ 90%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 93%]
|
||||||
|
........................................................................ [ 95%]
|
||||||
|
........................................................................ [ 97%]
|
||||||
|
........................................................................ [ 99%]
|
||||||
|
..................................... [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/admin_settings.py 108 0 100%
|
||||||
|
app/api/auth.py 52 0 100%
|
||||||
|
app/api/chat.py 280 1 99%
|
||||||
|
app/api/chat_images.py 50 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 13 0 100%
|
||||||
|
app/api/doc_drafts.py 99 0 100%
|
||||||
|
app/api/docs.py 234 1 99%
|
||||||
|
app/api/git_sources.py 259 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 33 0 100%
|
||||||
|
app/api/sync.py 204 0 100%
|
||||||
|
app/api/tokens.py 40 0 100%
|
||||||
|
app/api/ui_settings.py 55 0 100%
|
||||||
|
app/config.py 285 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 124 0 100%
|
||||||
|
app/core/compression.py 59 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/http_cache.py 22 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/rate_limit.py 44 0 100%
|
||||||
|
app/core/response_budget.py 63 0 100%
|
||||||
|
app/core/security_headers.py 20 0 100%
|
||||||
|
app/core/settings_catalog.py 18 0 100%
|
||||||
|
app/core/settings_overlay.py 111 0 100%
|
||||||
|
app/core/theming.py 38 0 100%
|
||||||
|
app/core/tokens.py 44 0 100%
|
||||||
|
app/db.py 22 0 100%
|
||||||
|
app/main.py 86 0 100%
|
||||||
|
app/models.py 141 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 454 1 99%
|
||||||
|
app/rag/archive_upload.py 134 0 100%
|
||||||
|
app/rag/chunker.py 220 4 98%
|
||||||
|
app/rag/doc_dates.py 18 0 100%
|
||||||
|
app/rag/folder_summaries.py 134 0 100%
|
||||||
|
app/rag/git_sources.py 77 0 100%
|
||||||
|
app/rag/importer.py 841 4 99%
|
||||||
|
app/rag/llm.py 246 1 99%
|
||||||
|
app/rag/md_images.py 63 0 100%
|
||||||
|
app/rag/overview.py 78 0 100%
|
||||||
|
app/rag/pdf.py 192 0 100%
|
||||||
|
app/rag/prompts.py 156 0 100%
|
||||||
|
app/rag/retriever.py 252 3 99%
|
||||||
|
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 58 0 100%
|
||||||
|
app/schemas.py 532 0 100%
|
||||||
|
--------------------------------------------------
|
||||||
|
TOTAL 6392 17 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
|
||||||
|
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
|
||||||
|
|
||||||
|
validation OK
|
||||||
@@ -4674,6 +4674,23 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
|
|||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
/* Phase 171 (task 01; TODO L4): the #settings-content block —
|
||||||
|
the disabled banner, the status + error lines, the search row,
|
||||||
|
the .table-wrap card, and the Save / Clear-all action row —
|
||||||
|
stacks with ONE uniform gap: the shell's own 1.25rem
|
||||||
|
(ASSUMPTION A4: no new spacing value; the 640px mobile
|
||||||
|
stacking composes with it unchanged). The gap kills both
|
||||||
|
layout complaints at once: the search box clears the table
|
||||||
|
card (the "overlap") and the action row separates from the
|
||||||
|
table (the "smushed"). NO per-element margins: the gap is the
|
||||||
|
ONLY spacing source; the #settings-status / .settings-error
|
||||||
|
min-heights already reserve their lines (the gap is additive
|
||||||
|
and uniform). */
|
||||||
|
#settings-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
/* The catalogue: FULL-WIDTH (AGENTS.md rule 5 — the .git-sources-
|
/* The catalogue: FULL-WIDTH (AGENTS.md rule 5 — the .git-sources-
|
||||||
table language: no skinny list), one cell per column, the
|
table language: no skinny list), one cell per column, the
|
||||||
.table-wrap card + its bounded scroll + the sticky thead carry
|
.table-wrap card + its bounded scroll + the sticky thead carry
|
||||||
@@ -4691,13 +4708,27 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
|
|||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.settings-table td { white-space: nowrap; }
|
.settings-table td { white-space: nowrap; }
|
||||||
/* The group band: the --surface row, ink text (15.3:1, AA). */
|
/* Phase 171 (task 02; TODO L4, owner-locked A3): the group band.
|
||||||
|
Today's defect: background --surface — the SAME fill as the
|
||||||
|
.table-wrap card, so the band is invisible. The fix: the
|
||||||
|
--brand-soft tint (the SOLID hex the sticky thead rule
|
||||||
|
documents at .table-wrap — rows never show through, and a
|
||||||
|
themed page's #bor-theme override of the token handles the
|
||||||
|
color — B4; no new palette variable, B3), weight 700, and a
|
||||||
|
--line top hairline so a band is delimited above AND below
|
||||||
|
(the border-bottom comes from the .settings-table th family)
|
||||||
|
and can never merge with the card's top edge or the band
|
||||||
|
above it. The text stays --ink on --brand-soft — 14.8:1 (AA;
|
||||||
|
the :root documented pairing). Static, text-only, no animation
|
||||||
|
(nothing for prefers-reduced-motion to still). */
|
||||||
.settings-group-row th {
|
.settings-group-row th {
|
||||||
background: var(--surface);
|
background: var(--brand-soft);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
|
font-weight: 700;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.settings-table tbody tr.settings-row:hover td { background: var(--bg); }
|
.settings-table tbody tr.settings-row:hover td { background: var(--bg); }
|
||||||
/* The Setting cell: the label (bold) over the BOR_* name (mono,
|
/* The Setting cell: the label (bold) over the BOR_* name (mono,
|
||||||
@@ -4808,7 +4839,9 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
|
|||||||
}
|
}
|
||||||
#settings-filter:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
|
#settings-filter:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
|
||||||
/* The action row: Save (the .token-generate brand pill — the
|
/* The action row: Save (the .token-generate brand pill — the
|
||||||
§7.4 in-flight state is the dimmed :disabled + aria-busy) and
|
§7.4 in-flight state is the dimmed :disabled + aria-busy; the
|
||||||
|
disabled cursor is `default` — the .settings-reset:disabled
|
||||||
|
idiom — the wait cursor was never part of §7.4) and
|
||||||
Clear all (the .token-revoke ghost — the destructive err hover
|
Clear all (the .token-revoke ghost — the destructive err hover
|
||||||
pair; the two-step confirm reuses the .history-confirm-* rules
|
pair; the two-step confirm reuses the .history-confirm-* rules
|
||||||
unchanged). */
|
unchanged). */
|
||||||
@@ -4832,7 +4865,7 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
#settings-save:hover:not(:disabled) { background: var(--brand-hover); color: var(--bg); }
|
#settings-save:hover:not(:disabled) { background: var(--brand-hover); color: var(--bg); }
|
||||||
#settings-save:disabled { opacity: 0.6; cursor: wait; }
|
#settings-save:disabled { opacity: 0.6; cursor: default; }
|
||||||
#settings-clear-all {
|
#settings-clear-all {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
padding: 0.35rem 0.7rem;
|
padding: 0.35rem 0.7rem;
|
||||||
|
|||||||
@@ -0,0 +1,537 @@
|
|||||||
|
"""Phase 171 E2E (Playwright): the admin Settings page LAYOUT — the
|
||||||
|
four layout contracts from ``TODO.md`` L4, proven in a real browser.
|
||||||
|
|
||||||
|
Source: ``TODO.md`` L4 — "In the admin settings page: The search
|
||||||
|
settings text box overlaps settings table. The settings category
|
||||||
|
headers are not differentiated from the settings themselves, making
|
||||||
|
them confusing at a glance. The save settings and clear all
|
||||||
|
overrides are smushed right up against the settings table. The save
|
||||||
|
settings shows a loading cursor when moused over rather than just
|
||||||
|
being disabled."
|
||||||
|
|
||||||
|
Run in isolation (DB must be up: ``podman compose up -d db``):
|
||||||
|
|
||||||
|
uv run pytest tests/e2e/test_admin_settings_layout.py -v --no-cov
|
||||||
|
|
||||||
|
Test → contract mapping (one story, one phase, one isolated file):
|
||||||
|
|
||||||
|
1. ``test_search_row_is_clear_of_the_table`` — the "overlap" pin:
|
||||||
|
at 1440px and 375px the ``#settings-filter`` box and the
|
||||||
|
``.table-wrap`` card's box do not intersect (the vertical gap
|
||||||
|
between the search row's bottom and the card's top is > 0), and
|
||||||
|
the card keeps the phase-156/168 bounded-scroll + sticky-
|
||||||
|
``thead`` contract under the new ``#settings-content`` rhythm.
|
||||||
|
2. ``test_actions_row_is_gapped_from_the_table`` — the "smushed"
|
||||||
|
pin: at 1440px and 375px the ``.settings-actions`` row is gapped
|
||||||
|
from the card (the gap between the card's bottom and the action
|
||||||
|
row's top is > 0) and ``#settings-save`` / ``#settings-clear-all``
|
||||||
|
are both reachable (no clipping at either viewport).
|
||||||
|
3. ``test_group_bands_are_distinct_from_the_rows`` — the "not
|
||||||
|
differentiated" pin: the ``Retrieval & honesty`` and
|
||||||
|
``LLM (aipi)`` ``tr.settings-group-row`` bands render; a band's
|
||||||
|
computed background-color ≠ the ``.table-wrap`` card's computed
|
||||||
|
background (the ``--surface`` row-cell fill), the band's
|
||||||
|
computed font-weight is ≥ 700 and its computed border-top-width
|
||||||
|
is ≥ 1px (the hairline — no merge with the card edge or a
|
||||||
|
previous band).
|
||||||
|
4. ``test_disabled_save_is_just_disabled`` — the "loading cursor"
|
||||||
|
pin: before any edit ``#settings-save`` is disabled with
|
||||||
|
``cursor: default`` (NOT ``wait``); editing one row (a
|
||||||
|
``type="search"`` filter + a value change — the phase-168
|
||||||
|
dirty-row flow) enables it (``cursor: pointer``) and a settled
|
||||||
|
**Save settings** dims it back to ``default`` (the §7.4
|
||||||
|
dimmed-``aria-busy`` lifecycle, phase 168, intact under the new
|
||||||
|
cursor).
|
||||||
|
5. ``test_settings_page_layout_and_accessibility`` — PLAN §7.1 +
|
||||||
|
rule 6: no horizontal page overflow at 375px and 1440px, the
|
||||||
|
table is ≥80% of the 72rem container (not a skinny list), the
|
||||||
|
keyboard tab order still reaches filter → first row input →
|
||||||
|
**Save settings** → **Clear all overrides** (the phase-168
|
||||||
|
keyboard contract under the new rhythm), and the whole test
|
||||||
|
issues ZERO external requests.
|
||||||
|
|
||||||
|
DB isolation: the shared e2e Postgres keeps ``app_settings`` (the
|
||||||
|
overlay rows — a leftover row would retune every later suite's app
|
||||||
|
through the conftest pin's failure mode), ``ui_settings`` (the
|
||||||
|
single row the caching middleware reads for EVERY served page — the
|
||||||
|
effective-strings resolution ``/api/config`` uses) and ``api_tokens``
|
||||||
|
rows across suites. An autouse fixture truncates ``app_settings``
|
||||||
|
and ``ui_settings`` and deletes the ``e2e-``-labeled tokens before
|
||||||
|
AND after every test (never a TRUNCATE on ``api_tokens`` — the
|
||||||
|
shared DB may hold the owner's real tokens); the module teardown
|
||||||
|
pins ``app_settings`` empty. This suite's only write is test 4's
|
||||||
|
one-row save (the lifecycle pin) — the per-test TRUNCATE before/
|
||||||
|
after is the same idiom as phase 168, and the teardown guarantees
|
||||||
|
no leftover row. The module app opts BACK IN over the conftest's
|
||||||
|
global ``BOR_SETTINGS_DB=0`` pin (``env["BOR_SETTINGS_DB"] = "1"``
|
||||||
|
— the ``test_pictorial_pdf.py`` opt-in pattern) and pins the
|
||||||
|
branding base to DISTINCTIVE values (``Env placeholder for panel
|
||||||
|
E2E`` / ``Env footer for panel E2E``). A module-scope ``.env`` mtime
|
||||||
|
guard pins LOCKED A1: no code path the suite drives opens the env
|
||||||
|
file for writing.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from playwright.sync_api import Page, expect
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from app.config import Settings
|
||||||
|
from app.db import SessionLocal
|
||||||
|
from e2e.auth_helpers import login
|
||||||
|
from e2e.conftest import (
|
||||||
|
ADMIN_PASSWORD,
|
||||||
|
APP_PORT,
|
||||||
|
REPO,
|
||||||
|
SESSION_SECRET,
|
||||||
|
USE_REAL_LLM,
|
||||||
|
_wait_http,
|
||||||
|
)
|
||||||
|
|
||||||
|
APP_URL = f"http://127.0.0.1:{APP_PORT}"
|
||||||
|
|
||||||
|
#: The distinctive env base (the panel-suite pin): "the DB row now
|
||||||
|
#: wins" is a VISIBLE DOM change, not a no-op against the code
|
||||||
|
#: default.
|
||||||
|
ENV_PLACEHOLDER = "Env placeholder for panel E2E"
|
||||||
|
ENV_FOOTER = "Env footer for panel E2E"
|
||||||
|
|
||||||
|
#: The save's own acceptance value (test 4 — the dirty-row flow).
|
||||||
|
SAVED_PLACEHOLDER = "DB placeholder — panel saved this"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Per-module app env (the tuning/theme pattern — the test_admin_settings_panel.py
|
||||||
|
# fixture, verbatim)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _boot_app(port: int, settings_db: str) -> subprocess.Popen[bytes]:
|
||||||
|
"""One module app (the shared env block + the task's pins), on
|
||||||
|
``port`` with the overlay ``settings_db`` (``"1"`` / ``"0"``)."""
|
||||||
|
env = dict(os.environ)
|
||||||
|
env.pop("DEBUGPY", None)
|
||||||
|
env["BOR_ENVIRONMENT"] = "e2e"
|
||||||
|
env["BOR_STATIC_DIR"] = str(REPO / "frontend")
|
||||||
|
env["BOR_LLM_BASE_URL"] = (
|
||||||
|
"https://aipi.reeseapps.com/v1"
|
||||||
|
if USE_REAL_LLM
|
||||||
|
else f"http://127.0.0.1:{_MOCK_PORT}/v1"
|
||||||
|
)
|
||||||
|
# The E2E-calibrated gate pins (the conftest pattern — the mock's
|
||||||
|
# token-overlap distribution, explicit so an operator's local
|
||||||
|
# .env cannot leak a different gate into the suite's app).
|
||||||
|
env["BOR_RELEVANCE_THRESHOLD"] = "0.30"
|
||||||
|
env["BOR_LEXICAL_SUPPORT_FLOOR"] = "0.15"
|
||||||
|
env["BOR_SOURCE_USEFULNESS_FLOOR"] = "0.15"
|
||||||
|
env["BOR_LLM_RETRY_DELAY"] = "0"
|
||||||
|
env["BOR_LLM_RETRIES"] = str(Settings.model_fields["llm_retries"].default)
|
||||||
|
env.setdefault(
|
||||||
|
"BOR_DATABASE_URL",
|
||||||
|
"postgresql+psycopg://reese:reese@localhost:5432/brain_of_reese",
|
||||||
|
)
|
||||||
|
# Phase 16: admin auth must be set or create_app() refuses to boot.
|
||||||
|
env["BOR_ADMIN_PASSWORD"] = ADMIN_PASSWORD
|
||||||
|
env["BOR_SESSION_SECRET"] = SESSION_SECRET
|
||||||
|
env["BOR_DOCS_REPO"] = ""
|
||||||
|
env["BOR_SUGGESTIONS"] = json.dumps(
|
||||||
|
Settings.model_fields["suggestions"].default
|
||||||
|
)
|
||||||
|
# The app name stays the CODE default (the theme-suite pin — the
|
||||||
|
# assertions below name it).
|
||||||
|
env["BOR_APP_NAME"] = Settings.model_fields["app_name"].default
|
||||||
|
# Phase 168 (task 06): the module opts BACK IN over the conftest's
|
||||||
|
# global BOR_SETTINGS_DB=0 pin (the test_pictorial_pdf.py pattern)
|
||||||
|
# — or stays OUT for the kill-switch app (test 11).
|
||||||
|
env["BOR_SETTINGS_DB"] = settings_db
|
||||||
|
# The distinctive env base: "the DB row now wins" is a visible
|
||||||
|
# DOM change, not a no-op against the default.
|
||||||
|
env["BOR_INPUT_PLACEHOLDER"] = ENV_PLACEHOLDER
|
||||||
|
env["BOR_FOOTER_TEXT"] = ENV_FOOTER
|
||||||
|
env["BOR_GIT_SOURCES"] = ""
|
||||||
|
return subprocess.Popen(
|
||||||
|
[sys.executable, "-m", "uvicorn", "app.main:app",
|
||||||
|
"--host", "127.0.0.1", "--port", str(port), "--log-level", "warning"],
|
||||||
|
cwd=REPO,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _stop_app(proc: subprocess.Popen[bytes]) -> None:
|
||||||
|
proc.terminate()
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=10)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def app_server(mock_llm: int) -> Iterator[str]:
|
||||||
|
"""The real app under test — per-module env: the branding vars
|
||||||
|
pinned to the DISTINCTIVE E2E values (the panel's saved value
|
||||||
|
must visibly WIN over them) and ``BOR_SETTINGS_DB=1`` (the
|
||||||
|
module opts back in over the conftest pin)."""
|
||||||
|
global _MOCK_PORT
|
||||||
|
_MOCK_PORT = mock_llm
|
||||||
|
proc = _boot_app(APP_PORT, "1")
|
||||||
|
try:
|
||||||
|
_wait_http(f"{APP_URL}/api/health")
|
||||||
|
yield APP_URL
|
||||||
|
finally:
|
||||||
|
_stop_app(proc)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def app_url(app_server: str) -> str:
|
||||||
|
return app_server
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# DB isolation + helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_settings_state() -> None:
|
||||||
|
"""Fresh settings + theme + token state per test: truncate
|
||||||
|
``app_settings`` (a leftover row is the cross-suite poison this
|
||||||
|
phase's D6 exists to prevent) and the single-row
|
||||||
|
``ui_settings`` (the middleware reads it for EVERY page — the
|
||||||
|
effective-strings resolution ``/api/config`` uses), and delete
|
||||||
|
this suite's issued tokens (label-scoped on ``e2e-`` — never a
|
||||||
|
TRUNCATE: the shared DB may hold the owner's real tokens)."""
|
||||||
|
with SessionLocal() as db:
|
||||||
|
db.execute(text("TRUNCATE app_settings"))
|
||||||
|
db.execute(text("TRUNCATE ui_settings"))
|
||||||
|
db.execute(text("DELETE FROM api_tokens WHERE label LIKE 'e2e-%'"))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _clean(db_ready: None) -> Iterator[None]:
|
||||||
|
_clean_settings_state()
|
||||||
|
yield
|
||||||
|
_clean_settings_state()
|
||||||
|
# The module's final state: app_settings EMPTY (the D6 poison
|
||||||
|
# guard — the next suite's app must boot with no overrides).
|
||||||
|
with SessionLocal() as db:
|
||||||
|
assert db.execute(text("SELECT count(*) FROM app_settings")).scalar() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
def _env_never_written() -> Iterator[None]:
|
||||||
|
"""LOCKED A1, mtime-pinned: no code path the suite drives opens
|
||||||
|
``.env`` for writing (the file is gitignored — a ``git status``
|
||||||
|
check cannot see it, so ``stat``'s mtime is the assertion)."""
|
||||||
|
env_file = REPO / ".env"
|
||||||
|
before = env_file.stat().st_mtime_ns if env_file.exists() else None
|
||||||
|
yield
|
||||||
|
after = env_file.stat().st_mtime_ns if env_file.exists() else None
|
||||||
|
assert before == after, "the app under test must never write .env"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. The "overlap" pin: the search row clears the table card (and the
|
||||||
|
# card keeps its bounded scroll + sticky thead under the new
|
||||||
|
# rhythm)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_row_is_clear_of_the_table(page: Page, app_url: str, db_ready: None) -> None:
|
||||||
|
page.set_default_timeout(30_000)
|
||||||
|
login(page, app_url, next="/settings.html")
|
||||||
|
expect(page.locator("#settings-content")).to_be_visible(timeout=15_000)
|
||||||
|
|
||||||
|
for width in (1440, 375):
|
||||||
|
page.set_viewport_size({"width": width, "height": 900})
|
||||||
|
page.wait_for_timeout(150) # the reflow settles before the measure
|
||||||
|
|
||||||
|
filter_box = page.locator("#settings-filter").bounding_box()
|
||||||
|
search_box = page.locator(".settings-search").bounding_box()
|
||||||
|
card_box = page.locator(".settings-shell .table-wrap").bounding_box()
|
||||||
|
assert filter_box is not None, f"the filter input must render at {width}px"
|
||||||
|
assert search_box is not None, f"the search row must render at {width}px"
|
||||||
|
assert card_box is not None, f"the table card must render at {width}px"
|
||||||
|
|
||||||
|
# The "overlap" pin: the filter input's box does not intersect
|
||||||
|
# the card's box…
|
||||||
|
assert filter_box["y"] + filter_box["height"] <= card_box["y"], (
|
||||||
|
f"the search input overlaps the table card at {width}px "
|
||||||
|
f"(input bottom {filter_box['y'] + filter_box['height']:.1f} "
|
||||||
|
f"vs card top {card_box['y']:.1f})"
|
||||||
|
)
|
||||||
|
# …and the row as a whole clears the card: the vertical gap
|
||||||
|
# between the search row's bottom and the card's top is > 0
|
||||||
|
# (the #settings-content flex gap, phase 171 task 01).
|
||||||
|
gap = card_box["y"] - (search_box["y"] + search_box["height"])
|
||||||
|
assert gap > 0, (
|
||||||
|
f"the search row must clear the table card at {width}px "
|
||||||
|
f"(gap {gap:.1f}px)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# The card keeps the phase-156/168 contract under the new
|
||||||
|
# rhythm: the sticky thead (the bounded scroll's sticky
|
||||||
|
# header) is intact.
|
||||||
|
sticky = page.evaluate(
|
||||||
|
"() => getComputedStyle("
|
||||||
|
"document.querySelector("
|
||||||
|
"'.settings-shell .table-wrap .settings-table thead th'"
|
||||||
|
")).position"
|
||||||
|
)
|
||||||
|
assert sticky == "sticky", (
|
||||||
|
f"the thead th must stay sticky at {width}px (got {sticky!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. The "smushed" pin: the action row is gapped from the table card
|
||||||
|
# and both action buttons are reachable (no clipping)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_actions_row_is_gapped_from_the_table(page: Page, app_url: str, db_ready: None) -> None:
|
||||||
|
page.set_default_timeout(30_000)
|
||||||
|
login(page, app_url, next="/settings.html")
|
||||||
|
expect(page.locator("#settings-content")).to_be_visible(timeout=15_000)
|
||||||
|
|
||||||
|
for width in (1440, 375):
|
||||||
|
page.set_viewport_size({"width": width, "height": 900})
|
||||||
|
page.wait_for_timeout(150) # the reflow settles before the measure
|
||||||
|
|
||||||
|
card_box = page.locator(".settings-shell .table-wrap").bounding_box()
|
||||||
|
actions_box = page.locator(".settings-actions").bounding_box()
|
||||||
|
assert card_box is not None, f"the table card must render at {width}px"
|
||||||
|
assert actions_box is not None, f"the action row must render at {width}px"
|
||||||
|
|
||||||
|
# The "smushed" pin: the gap between the card's bottom and
|
||||||
|
# the action row's top is > 0 (the #settings-content flex
|
||||||
|
# gap, phase 171 task 01).
|
||||||
|
gap = actions_box["y"] - (card_box["y"] + card_box["height"])
|
||||||
|
assert gap > 0, (
|
||||||
|
f"the action row must be gapped from the table card at "
|
||||||
|
f"{width}px (gap {gap:.1f}px)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Both action buttons are reachable — visible, non-degenerate,
|
||||||
|
# and not clipped horizontally at either viewport (at 375px
|
||||||
|
# the media query stacks the row; the buttons must not be
|
||||||
|
# cut off).
|
||||||
|
for selector in ("#settings-save", "#settings-clear-all"):
|
||||||
|
expect(page.locator(selector)).to_be_visible()
|
||||||
|
box = page.locator(selector).bounding_box()
|
||||||
|
assert box is not None, f"{selector} must render at {width}px"
|
||||||
|
assert box["width"] > 0 and box["height"] > 0, (
|
||||||
|
f"{selector} must be non-degenerate at {width}px"
|
||||||
|
)
|
||||||
|
assert box["x"] >= 0, f"{selector} is clipped off-canvas at {width}px"
|
||||||
|
assert box["x"] + box["width"] <= width + 1, (
|
||||||
|
f"{selector} is clipped horizontally at {width}px "
|
||||||
|
f"(right edge {box['x'] + box['width']:.1f} > {width})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. The "not differentiated" pin: the group bands are distinct from
|
||||||
|
# the rows (the --brand-soft tint, weight 700, the top hairline)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_bands_are_distinct_from_the_rows(
|
||||||
|
page: Page, app_url: str, db_ready: None
|
||||||
|
) -> None:
|
||||||
|
page.set_default_timeout(30_000)
|
||||||
|
login(page, app_url, next="/settings.html")
|
||||||
|
expect(page.locator("#settings-content")).to_be_visible(timeout=15_000)
|
||||||
|
|
||||||
|
# The pinned groups render (in the server's order — at least
|
||||||
|
# these two).
|
||||||
|
expect(
|
||||||
|
page.locator("tr.settings-group-row", has_text="Retrieval & honesty")
|
||||||
|
).to_be_visible()
|
||||||
|
expect(page.locator("tr.settings-group-row", has_text="LLM (aipi)")).to_be_visible()
|
||||||
|
|
||||||
|
# The "not differentiated" pin: a band's computed
|
||||||
|
# background-color ≠ the .table-wrap card's computed background
|
||||||
|
# (the --surface row-cell fill), the band's font-weight is ≥
|
||||||
|
# 700, and the band carries a ≥1px top hairline (no merge with
|
||||||
|
# the card's top edge or the band above it).
|
||||||
|
band = page.evaluate(
|
||||||
|
"""() => {
|
||||||
|
const th = document.querySelector('tr.settings-group-row th');
|
||||||
|
const card = document.querySelector('.settings-shell .table-wrap');
|
||||||
|
const t = getComputedStyle(th);
|
||||||
|
return {
|
||||||
|
band: t.backgroundColor,
|
||||||
|
card: getComputedStyle(card).backgroundColor,
|
||||||
|
weight: t.fontWeight,
|
||||||
|
borderTop: parseFloat(t.borderTopWidth),
|
||||||
|
};
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
assert band["band"] != band["card"], (
|
||||||
|
f"the band must be tinted distinct from the card "
|
||||||
|
f"({band['band']!r} vs {band['card']!r})"
|
||||||
|
)
|
||||||
|
assert int(band["weight"]) >= 700, (
|
||||||
|
f"the band must be weight ≥ 700 (got {band['weight']!r})"
|
||||||
|
)
|
||||||
|
assert float(band["borderTop"]) >= 1.0, (
|
||||||
|
f"the band must carry a ≥1px top hairline (got {band['borderTop']!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. The "loading cursor" pin: a disabled Save is JUST disabled
|
||||||
|
# (cursor: default, not wait) — the §7.4 lifecycle intact under
|
||||||
|
# the new cursor
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_save_is_just_disabled(page: Page, app_url: str, db_ready: None) -> None:
|
||||||
|
page.set_default_timeout(30_000)
|
||||||
|
login(page, app_url, next="/settings.html")
|
||||||
|
expect(page.locator("#settings-content")).to_be_visible(timeout=15_000)
|
||||||
|
|
||||||
|
# The search input is a type="search" control (the phase-168
|
||||||
|
# client-side filter row)…
|
||||||
|
assert page.locator("#settings-filter").get_attribute("type") == "search"
|
||||||
|
|
||||||
|
# Before any edit: Save is disabled and its computed cursor is
|
||||||
|
# `default` — NOT `wait` (the loading-cursor pin, TODO L4).
|
||||||
|
expect(page.locator("#settings-save")).to_be_disabled()
|
||||||
|
cursor = page.evaluate(
|
||||||
|
"() => getComputedStyle(document.querySelector('#settings-save')).cursor"
|
||||||
|
)
|
||||||
|
assert cursor == "default", (
|
||||||
|
f"a disabled Save must show `default` (got {cursor!r})"
|
||||||
|
)
|
||||||
|
assert cursor != "wait", "the loading cursor must be gone"
|
||||||
|
|
||||||
|
# Edit one row (the phase-168 dirty-row flow): filter to the row,
|
||||||
|
# change the value…
|
||||||
|
page.fill("#settings-filter", "placeholder")
|
||||||
|
expect(page.locator('tr[data-key="input_placeholder"]')).to_be_visible()
|
||||||
|
page.fill('tr[data-key="input_placeholder"] .settings-control', SAVED_PLACEHOLDER)
|
||||||
|
expect(page.locator("#settings-dirty")).to_have_text("1 unsaved changes")
|
||||||
|
# …the button ENABLES — cursor `pointer` (a live control)…
|
||||||
|
expect(page.locator("#settings-save")).to_be_enabled()
|
||||||
|
cursor = page.evaluate(
|
||||||
|
"() => getComputedStyle(document.querySelector('#settings-save')).cursor"
|
||||||
|
)
|
||||||
|
assert cursor == "pointer", (
|
||||||
|
f"an enabled Save must show `pointer` (got {cursor!r})"
|
||||||
|
)
|
||||||
|
# …Save settings (the §7.4 in-flight state is the dimmed
|
||||||
|
# :disabled + aria-busy) and wait for it to settle…
|
||||||
|
page.click("#settings-save")
|
||||||
|
expect(page.locator("#settings-status")).to_contain_text(
|
||||||
|
"Saved 1 settings.", timeout=30_000
|
||||||
|
)
|
||||||
|
# …the settled state: back to disabled, dimmed (opacity 0.6),
|
||||||
|
# the aria-busy removed, and the cursor `default` again (the
|
||||||
|
# §7.4 lifecycle, phase 168, intact under the new cursor).
|
||||||
|
expect(page.locator("#settings-save")).to_be_disabled()
|
||||||
|
expect(page.locator("#settings-save")).not_to_have_attribute("aria-busy")
|
||||||
|
style = page.evaluate(
|
||||||
|
"""() => {
|
||||||
|
const s = getComputedStyle(document.querySelector('#settings-save'));
|
||||||
|
return { cursor: s.cursor, opacity: s.opacity };
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
assert style["cursor"] == "default", (
|
||||||
|
f"the settled Save must show `default` (got {style['cursor']!r})"
|
||||||
|
)
|
||||||
|
assert float(style["opacity"]) == 0.6, (
|
||||||
|
f"the settled Save must be dimmed to 0.6 (got {style['opacity']!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5. PLAN §7.1 + rule 6: no horizontal overflow at 375/1440, the
|
||||||
|
# table is ≥80% of the container, the keyboard walk, zero
|
||||||
|
# external requests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_page_layout_and_accessibility(
|
||||||
|
page: Page, app_url: str, db_ready: None
|
||||||
|
) -> None:
|
||||||
|
page.set_default_timeout(30_000)
|
||||||
|
externals: list[str] = []
|
||||||
|
|
||||||
|
def _watch(request: object) -> None:
|
||||||
|
url = getattr(request, "url", "")
|
||||||
|
if url.startswith(("http://", "https://")) and "127.0.0.1" not in url:
|
||||||
|
externals.append(url)
|
||||||
|
|
||||||
|
page.on("request", _watch)
|
||||||
|
login(page, app_url, next="/settings.html")
|
||||||
|
expect(page.locator("#settings-content")).to_be_visible(timeout=15_000)
|
||||||
|
|
||||||
|
# A dirty row (typed, never saved — the suite leaves no
|
||||||
|
# override): the walk's Save must be ENABLED (a disabled
|
||||||
|
# button is not tabbable).
|
||||||
|
page.fill('tr[data-key="app_name"] .settings-control', "E2E layout brain")
|
||||||
|
expect(page.locator("#settings-save")).to_be_enabled()
|
||||||
|
|
||||||
|
# No horizontal page overflow at 375px or at 1440px (the 72rem
|
||||||
|
# column at every viewport — the table scrolls INSIDE its card,
|
||||||
|
# the page itself never does).
|
||||||
|
for width in (375, 1440):
|
||||||
|
page.set_viewport_size({"width": width, "height": 900})
|
||||||
|
page.wait_for_timeout(150) # the reflow settles before the measure
|
||||||
|
overflow = page.evaluate(
|
||||||
|
"() => document.documentElement.scrollWidth <= window.innerWidth + 1"
|
||||||
|
)
|
||||||
|
assert overflow, f"horizontal overflow at {width}px"
|
||||||
|
|
||||||
|
# At 1440 the table is ≥80% of the 72rem container (not a
|
||||||
|
# skinny wasted-space list — PLAN §7.1).
|
||||||
|
ratio = page.evaluate(
|
||||||
|
"""() => document.querySelector('.settings-shell .table-wrap')
|
||||||
|
.getBoundingClientRect().width /
|
||||||
|
document.querySelector('.settings-shell').getBoundingClientRect().width"""
|
||||||
|
)
|
||||||
|
assert ratio >= 0.8, f"the table must be ≥80% of the container (got {ratio:.2f})"
|
||||||
|
|
||||||
|
# The keyboard tab order still reaches filter → first row input →
|
||||||
|
# Save settings → Clear all overrides (the phase-168 keyboard
|
||||||
|
# contract under the new rhythm). The walk starts at the
|
||||||
|
# skip-link — the document's FIRST tabbable element (a blur()
|
||||||
|
# would only continue the cycle after the last focused element,
|
||||||
|
# so the script focuses the cycle start; every move after it is
|
||||||
|
# a real Tab keypress).
|
||||||
|
page.set_viewport_size({"width": 1280, "height": 800})
|
||||||
|
page.evaluate("() => document.querySelector('a.skip-link').focus()")
|
||||||
|
seen: list[str] = []
|
||||||
|
for _ in range(200):
|
||||||
|
page.keyboard.press("Tab")
|
||||||
|
seen.append(
|
||||||
|
page.evaluate(
|
||||||
|
"""() => {
|
||||||
|
const el = document.activeElement;
|
||||||
|
if (!el) return "";
|
||||||
|
return el.id || (typeof el.className === "string" ? el.className : "");
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
i_filter = next((i for i, s in enumerate(seen) if s == "settings-filter"), None)
|
||||||
|
i_first_input = next((i for i, s in enumerate(seen) if s.startswith("set-")), None)
|
||||||
|
i_save = next((i for i, s in enumerate(seen) if s == "settings-save"), None)
|
||||||
|
i_clear = next((i for i, s in enumerate(seen) if s == "settings-clear-all"), None)
|
||||||
|
assert i_filter is not None, f"the walk never reached the filter (seen={seen[:20]})"
|
||||||
|
assert i_first_input is not None, "the walk never reached a settings control"
|
||||||
|
assert i_save is not None, "the walk never reached Save (the dirty row enables it)"
|
||||||
|
assert i_clear is not None, "the walk never reached Clear all"
|
||||||
|
assert i_filter < i_first_input, "the filter must precede the first input"
|
||||||
|
assert i_first_input < i_save, "the first input must precede Save"
|
||||||
|
assert i_save < i_clear, "Save must precede Clear all"
|
||||||
|
|
||||||
|
# Rule 6: the whole test issued ZERO external requests (the CSP
|
||||||
|
# default-src 'self' posture — every asset is local).
|
||||||
|
assert externals == [], f"external requests leaked: {externals}"
|
||||||
@@ -41,6 +41,25 @@ Pinned (task 05 spec):
|
|||||||
``.settings-src.is-*`` variants + the dirty-row marker and adds
|
``.settings-src.is-*`` variants + the dirty-row marker and adds
|
||||||
NO new ``:root`` variable (the phase-156 B3 rule — the badges
|
NO new ``:root`` variable (the phase-156 B3 rule — the badges
|
||||||
are solid existing tokens).
|
are solid existing tokens).
|
||||||
|
* phase 171 (task 01): ``styles.css`` styles ``#settings-content``
|
||||||
|
as a flex column with the shell's own 1.25rem gap (the ONE
|
||||||
|
uniform gap — the search row and the action row keep no
|
||||||
|
margin), and the four 640px stacking rules survive unchanged.
|
||||||
|
* phase 171 (task 02): the ``.settings-group-row th`` band is the
|
||||||
|
``--brand-soft`` tint (the sticky-th solid — B3: no new palette
|
||||||
|
variable), weight 700, and hairline-delimited above (a
|
||||||
|
``--line`` border-top) — the band reads as a category at a
|
||||||
|
glance and never merges with the card edge or the band above
|
||||||
|
it (the ``.settings-table th`` family supplies the bottom
|
||||||
|
border); the row cells stay on the card's ``--surface``.
|
||||||
|
* phase 171 (task 03): a disabled ``#settings-save`` is *just*
|
||||||
|
disabled — ``cursor: default`` (the ``.settings-reset:disabled``
|
||||||
|
idiom; the ``wait`` loading cursor is gone), the §7.4 dim
|
||||||
|
(``opacity: 0.6``) + ``aria-busy`` in-flight state intact, and
|
||||||
|
``cursor: wait`` is absent from every ``.settings*`` /
|
||||||
|
``#settings-*`` rule (the negative pin is scoped to the
|
||||||
|
settings rules — the chat edit form's ``.edit-save`` keeps its
|
||||||
|
own ``wait`` and is out of scope).
|
||||||
* the FOUR pages that carry the Theme link (index / document /
|
* the FOUR pages that carry the Theme link (index / document /
|
||||||
login / shared) now carry the Settings link too (the phase-34
|
login / shared) now carry the Settings link too (the phase-34
|
||||||
one-bar contract — test_nav_consistency pins the parity).
|
one-bar contract — test_nav_consistency pins the parity).
|
||||||
@@ -564,6 +583,187 @@ def test_styles_css_settings_block_uses_existing_tokens_only() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- phase 171 (task 01): the #settings-content rhythm ----------
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_content_is_a_flex_column_with_the_shell_gap() -> None:
|
||||||
|
"""Phase 171 (task 01; TODO L4): ``#settings-content`` — the
|
||||||
|
disabled banner, the status + error lines, the search row, the
|
||||||
|
``.table-wrap`` card, and the Save / Clear-all row — is a
|
||||||
|
flex column with the shell's own 1.25rem gap (ASSUMPTION A4:
|
||||||
|
no new spacing value). ONE uniform gap kills both layout
|
||||||
|
complaints: the search box clears the table card (the
|
||||||
|
"overlap") and the action row separates from the table (the
|
||||||
|
"smushed"). The gap is the ONLY spacing source: the search
|
||||||
|
row and the action row keep NO margin, and the 640px mobile
|
||||||
|
stacking composes with it unchanged."""
|
||||||
|
css = _read(ASSETS / "styles.css")
|
||||||
|
rule = re.search(r"#settings-content \{([\s\S]*?)\n\}", css)
|
||||||
|
assert rule, (
|
||||||
|
"styles.css must style #settings-content (the phase 171 rhythm rule)"
|
||||||
|
)
|
||||||
|
body = rule.group(1)
|
||||||
|
for decl in (
|
||||||
|
"display: flex",
|
||||||
|
"flex-direction: column",
|
||||||
|
"gap: 1.25rem",
|
||||||
|
):
|
||||||
|
assert decl in body, (
|
||||||
|
f"#settings-content must declare {decl} (the shell's rhythm, A4)"
|
||||||
|
)
|
||||||
|
# The gap is the ONLY spacing source (the task's verification
|
||||||
|
# by reading, pinned): the search row and the action row got
|
||||||
|
# NO new margin — the .settings-status / .settings-error
|
||||||
|
# min-heights reserve their own lines, the gap is additive.
|
||||||
|
for selector in (".settings-search", ".settings-actions"):
|
||||||
|
srule = re.search(rf"{re.escape(selector)} \{{([\s\S]*?)\n\}}", css)
|
||||||
|
assert srule, f"styles.css must style {selector}"
|
||||||
|
assert "margin" not in srule.group(1), (
|
||||||
|
f"{selector} keeps no margin — the #settings-content gap is "
|
||||||
|
"the only spacing source (phase 171 task 01)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- phase 171 (task 02): the group band is distinct ----------
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_group_band_is_branded_bold_hairlined() -> None:
|
||||||
|
"""Phase 171 (task 02; TODO L4, owner-locked A3): the
|
||||||
|
``.settings-group-row th`` band — today ``background:
|
||||||
|
var(--surface)`` (the SAME fill as the ``.table-wrap`` card,
|
||||||
|
i.e. invisible) — becomes a band that reads as a category at a
|
||||||
|
glance: the ``--brand-soft`` tint (the SOLID hex the sticky
|
||||||
|
thead rule already documents — B3: no new palette variable;
|
||||||
|
a themed page's ``#bor-theme`` override of the token handles
|
||||||
|
the color — B4), weight 700, and a ``--line`` top hairline so
|
||||||
|
a band is delimited above AND below (the ``border-bottom``
|
||||||
|
comes from the ``.settings-table th`` family) and can never
|
||||||
|
merge with the card's top edge or the band above it. The
|
||||||
|
text stays ``--ink`` on ``--brand-soft`` (14.8:1, AA). Static,
|
||||||
|
text-only, no animation — and the rule keeps its existing
|
||||||
|
0.82rem / uppercase / letter-spacing typography."""
|
||||||
|
css = _read(ASSETS / "styles.css")
|
||||||
|
rule = re.search(r"\.settings-group-row th \{([\s\S]*?)\n\}", css)
|
||||||
|
assert rule, (
|
||||||
|
"styles.css must style .settings-group-row th (the group band)"
|
||||||
|
)
|
||||||
|
body = rule.group(1)
|
||||||
|
for decl in (
|
||||||
|
"background: var(--brand-soft)",
|
||||||
|
"font-weight: 700",
|
||||||
|
"border-top: 1px solid var(--line)",
|
||||||
|
):
|
||||||
|
assert decl in body, (
|
||||||
|
f".settings-group-row th must declare {decl} (the phase 171 "
|
||||||
|
"band — A3)"
|
||||||
|
)
|
||||||
|
# The old invisible fill is gone (the band is distinct from the
|
||||||
|
# row cells' --surface by construction).
|
||||||
|
assert "background: var(--surface)" not in body, (
|
||||||
|
"the band must not sit on the card's --surface (the phase-168 "
|
||||||
|
"invisible-band defect)"
|
||||||
|
)
|
||||||
|
# The text pairing + the existing typography survive.
|
||||||
|
assert "color: var(--ink)" in body, (
|
||||||
|
"the band text stays --ink (14.8:1 on --brand-soft, AA)"
|
||||||
|
)
|
||||||
|
for decl in (
|
||||||
|
"font-size: 0.82rem",
|
||||||
|
"text-transform: uppercase",
|
||||||
|
"letter-spacing: 0.04em",
|
||||||
|
):
|
||||||
|
assert decl in body, f"the band keeps {decl} (task 02: typography unchanged)"
|
||||||
|
# The bottom delimiter is the .settings-table th family's
|
||||||
|
# border-bottom (the band is delimited above AND below) — pinned
|
||||||
|
# so the band can never merge with the row beneath it.
|
||||||
|
table_th = re.search(r"\.settings-table th, \.settings-table td \{([\s\S]*?)\n\}", css)
|
||||||
|
assert table_th, "the .settings-table th/td family rule must exist"
|
||||||
|
assert "border-bottom: 1px solid var(--line)" in table_th.group(1), (
|
||||||
|
"the .settings-table th family supplies the band's bottom border"
|
||||||
|
)
|
||||||
|
# Static, text-only, no animation (nothing for
|
||||||
|
# prefers-reduced-motion to still).
|
||||||
|
assert "transition" not in body and "animation" not in body, (
|
||||||
|
"the band is static (no animation)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- phase 171 (task 03): a disabled Save is just disabled ----------
|
||||||
|
|
||||||
|
|
||||||
|
def _settings_rule_bodies(css: str) -> list[str]:
|
||||||
|
"""Every CSS rule body whose selector part is a settings-page
|
||||||
|
selector (``.settings*`` / ``#settings-*``) — the scope of the
|
||||||
|
task-03 negative pin. Comments are stripped from the selector
|
||||||
|
part before matching; rules inside the 640px media query match
|
||||||
|
on their own selector (the media preamble is a separate match
|
||||||
|
and is excluded). Non-settings controls (the chat edit form's
|
||||||
|
``.edit-save`` keeps its own ``cursor: wait`` — a different
|
||||||
|
control, out of scope) never match."""
|
||||||
|
bodies: list[str] = []
|
||||||
|
for m in re.finditer(r"([^{}]+)\{([^{}]*)\}", css):
|
||||||
|
sel = re.sub(r"/\*[\s\S]*?\*/", "", m.group(1)).strip()
|
||||||
|
if not sel:
|
||||||
|
continue
|
||||||
|
parts = [p.strip() for p in sel.split(",")]
|
||||||
|
if any(p.startswith((".settings", "#settings-")) for p in parts):
|
||||||
|
bodies.append(m.group(2))
|
||||||
|
return bodies
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_save_disabled_is_just_disabled_no_wait_cursor() -> None:
|
||||||
|
"""Phase 171 (task 03; TODO L4, owner's words): a disabled
|
||||||
|
**Save settings** is *just* disabled — its computed cursor is
|
||||||
|
``default`` (the ``.settings-reset:disabled`` idiom), NOT the
|
||||||
|
``wait`` (loading) cursor the owner saw on hover. The §7.4
|
||||||
|
in-flight state survives unchanged: the **dimmed** ``:disabled``
|
||||||
|
(``opacity: 0.6``) + ``aria-busy`` — the cursor was never part
|
||||||
|
of the contract. The ``#settings-save:hover:not(:disabled)``
|
||||||
|
rule is byte-unchanged (the §7.4 lifecycle's ``cursor: pointer``
|
||||||
|
when enabled). And ``cursor: wait`` is ABSENT from every
|
||||||
|
settings-page rule (the house negative-pin idiom, scoped to the
|
||||||
|
``.settings*`` / ``#settings-*`` selectors — the chat edit
|
||||||
|
form's ``.edit-save`` rule keeps its own ``wait`` and must not
|
||||||
|
fail this pin)."""
|
||||||
|
css = _read(ASSETS / "styles.css")
|
||||||
|
rule = re.search(r"#settings-save:disabled \{([^{}]*)\}", css)
|
||||||
|
assert rule, "styles.css must style #settings-save:disabled"
|
||||||
|
body = rule.group(1)
|
||||||
|
assert "cursor: default" in body, (
|
||||||
|
"#settings-save:disabled must be cursor: default (the "
|
||||||
|
".settings-reset:disabled idiom — just disabled, task 03)"
|
||||||
|
)
|
||||||
|
assert "opacity: 0.6" in body, (
|
||||||
|
"the §7.4 dim survives (opacity 0.6 — the in-flight state)"
|
||||||
|
)
|
||||||
|
assert "cursor: wait" not in body, (
|
||||||
|
"the wait (loading) cursor is gone from the disabled save"
|
||||||
|
)
|
||||||
|
# The hover rule is byte-unchanged (task 03: the :not(:disabled)
|
||||||
|
# guard + the brand hover pair stand).
|
||||||
|
hover = re.search(
|
||||||
|
r"#settings-save:hover:not\(:disabled\) \{([^{}]*)\}", css
|
||||||
|
)
|
||||||
|
assert hover, "the #settings-save:hover:not(:disabled) rule must exist"
|
||||||
|
hbody = hover.group(1)
|
||||||
|
assert "background: var(--brand-hover)" in hbody
|
||||||
|
assert "color: var(--bg)" in hbody
|
||||||
|
assert "cursor: wait" not in hbody
|
||||||
|
# The idiom it mirrors stands (the .settings-reset:disabled
|
||||||
|
# cursor: default the comment references is live).
|
||||||
|
reset = re.search(r"\.settings-reset:disabled \{([^{}]*)\}", css)
|
||||||
|
assert reset and "cursor: default" in reset.group(1), (
|
||||||
|
"the .settings-reset:disabled idiom (cursor: default) must stand"
|
||||||
|
)
|
||||||
|
# The negative pin: no settings-page rule (a .settings* /
|
||||||
|
# #settings-* selector) may paint cursor: wait.
|
||||||
|
for b in _settings_rule_bodies(css):
|
||||||
|
assert "cursor: wait" not in b, (
|
||||||
|
"no settings-page rule may paint cursor: wait (phase 171 "
|
||||||
|
f"task 03): {b.strip()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------- the four Theme-bearing pages carry the Settings link ----------
|
# ---------- the four Theme-bearing pages carry the Settings link ----------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user