From 58e9d94cffcccb7bd3523de4dae66c609fbbfed5 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Sat, 12 Sep 2026 13:37:19 -0400 Subject: [PATCH] phase: 100_page_width_consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Phase 100 — final verification pass: all green.** - Verified the shipped CSS contract directly: `--chat-column: 72rem` in `:root`; 0 literal `max-width: 46rem`; no `@media (min-width: 1500px)` block; exactly 4 token-capped reading columns; tuning/theme/doc-edit shells cap-free, structurally `.sources-shell`; `mock_llm.py` diff is comment-only. - Defect found & fixed (phase-93 suite): `test_theme_semantic_completion.py::test_reset_removes_tag_byte_identical` raced theme.js's post-PUT refetch — it asserted the `#bor-theme` tag was gone right after the result line, but tag removal lands in the reconcile after the re-fetch (failed ~1 in 5 runs after `test_chat_rag`, reproduced). Fixed with an auto-waiting `expect(...).to_have_count(0)` (settled-state wait, the file's existing house pattern). 6/6 clean on the repro loop after. - Tests/lint/coverage: `uv run pytest --cov=app --cov-report=term-missing` → 2052 passed, **99%** on `app/`; `uv run ruff check .` + `uv run pyright` → 0 errors. - E2E in isolation (all passed): `test_wide_desktop_column.py` 3 (the phase suite — chat==tuning==theme==RAG ±4px at 1280 & 1920, ≈1152px; shared ≈1152px; standalone doc ≈1112px; modal unchanged ≈1100px; 360px overflow-free), `test_ui_customization` 4, `test_admin_theme_tab` 5, `test_document_viewer` 7, `test_save_share_ux` 5, `test_sticky_navbar` 3, `test_markdown_tables` 6, `test_responsive_polish` 7, `test_chat_rag` 3, `test_theme_semantic_completion` 8. - Completion criteria: ① measured 72rem everywhere (≥~1200px, ±4px) + full-width below + 360px clean — **PASS** (E2E); ② zero 46rem rules / no 1500px block / four token selectors — **PASS** (grep + unit pins); ③ B4 byte-identical no-op + mobile squeeze — **PASS** (theme + responsive suites green); ④ full suite / coverage / lint — **PASS**; ⑤ atomic commit — left to the harness per executor protocol (all changes in the working tree, uncommitted). - Deviations: none from phase decisions; only change in this pass is the race fix above (test-only, behavior unchanged). - Next pending phase: `98_sync_summary_visibility` (numeric order in `todo/`; also pending: 99, 101–105). --- .../100_page_width_consistency/00_phase.md | 0 .../01_width_css.md | 0 .../02_width_unit_pins.md | 0 .../03_e2e_width_sweep.md | 0 ...00_page_width_consistency__00_phase.a1.err | 0 ...100_page_width_consistency__00_phase.a1.md | 9 + ...ge_width_consistency__00_phase.a1.validate | 96 ++++ ...age_width_consistency__01_width_css.a1.err | 0 ...page_width_consistency__01_width_css.a1.md | 18 + ...idth_consistency__01_width_css.a1.validate | 295 ++++++++++++ ...age_width_consistency__01_width_css.a2.err | 0 ...page_width_consistency__01_width_css.a2.md | 13 + ...idth_consistency__01_width_css.a2.validate | 96 ++++ ...dth_consistency__02_width_unit_pins.a1.err | 0 ...idth_consistency__02_width_unit_pins.a1.md | 14 + ...onsistency__02_width_unit_pins.a1.validate | 96 ++++ ...dth_consistency__03_e2e_width_sweep.a1.err | 0 ...idth_consistency__03_e2e_width_sweep.a1.md | 18 + ...onsistency__03_e2e_width_sweep.a1.validate | 96 ++++ frontend/assets/styles.css | 149 +++--- tests/e2e/mock_llm.py | 9 +- tests/e2e/test_document_viewer.py | 15 +- tests/e2e/test_markdown_tables.py | 7 +- tests/e2e/test_responsive_polish.py | 43 +- tests/e2e/test_save_share_ux.py | 5 +- tests/e2e/test_sticky_navbar.py | 2 +- tests/e2e/test_theme_semantic_completion.py | 9 +- tests/e2e/test_wide_desktop_column.py | 431 ++++++++++-------- tests/unit/test_doc_edit_screen.py | 27 +- tests/unit/test_markdown_tables.py | 3 +- tests/unit/test_pinned_composer.py | 4 +- tests/unit/test_remove_confirm_modal.py | 9 +- tests/unit/test_shared_page.py | 17 +- tests/unit/test_source_ignore_paths.py | 7 +- tests/unit/test_wide_column_css.py | 235 +++++----- 35 files changed, 1295 insertions(+), 428 deletions(-) rename .agents/phases/{todo => complete}/100_page_width_consistency/00_phase.md (100%) rename .agents/phases/{todo => complete}/100_page_width_consistency/01_width_css.md (100%) rename .agents/phases/{todo => complete}/100_page_width_consistency/02_width_unit_pins.md (100%) rename .agents/phases/{todo => complete}/100_page_width_consistency/03_e2e_width_sweep.md (100%) create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__00_phase.a1.err create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__00_phase.a1.md create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__00_phase.a1.validate create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a1.err create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a1.md create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a1.validate create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a2.err create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a2.md create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a2.validate create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__02_width_unit_pins.a1.err create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__02_width_unit_pins.a1.md create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__02_width_unit_pins.a1.validate create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__03_e2e_width_sweep.a1.err create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__03_e2e_width_sweep.a1.md create mode 100644 .agents/reports/100_page_width_consistency/100_page_width_consistency__03_e2e_width_sweep.a1.validate diff --git a/.agents/phases/todo/100_page_width_consistency/00_phase.md b/.agents/phases/complete/100_page_width_consistency/00_phase.md similarity index 100% rename from .agents/phases/todo/100_page_width_consistency/00_phase.md rename to .agents/phases/complete/100_page_width_consistency/00_phase.md diff --git a/.agents/phases/todo/100_page_width_consistency/01_width_css.md b/.agents/phases/complete/100_page_width_consistency/01_width_css.md similarity index 100% rename from .agents/phases/todo/100_page_width_consistency/01_width_css.md rename to .agents/phases/complete/100_page_width_consistency/01_width_css.md diff --git a/.agents/phases/todo/100_page_width_consistency/02_width_unit_pins.md b/.agents/phases/complete/100_page_width_consistency/02_width_unit_pins.md similarity index 100% rename from .agents/phases/todo/100_page_width_consistency/02_width_unit_pins.md rename to .agents/phases/complete/100_page_width_consistency/02_width_unit_pins.md diff --git a/.agents/phases/todo/100_page_width_consistency/03_e2e_width_sweep.md b/.agents/phases/complete/100_page_width_consistency/03_e2e_width_sweep.md similarity index 100% rename from .agents/phases/todo/100_page_width_consistency/03_e2e_width_sweep.md rename to .agents/phases/complete/100_page_width_consistency/03_e2e_width_sweep.md diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__00_phase.a1.err b/.agents/reports/100_page_width_consistency/100_page_width_consistency__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__00_phase.a1.md b/.agents/reports/100_page_width_consistency/100_page_width_consistency__00_phase.a1.md new file mode 100644 index 0000000..f610c20 --- /dev/null +++ b/.agents/reports/100_page_width_consistency/100_page_width_consistency__00_phase.a1.md @@ -0,0 +1,9 @@ +**Phase 100 — final verification pass: all green.** + +- Verified the shipped CSS contract directly: `--chat-column: 72rem` in `:root`; 0 literal `max-width: 46rem`; no `@media (min-width: 1500px)` block; exactly 4 token-capped reading columns; tuning/theme/doc-edit shells cap-free, structurally `.sources-shell`; `mock_llm.py` diff is comment-only. +- Defect found & fixed (phase-93 suite): `test_theme_semantic_completion.py::test_reset_removes_tag_byte_identical` raced theme.js's post-PUT refetch — it asserted the `#bor-theme` tag was gone right after the result line, but tag removal lands in the reconcile after the re-fetch (failed ~1 in 5 runs after `test_chat_rag`, reproduced). Fixed with an auto-waiting `expect(...).to_have_count(0)` (settled-state wait, the file's existing house pattern). 6/6 clean on the repro loop after. +- Tests/lint/coverage: `uv run pytest --cov=app --cov-report=term-missing` → 2052 passed, **99%** on `app/`; `uv run ruff check .` + `uv run pyright` → 0 errors. +- E2E in isolation (all passed): `test_wide_desktop_column.py` 3 (the phase suite — chat==tuning==theme==RAG ±4px at 1280 & 1920, ≈1152px; shared ≈1152px; standalone doc ≈1112px; modal unchanged ≈1100px; 360px overflow-free), `test_ui_customization` 4, `test_admin_theme_tab` 5, `test_document_viewer` 7, `test_save_share_ux` 5, `test_sticky_navbar` 3, `test_markdown_tables` 6, `test_responsive_polish` 7, `test_chat_rag` 3, `test_theme_semantic_completion` 8. +- Completion criteria: ① measured 72rem everywhere (≥~1200px, ±4px) + full-width below + 360px clean — **PASS** (E2E); ② zero 46rem rules / no 1500px block / four token selectors — **PASS** (grep + unit pins); ③ B4 byte-identical no-op + mobile squeeze — **PASS** (theme + responsive suites green); ④ full suite / coverage / lint — **PASS**; ⑤ atomic commit — left to the harness per executor protocol (all changes in the working tree, uncommitted). +- Deviations: none from phase decisions; only change in this pass is the race fix above (test-only, behavior unchanged). +- Next pending phase: `98_sync_summary_visibility` (numeric order in `todo/`; also pending: 99, 101–105). diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__00_phase.a1.validate b/.agents/reports/100_page_width_consistency/100_page_width_consistency__00_phase.a1.validate new file mode 100644 index 0000000..2a7fa7f --- /dev/null +++ b/.agents/reports/100_page_width_consistency/100_page_width_consistency__00_phase.a1.validate @@ -0,0 +1,96 @@ +........................................................................ [ 3%] +........................................................................ [ 7%] +........................................................................ [ 10%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 21%] +........................................................................ [ 24%] +........................................................................ [ 28%] +........................................................................ [ 31%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 49%] +........................................................................ [ 52%] +........................................................................ [ 56%] +........................................................................ [ 59%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 70%] +........................................................................ [ 73%] +........................................................................ [ 77%] +........................................................................ [ 80%] +........................................................................ [ 84%] +........................................................................ [ 87%] +........................................................................ [ 91%] +........................................................................ [ 94%] +........................................................................ [ 98%] +.................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 181 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 127 1 99% +app/api/git_sources.py 229 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 115 0 100% +app/api/tokens.py 28 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 140 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/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 66 0 100% +app/models.py 125 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 316 1 99% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/folder_summaries.py 121 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 190 3 98% +app/rag/llm.py 243 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 306 0 100% +-------------------------------------------------- +TOTAL 3824 15 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 diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a1.err b/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a1.md b/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a1.md new file mode 100644 index 0000000..26b96a9 --- /dev/null +++ b/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a1.md @@ -0,0 +1,18 @@ +Task 01 complete. All gates verified. + +**Implemented** (`frontend/assets/styles.css` only — D3, no HTML/JS/app change): +- `:root { --chat-column: 72rem }` — now equals the `.container` cap; provenance comment cites the owner instruction 2026-09-12 and supersedes the 2026-08-31 instruction +- Entire `@media (min-width: 1500px)` block deleted (92rem doubling + `.doc-shell` cap lift); `prefers-reduced-motion` block kept +- `.tuning-shell` / `.theme-shell` / `.doc-edit-shell`: `max-width: 46rem` + `margin-inline: auto` removed — now structurally match `.sources-shell` (flex column, no cap) +- `.remove-confirm-panel` / `.ignore-editor-panel`: `min(46rem, …)` → `min(var(--chat-column), …)` (the task's hard grep gate required removing the literals; the phase context lists both pin files for task 02's sweep) +- Comment sweep: all stale 46rem/92rem/1500px width claims rewritten to the 72rem contract (0 added lines carry old literals; 36 removed) + +**Verification:** +- Width grep clean: `grep -nE "(^|[^.0-9])46rem|(^|[^.0-9])92rem|1500px|…"` → nothing. The task's exact grep only matches 7 `font-size: 0.92rem;` lines — font sizes are non-width CSS, explicitly "do NOT touch" +- Playwright sanity (mock LLM + real app): chat/RAG/tuning/theme all measure **1152px (72rem), centered, at both 1920px and 1280px** — identical to the RAG reference; doc modal panel unchanged at **1100px** (`.doc-md` 1058px, panel binds per D2); standalone doc page 1152px shell / 1112px column; shared page 1152px; **360px: zero horizontal overflow** (chat/tuning/shared scrollWidth = 360). DB restored byte-exact after the check +- `uv run pytest` → 2043 passed, **10 failed — exactly the width pins task 02 rewrites** (`test_wide_column_css.py` ×7, `test_doc_edit_screen.py`, `test_remove_confirm_modal.py`, `test_source_ignore_paths.py`; the other named files carry fixture/docstring text only and stay green) +- `uv run pytest --cov=app` → **99% on `app/`** (>90% gate); `uv run ruff check .` clean; `uv run pyright` → 0 errors, 0 warnings + +**Notable decisions:** the two 46rem dialog panels ride the token (only literal-free reading that satisfies the grep gate); `0.92rem` font sizes left untouched per the do-not-touch rule. + +**Next pending task:** `02_width_unit_pins.md` diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a1.validate b/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a1.validate new file mode 100644 index 0000000..c3d99d5 --- /dev/null +++ b/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a1.validate @@ -0,0 +1,295 @@ +........................................................................ [ 3%] +........................................................................ [ 7%] +........................................................................ [ 10%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 21%] +.....F.................................................................. [ 24%] +........................................................................ [ 28%] +........................................................................ [ 31%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 49%] +........................................................F............... [ 52%] +........................................................................ [ 56%] +........................................................................ [ 59%] +........................................................................ [ 63%] +F....................................................................... [ 66%] +........................................................................ [ 70%] +........................................................................ [ 73%] +..................FFF.F.FFF............................................. [ 77%] +........................................................................ [ 80%] +........................................................................ [ 84%] +........................................................................ [ 87%] +........................................................................ [ 91%] +........................................................................ [ 94%] +........................................................................ [ 98%] +..................................... [100%] +=================================== FAILURES =================================== +______________ test_doc_edit_shell_is_the_hardcoded_46rem_column _______________ + + def test_doc_edit_shell_is_the_hardcoded_46rem_column() -> None: + """.doc-edit-shell: the 46rem base column — HARD-CODED 46rem (a + form column, not a reading column — it must NOT ride + --chat-column, so phase 58's wide-desktop doubling never stretches + the form), centered, a flex column on the container frame.""" + css = _css() + block = re.search(r"\.doc-edit-shell \{([\s\S]*?)\n\}", css) + assert block, "styles.css must style .doc-edit-shell" + body = block.group(1) +> assert "max-width: 46rem" in body, "the 46rem base column (hard-coded)" +E AssertionError: the 46rem base column (hard-coded) +E assert 'max-width: 46rem' in '\n width: 100%;\n display: flex;\n flex-direction: column;\n gap: 1.25rem;\n flex: 1;' + +tests/unit/test_doc_edit_screen.py:467: AssertionError +__________________ test_modal_css_targets_and_contrast_pairs ___________________ + + def test_modal_css_targets_and_contrast_pairs() -> None: + """The WCAG 2.1 AA basics in CSS: both buttons >=44px; the + destructive button rides the err token family (err-ink on err-bg + 9.3:1, the err-line border — the .tuning-delete / .steering-delete + convention; the hover inverts to --bg on --err-line, 5.2:1); + Cancel is the ghost ink-soft family (5.1:1 on --surface); the + error line is the err pair; the panel caps at the 46rem + chat-column width or the viewport.""" + css = _css() + btn = css[css.find(".remove-confirm-btn {"):] + btn = btn[: btn.find("\n}")] + assert "min-height: 44px" in btn and "min-width: 44px" in btn + remove = css[css.find(".remove-confirm-remove {"):] + remove = remove[: remove.find("\n}")] + for prop in ("var(--err-bg)", "var(--err-ink)", "var(--err-line)"): + assert prop in remove, f"the destructive pair must keep {prop}" + hover = css[css.find(".remove-confirm-remove:hover:not(:disabled) {"):] + hover = hover[: hover.find("\n}")] + assert "var(--err-line)" in hover and "var(--bg)" in hover, ( + "the hover inversion: dark --bg on --err-line (5.2:1)" + ) + cancel = css[css.find(".remove-confirm-cancel {"):] + cancel = cancel[: cancel.find("\n}")] + assert "var(--ink-soft)" in cancel and "transparent" in cancel + err = css[css.find(".remove-confirm-error {"):] + err = err[: err.find("\n}")] + assert "var(--err-ink)" in err and "var(--err-bg)" in err + panel = css[css.find(".remove-confirm-panel {"):] + panel = panel[: panel.find("\n}")] +> assert "min(46rem" in panel, "the 46rem chat-column cap (or the viewport)" +E AssertionError: the 46rem chat-column cap (or the viewport) +E assert 'min(46rem' in '.remove-confirm-panel {\n /* position:relative lifts the panel above the fixed backdrop\n (positioned elements p...d: var(--surface);\n border: 1px solid var(--line);\n border-radius: var(--radius);\n box-shadow: var(--shadow-lg);' + +tests/unit/test_remove_confirm_modal.py:476: AssertionError +_________________ test_dialog_button_and_target_contrast_pairs _________________ + + def test_dialog_button_and_target_contrast_pairs() -> None: + """The WCAG 2.1 AA basics in CSS: both dialog buttons >=44px; + Cancel is the ghost ink-soft family (5.1:1 on --surface) with the + brand-soft hover (12.4:1); Save is the solid brand family (--bg + text on --brand 5.2:1, the .new-chat-btn convention) with the + lightened hover; the error line is the err pair; the panel caps + at the 46rem chat-column width or the viewport; the visible + label is ink-soft (5.1:1) — never a label-less textarea.""" + css = _css() + btn = _css_rule(css, ".ignore-editor-btn") + assert "min-height: 44px" in btn and "min-width: 44px" in btn + cancel = _css_rule(css, ".ignore-editor-cancel") + assert "var(--ink-soft)" in cancel and "transparent" in cancel + cancel_hover = _css_rule(css, ".ignore-editor-cancel:hover:not(:disabled)") + assert "var(--brand-soft)" in cancel_hover and "var(--brand-ink)" in cancel_hover + save = _css_rule(css, ".ignore-editor-save") + assert "var(--brand)" in save and "var(--bg)" in save, ( + "Save: the solid brand family (--bg text on --brand, 5.2:1)" + ) + save_hover = _css_rule(css, ".ignore-editor-save:hover:not(:disabled)") + assert "background" in save_hover, "the hover lightens the fill" + err = _css_rule(css, ".ignore-editor-error") + assert "var(--err-ink)" in err and "var(--err-bg)" in err + panel = _css_rule(css, ".ignore-editor-panel") +> assert "min(46rem" in panel, "the 46rem chat-column cap (or the viewport)" +E AssertionError: the 46rem chat-column cap (or the viewport) +E assert 'min(46rem' in '.ignore-editor-panel {\n \n position: relative;\n z-index: 1;\n margin: auto;\n width: min(var(--chat-column), c...var(--surface);\n border: 1px solid var(--line);\n border-radius: var(--radius);\n box-shadow: var(--shadow-lg);\n}' + +tests/unit/test_source_ignore_paths.py:520: AssertionError +__________________ test_root_declares_chat_column_46rem_base ___________________ + + def test_root_declares_chat_column_46rem_base() -> None: + """:root declares --chat-column: 46rem (the PLAN §7 base) with the + owner-provenance comment (instruction 2026-08-31, TODO L5).""" + css = _css() + root = _rule_block(css, ":root") +> assert "--chat-column: 46rem" in root, ( + ":root must declare the --chat-column base (46rem)" + ) +E AssertionError: :root must declare the --chat-column base (46rem) +E assert '--chat-column: 46rem' in '{\n /* Palette — all text/background pairs meet WCAG 2.1 AA (>= 4.5:1) */\n --bg: #0f0a0a; /* page: ink ... — supersedes the 2026-08-31 instruction, and\n the wide-desktop doubling with it). */\n --chat-column: 72rem;\n}' + +tests/unit/test_wide_column_css.py:66: AssertionError +___________________ test_wide_media_block_doubles_the_token ____________________ + + def test_wide_media_block_doubles_the_token() -> None: + """A @media (min-width: 1500px) block sets --chat-column: 92rem on + :root — the single wide override (2x the base).""" + css = _css() + m = re.search(r"@media \(min-width: 1500px\) \{", css) +> assert m, "styles.css must carry the @media (min-width: 1500px) block" +E AssertionError: styles.css must carry the @media (min-width: 1500px) block +E assert None + +tests/unit/test_wide_column_css.py:82: AssertionError +____________ test_wide_block_lives_in_the_bottom_responsive_region _____________ + + def test_wide_block_lives_in_the_bottom_responsive_region() -> None: + """The min-width sibling sits alongside the max-width responsive + blocks at the bottom of the file (after the <=640px block).""" + css = _css() +> wide = css.index("@media (min-width: 1500px)") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E ValueError: substring not found + +tests/unit/test_wide_column_css.py:108: ValueError +_____________ test_shared_shell_keeps_the_centered_column_comment ______________ + + def test_shared_shell_keeps_the_centered_column_comment() -> None: + """.shared-shell's inline comment keeps the "centered chat column" + wording and notes the wide override (task 01 work item).""" + css = _css() + rule = css[css.index(".shared-shell {") : css.index(".shared-shell {") + 400] +> assert "the PLAN §7 centered chat column" in rule +E assert 'the PLAN §7 centered chat column' in '.shared-shell {\n width: 100%;\n max-width: var(--chat-column); /* the centered chat column — 72rem,\n the .con...\'s title; the static\n fallback is "Shared conversation") — the page-head h1 size. */\n#shared-title { margin: 0 0 ' + +tests/unit/test_wide_column_css.py:143: AssertionError +___________________ test_tuning_shell_stays_hardcoded_46rem ____________________ + + def test_tuning_shell_stays_hardcoded_46rem() -> None: + """.tuning-shell (the form column, out of scope) keeps its + hard-coded max-width: 46rem at every width — it never widens.""" + css = _css() + tuning = _rule_block(css, ".tuning-shell") +> assert "max-width: 46rem" in tuning, ( + ".tuning-shell must stay hard-coded 46rem (negative pin)" + ) +E AssertionError: .tuning-shell must stay hard-coded 46rem (negative pin) +E assert 'max-width: 46rem' in '{\n display: flex;\n flex-direction: column;\n gap: 1.25rem;\n flex: 1;\n}' + +tests/unit/test_wide_column_css.py:161: AssertionError +__________________ test_no_other_hardcoded_46rem_rule_remains __________________ + + def test_no_other_hardcoded_46rem_rule_remains() -> None: + """After the switch, the form columns are the ONLY rules with a + literal max-width: 46rem: .tuning-shell (phase 27), + .doc-edit-shell (phase 59, task 06 — the doc edit screen is a + FORM column, not a reading column, so it must not ride + --chat-column and phase 58's wide-desktop doubling must never + stretch the form), and .theme-shell (phase 91 task 04 — the + admin Theme editor is a form column too: the palette grid + + fieldsets must never ride the wide-desktop doubling). Every + reading column rides the token (the --chat-column base + declaration is the other non-rule occurrence of 46rem).""" + css = _css() +> assert css.count("max-width: 46rem") == 3, ( + "only the form columns (.tuning-shell, .doc-edit-shell, " + ".theme-shell) may keep a literal max-width: 46rem" + ) +E AssertionError: only the form columns (.tuning-shell, .doc-edit-shell, .theme-shell) may keep a literal max-width: 46rem +E assert 0 == 3 +E + where 0 = ('max-width: 46rem') +E + where = '/* ==========================================================================\n Brain of Reese — design system (no ...ill animate. */\n@media (prefers-reduced-motion: reduce) {\n .app-nav,\n .app-nav.is-open { transition: none; }\n}\n'.count + +tests/unit/test_wide_column_css.py:181: AssertionError +_____________ test_comments_cite_the_wide_override_with_provenance _____________ + + def test_comments_cite_the_wide_override_with_provenance() -> None: + """The block comments that claimed the "46rem column contract" now + name base 46rem + the 2x wide override, with the owner + instruction (2026-08-31, TODO L5) as the provenance at the token + and the media block.""" + css = _css() + # The stale "≤46rem" contract claims are gone from the file. + assert "≤46rem" not in css, ( + "the stale '≤46rem' contract wording must be updated" + ) + # Provenance at the two authoritative spots (token + wide block). +> token_idx = css.index("--chat-column: 46rem") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E ValueError: substring not found + +tests/unit/test_wide_column_css.py:201: ValueError +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 181 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 127 1 99% +app/api/git_sources.py 229 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 115 0 100% +app/api/tokens.py 28 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 140 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/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 66 0 100% +app/models.py 125 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 316 1 99% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/folder_summaries.py 121 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 190 3 98% +app/rag/llm.py 243 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 306 0 100% +-------------------------------------------------- +TOTAL 3824 15 99% +=========================== short test summary info ============================ +FAILED tests/unit/test_doc_edit_screen.py::test_doc_edit_shell_is_the_hardcoded_46rem_column +FAILED tests/unit/test_remove_confirm_modal.py::test_modal_css_targets_and_contrast_pairs +FAILED tests/unit/test_source_ignore_paths.py::test_dialog_button_and_target_contrast_pairs +FAILED tests/unit/test_wide_column_css.py::test_root_declares_chat_column_46rem_base +FAILED tests/unit/test_wide_column_css.py::test_wide_media_block_doubles_the_token +FAILED tests/unit/test_wide_column_css.py::test_wide_block_lives_in_the_bottom_responsive_region +FAILED tests/unit/test_wide_column_css.py::test_shared_shell_keeps_the_centered_column_comment +FAILED tests/unit/test_wide_column_css.py::test_tuning_shell_stays_hardcoded_46rem +FAILED tests/unit/test_wide_column_css.py::test_no_other_hardcoded_46rem_rule_remains +FAILED tests/unit/test_wide_column_css.py::test_comments_cite_the_wide_override_with_provenance +tests FAILED: unit + integration suite did not pass +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 FAILED (see output above) diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a2.err b/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a2.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a2.md b/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a2.md new file mode 100644 index 0000000..3fc4847 --- /dev/null +++ b/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a2.md @@ -0,0 +1,13 @@ +Everything is green. Final report: + +**Fixed:** the harness gate requires a fully green suite, so the old-contract width pins (which asserted the retired 46rem/92rem contract) were updated to the new 72rem contract — `test_wide_column_css.py` rewritten (token = 72rem = `.container` cap with 2026-09-12 provenance; no `min-width:1500px` block; flipped negative pin: zero literal `max-width: 46rem`; the three form shells structurally match `.sources-shell`; both dialogs pinned at `min(var(--chat-column), …)`; no stale width comments), plus the single pin + docstrings in `test_doc_edit_shell` (no cap, rides the container), `test_remove_confirm_modal` and `test_source_ignore_paths` (`min(var(--chat-column)`). `styles.css` from the prior attempt is unchanged (already verified: 1152px columns on every page at 1280/1920, modal panel unchanged 1100px, no 360px overflow). + +**Results (exact commands):** +- `uv run pytest` → **2052 passed, 0 failed** +- `uv run pytest --cov=app --cov-report=term` → **app/ 99%** (>90% gate) +- `uv run ruff check .` → **All checks passed!** +- `uv run pyright` → **0 errors, 0 warnings** + +**Notable decisions:** the harness rejection overrode the task file's "expected breakage" clause — the pin updates (task 02's unit-pin scope) landed here to satisfy the green gate; task 02's remaining work is only the docstring sweep of `test_markdown_tables` / `test_shared_page` / `test_pinned_composer` (fixture text, per pin judgment). No HTML/JS/app change (D3); DB left as found. + +**Next pending task:** `02_width_unit_pins.md` (unit-pin scope now largely satisfied — residual docstring sweep remains) diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a2.validate b/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a2.validate new file mode 100644 index 0000000..2a7fa7f --- /dev/null +++ b/.agents/reports/100_page_width_consistency/100_page_width_consistency__01_width_css.a2.validate @@ -0,0 +1,96 @@ +........................................................................ [ 3%] +........................................................................ [ 7%] +........................................................................ [ 10%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 21%] +........................................................................ [ 24%] +........................................................................ [ 28%] +........................................................................ [ 31%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 49%] +........................................................................ [ 52%] +........................................................................ [ 56%] +........................................................................ [ 59%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 70%] +........................................................................ [ 73%] +........................................................................ [ 77%] +........................................................................ [ 80%] +........................................................................ [ 84%] +........................................................................ [ 87%] +........................................................................ [ 91%] +........................................................................ [ 94%] +........................................................................ [ 98%] +.................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 181 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 127 1 99% +app/api/git_sources.py 229 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 115 0 100% +app/api/tokens.py 28 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 140 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/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 66 0 100% +app/models.py 125 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 316 1 99% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/folder_summaries.py 121 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 190 3 98% +app/rag/llm.py 243 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 306 0 100% +-------------------------------------------------- +TOTAL 3824 15 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 diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__02_width_unit_pins.a1.err b/.agents/reports/100_page_width_consistency/100_page_width_consistency__02_width_unit_pins.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__02_width_unit_pins.a1.md b/.agents/reports/100_page_width_consistency/100_page_width_consistency__02_width_unit_pins.a1.md new file mode 100644 index 0000000..844a574 --- /dev/null +++ b/.agents/reports/100_page_width_consistency/100_page_width_consistency__02_width_unit_pins.a1.md @@ -0,0 +1,14 @@ +**Task 02 — unit pins for the 72rem width contract — complete.** + +**Implemented** +- `test_wide_column_css.py` rewritten to the phase-100 contract (kept the balanced-brace `_rule_block` helpers): 72rem token + 2026-09-12 provenance + token-equals-`.container` pin; no `@media (min-width: 1500px)` block and no `92rem`; the four reading-column selectors use the token (`count == 4`); flipped negative pin — zero literal `max-width: 46rem`; `.tuning-shell`/`.theme-shell`/`.doc-edit-shell` cap-free, token-free, `margin-inline`-free with the `.sources-shell` shape; dialog panels ride `min(var(--chat-column), calc(100vw - 2rem))`; stale comment claims gone, 72rem named at chat/shared shells. +- Grep sweep (`46rem|92rem|chat-column` under `tests/unit/`) judged per pin: `test_doc_edit_screen.py` (shell cap-free pin), `test_remove_confirm_modal.py` + `test_source_ignore_paths.py` (dialog `min(var(--chat-column)` pins), `test_markdown_tables.py`, `test_shared_page.py` (docstrings + token pin kept; its "no 46rem" negative pin still valid), `test_pinned_composer.py` (docstring) all updated to the new contract; no fixture text touched. Every remaining reference is a new-contract pin or description. + +**Results** +- `uv run pytest tests/unit/ -q` → green (exit 0; 8/8 in the rewritten file) +- `uv run pytest --cov=app --cov-report=term-missing` → 2052 passed, 1 warning; `app/` coverage **99%** (>90% gate) +- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings + +**Decisions** — dialog panels (remove-confirm, ignore-editor) were moved onto the token by task 01, so their unit pins now assert the token form (behavior change is task 01's, already validated); no `app/`/HTML/JS touched. No defects found in prior work. + +**Next pending task:** `.agents/phases/todo/100_page_width_consistency/03_e2e_width_sweep.md` diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__02_width_unit_pins.a1.validate b/.agents/reports/100_page_width_consistency/100_page_width_consistency__02_width_unit_pins.a1.validate new file mode 100644 index 0000000..2a7fa7f --- /dev/null +++ b/.agents/reports/100_page_width_consistency/100_page_width_consistency__02_width_unit_pins.a1.validate @@ -0,0 +1,96 @@ +........................................................................ [ 3%] +........................................................................ [ 7%] +........................................................................ [ 10%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 21%] +........................................................................ [ 24%] +........................................................................ [ 28%] +........................................................................ [ 31%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 49%] +........................................................................ [ 52%] +........................................................................ [ 56%] +........................................................................ [ 59%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 70%] +........................................................................ [ 73%] +........................................................................ [ 77%] +........................................................................ [ 80%] +........................................................................ [ 84%] +........................................................................ [ 87%] +........................................................................ [ 91%] +........................................................................ [ 94%] +........................................................................ [ 98%] +.................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 181 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 127 1 99% +app/api/git_sources.py 229 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 115 0 100% +app/api/tokens.py 28 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 140 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/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 66 0 100% +app/models.py 125 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 316 1 99% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/folder_summaries.py 121 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 190 3 98% +app/rag/llm.py 243 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 306 0 100% +-------------------------------------------------- +TOTAL 3824 15 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 diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__03_e2e_width_sweep.a1.err b/.agents/reports/100_page_width_consistency/100_page_width_consistency__03_e2e_width_sweep.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__03_e2e_width_sweep.a1.md b/.agents/reports/100_page_width_consistency/100_page_width_consistency__03_e2e_width_sweep.a1.md new file mode 100644 index 0000000..b564421 --- /dev/null +++ b/.agents/reports/100_page_width_consistency/100_page_width_consistency__03_e2e_width_sweep.a1.md @@ -0,0 +1,18 @@ +**Phase 100, task 03 complete — all green.** + +**Implemented** +- Rewrote `tests/e2e/test_wide_desktop_column.py` to the measured phase-100 contract: chat == tuning == theme == RAG shells (±4px) at 1280×800 AND 1920×1080 (each ≈1152px, the 72rem container); modal unchanged (panel ~1100px, inner `.doc-md` ~1058px); shared `.shared-shell` ≈1152px; standalone `document.html` `.doc-md` ≈1112px; 360px overflow-free, 900px full-width — seeding helpers kept. Verified pins are discriminating (temp 46rem token → all 3 fail; CSS restored). +- Updated in place: `test_document_viewer.py` (standalone `.doc-md` ≤736px pin → ≈1112px), `test_responsive_polish.py` (92rem/46rem pins + constants → 72rem container); comment-only fixes in `test_save_share_ux.py`, `test_sticky_navbar.py`, `test_markdown_tables.py`, `mock_llm.py`. + +**Test / lint / coverage (exact commands)** +- `uv run pytest tests/e2e/test_wide_desktop_column.py -v --no-cov` → 3 passed (in isolation) +- Updated suites in isolation: document_viewer 7, save_share_ux 5, sticky_navbar 3, markdown_tables 6, responsive_polish 7 — all passed +- Theme no-op UNCHANGED: `test_ui_customization.py` 4, `test_admin_theme_tab.py` 5 — all passed +- Regression: `test_chat_rag.py` 3, `test_theme_semantic_completion.py` 8 — all passed +- `uv run pytest --cov=app --cov-report=term-missing` → 2052 passed, **99%** on `app/` (>90%) +- `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings + +**Decisions** +- Per harness protocol I did NOT move the phase dir or commit — all changes left in the working tree for the harness's single atomic phase commit (task-file step 5's commit/move handled by harness). + +**Next pending task:** phase 100 is fully done; next in numeric order is `101_tokens_page_overhaul` (task 01). diff --git a/.agents/reports/100_page_width_consistency/100_page_width_consistency__03_e2e_width_sweep.a1.validate b/.agents/reports/100_page_width_consistency/100_page_width_consistency__03_e2e_width_sweep.a1.validate new file mode 100644 index 0000000..2a7fa7f --- /dev/null +++ b/.agents/reports/100_page_width_consistency/100_page_width_consistency__03_e2e_width_sweep.a1.validate @@ -0,0 +1,96 @@ +........................................................................ [ 3%] +........................................................................ [ 7%] +........................................................................ [ 10%] +........................................................................ [ 14%] +........................................................................ [ 17%] +........................................................................ [ 21%] +........................................................................ [ 24%] +........................................................................ [ 28%] +........................................................................ [ 31%] +........................................................................ [ 35%] +........................................................................ [ 38%] +........................................................................ [ 42%] +........................................................................ [ 45%] +........................................................................ [ 49%] +........................................................................ [ 52%] +........................................................................ [ 56%] +........................................................................ [ 59%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 70%] +........................................................................ [ 73%] +........................................................................ [ 77%] +........................................................................ [ 80%] +........................................................................ [ 84%] +........................................................................ [ 87%] +........................................................................ [ 91%] +........................................................................ [ 94%] +........................................................................ [ 98%] +.................................... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 181 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 127 1 99% +app/api/git_sources.py 229 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 115 0 100% +app/api/tokens.py 28 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 140 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/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 66 0 100% +app/models.py 125 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 316 1 99% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/folder_summaries.py 121 0 100% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 190 3 98% +app/rag/llm.py 243 1 99% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 306 0 100% +-------------------------------------------------- +TOTAL 3824 15 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 diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index fee6359..9a365cb 100644 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -45,10 +45,13 @@ --header-h: 64px; - /* Reading column: 46rem base (PLAN §7 lineage); 2x on wide desktops - (owner instruction 2026-08-31, TODO L5 / D2 — chat + shared + - document view). */ - --chat-column: 46rem; + /* Reading column: 72rem — EQUAL to the .container's 72rem cap, so + the reading columns (chat, shared, the document viewer's .doc-md + + its summary panel) ride the same width as every other page + (owner instruction 2026-09-12: "match the width of the RAG page + for all other pages" — supersedes the 2026-08-31 instruction, and + the wide-desktop doubling with it). */ + --chat-column: 72rem; } * { box-sizing: border-box; } @@ -389,8 +392,11 @@ body::before { /* Chat is a vertical conversation: a centered, capped column is the correct layout here (PLAN §UI/UX). The surrounding frame keeps it from collapsing into a hairline on wide screens. The cap is the - --chat-column token: 46rem base, 2x (92rem) at >=1500px wide - desktops (owner instruction 2026-08-31, TODO.md L5 / D2). */ + --chat-column token: 72rem — equal to the .container's cap, so the + chat column reads at the RAG page's full container width at every + viewport (owner instruction 2026-09-12: "match the width of the + RAG page for all other pages" — the wide-desktop doubling was + retired with it). */ .chat-shell { max-width: var(--chat-column); margin-inline: auto; @@ -409,8 +415,8 @@ body::before { flips this to a vertical stack (flex-direction: column + align-items: stretch — full-width pills, New chat above Share; the existing ≤640px pill rules apply to the stacked pills unchanged). - The reading-column contract is untouched (--chat-column: 46rem base, - 92rem at >=1500px — PLAN §7 lineage). + The reading-column contract is untouched (--chat-column: 72rem — + the .container width at every viewport). Phase 65 (task 03, 2026-09-01, `TODO.md` L3, owner-locked A2): the row is the TOP member of the pinned .chat-bottom cluster (task @@ -504,8 +510,9 @@ body::before { /* GFM pipe tables (phase 44, 2026-08-27, TODO.md L6): the shared renderer wraps every table in .md-table-wrap — the horizontal scroller, so a wide table scrolls inside the bubble instead of - breaking the reading column (--chat-column: 46rem base, 92rem at - >=1500px) — around a semantic + breaking the reading column (--chat-column: 72rem — the + .container width at every viewport) — around a semantic +
(escape-first cells; alignment colons render left, owner decision). Phase-08 tokens only: --line hairline borders and the thead tinted from the plain surface family — --ink on --surface is 14.5:1 (PLAN @@ -603,8 +610,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } --surface ≈10.4:1 (11.6:1 on the page bg), and --ink on --brand-soft in the path `code` ≈11.5:1 — all comfortably AA in the (single dark) theme. Inline rows only: appending lines never shifts the chat - column (46rem base; 92rem at >=1500px — no new container), and the - rows are not interactive — no focus targets. */ + column (72rem — the .container width at every viewport; no new + container), and the rows are not interactive — no focus targets. */ .tool-calls { display: flex; flex-direction: column; @@ -960,15 +967,16 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } /* ---------- Global tuning page (phase 27) ---------- */ /* /tuning.html: create / edit / delete steering notes without a chat - conversation. Same width discipline as the chat column — a centered, - capped column on the 72rem frame; the form and the note list span its - FULL width (no skinny lists). Every interactive target is >=44px; - text pairs reuse the Phase-08 AA palette (dark ink on brand 5.2:1, - brand-ink/brand-soft 6.9:1, ok 10.6:1, err 9.1:1, ink-soft >=6.9:1). - No filter: blur, no CDN, system font stack. */ + conversation. Full-container width (owner instruction 2026-09-12: + "match the width of the RAG page for all other pages" — the + phase-27 capped-column discipline is superseded): the shell rides + the .container's 72rem frame like the RAG page, and the form and + the note list span its FULL width (no skinny lists). Every + interactive target is >=44px; text pairs reuse the Phase-08 AA + palette (dark ink on brand 5.2:1, brand-ink/brand-soft 6.9:1, + ok 10.6:1, err 9.1:1, ink-soft >=6.9:1). No filter: blur, no CDN, + system font stack. */ .tuning-shell { - max-width: 46rem; - margin-inline: auto; display: flex; flex-direction: column; gap: 1.25rem; @@ -1241,8 +1249,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } /* ---------- Composer ---------- */ /* Phase 52 (2026-08-30, TODO.md L3): the composer is PINNED to the viewport bottom. The page scrolls at the document level and - `.chat-shell` (the centered reading column — 46rem base, 92rem at - >=1500px) is the composer's sticky + `.chat-shell` (the centered reading column — 72rem, the + .container width at every viewport) is the composer's sticky containing block, so the box sticks to the bottom edge of the viewport at every scroll position and settles back into its normal flow position (above the footer) once the document bottom is @@ -2265,8 +2273,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } a fixed full-viewport dim backdrop + a centered panel (z-index 1000, above the sticky header (20) + skip-link (100); NO blur — the phase-08 no-blur perf anchor), scaled to a compact dialog: - the 46rem chat-column width or the viewport, whichever is - narrower. Phase-08 tokens only; system fonts; no CDN. + the chat-column width (the --chat-column token — 72rem, the + .container width) or the viewport, whichever is narrower. + Phase-08 tokens only; system fonts; no CDN. AA pairs: title/copy are --ink on --surface (13.8:1); the source value is --ink on --bg (16.7:1); the error line is the err pair @@ -2302,7 +2311,7 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } position: relative; z-index: 1; margin: auto; - width: min(46rem, calc(100vw - 2rem)); + width: min(var(--chat-column), calc(100vw - 2rem)); padding: 1.5rem; background: var(--surface); border: 1px solid var(--line); @@ -2405,7 +2414,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } #remove-confirm-dialog overlay contract (phase 69): a fixed full-viewport dim backdrop + a centered panel (z-index 1000, above the sticky header (20) + skip-link (100); NO blur — the phase-08 - no-blur perf anchor), scaled to the 46rem chat-column width or the + no-blur perf anchor), scaled to the chat-column width (the + --chat-column token — 72rem, the .container width) or the viewport, whichever is narrower. The box: a VISIBLE block label (WCAG — never aria-label-only) over a mono textarea (the box is a data entry, not prose — the var(--mono) stack); the error line is @@ -2442,7 +2452,7 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } position: relative; z-index: 1; margin: auto; - width: min(46rem, calc(100vw - 2rem)); + width: min(var(--chat-column), calc(100vw - 2rem)); padding: 1.5rem; background: var(--surface); border: 1px solid var(--line); @@ -3168,8 +3178,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } /* ---------- Theme view (phase 91, tasks 04 + 05) ---------- The shell's seventh view (#view-theme): the admin palette + branding - editor (task 05). The centered 46rem column (the .tuning-shell - language — this is a form view, the tuning-view width pattern), the + editor (task 05). The full 72rem container width (owner + instruction 2026-09-12 — the phase-91 form-column cap is + superseded), the form card (the #tune-form language: surface fill, --line hairline, radius, shadow), the fieldset groups (Branding / Palette) with the .theme-note sub-copy, the 3-column (desktop) / 1-column (mobile) @@ -3184,8 +3195,6 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } tab (B3). Every pair reuses the Phase-08 AA palette; :focus-visible via the global 3px outline rule. No CDN, system fonts. */ .theme-shell { - max-width: 46rem; - margin-inline: auto; display: flex; flex-direction: column; gap: 1.25rem; @@ -3347,12 +3356,13 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } /* ---------- Shared page (phase 51, task 03) ---------- /shared/: the anonymous read-only conversation (owner-locked - 2026-08-29, TODO.md L6). The shell maps to the PLAN §7 centered - chat column (--chat-column: 46rem base, 92rem at >=1500px) — the - conversation reads exactly like the chat - page (the .msg/.bubble/.thinking/.tool-calls/.msg-meta rules apply - unchanged) with NO composer, so the column contract holds for a - guest. Zero interactive controls (owner-locked): the chips are + 2026-08-29, TODO.md L6). The shared page reads exactly like the + chat page — both at the 72rem container width (--chat-column, now + equal to the .container cap — owner instruction 2026-09-12; the + wide-desktop doubling was retired) — and the .msg/.bubble/.thinking/ + .tool-calls/.msg-meta rules apply unchanged, with NO composer, so + the column contract holds for a guest. Zero interactive controls + (owner-locked): the chips are plain text, so the pill families' pointer treatments are switched off IN THIS SCOPE ONLY — the chat page's interactive chips keep their styles untouched. Every pair reuses the Phase-08 AA palette @@ -3361,8 +3371,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } fonts. */ .shared-shell { width: 100%; - max-width: var(--chat-column); /* the PLAN §7 centered chat column - (46rem base; 92rem at >=1500px — phase 58) */ + max-width: var(--chat-column); /* the centered chat column — 72rem, + the .container width at every viewport */ margin-inline: auto; display: flex; flex-direction: column; @@ -3525,9 +3535,10 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } #doc-content { display: flex; flex-direction: column; } .doc-loading { margin: 1.5rem auto; text-align: center; color: var(--ink-soft); } -/* Markdown: the centered reading column — 46rem base, 2x (92rem) at - >=1500px (PLAN §7.1 lineage; owner instruction 2026-08-31, TODO.md - L5 / D2). */ +/* Markdown: the centered reading column — 72rem, the .container width + at every viewport (owner instruction 2026-09-12: "match the width + of the RAG page for all other pages" — the wide-desktop doubling + was retired). */ .doc-md { width: 100%; max-width: var(--chat-column); @@ -3576,8 +3587,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } overflow-wrap: anywhere; } /* md/markdown: the panel matches the .doc-md centered reading column - (46rem base; 92rem at >=1500px — the same --chat-column token) — - it is the column's label. Raw formats stay full width (the + (72rem — the same --chat-column token, the .container width at + every viewport) — it is the column's label. Raw formats stay full width (the .doc-raw default above), matching the full-width pre; in engines without :has() the panel degrades to that full-width default. */ .doc-summary:has(+ .doc-md) { @@ -3743,8 +3754,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } /* ---------- Document modal (phase 26) ---------- "New documents should open in an almost-fullscreen modal, not in a new page" (TODO.md L4). The overlay reuses the viewer page's .doc-meta - badge classes, the .doc-md reading column (46rem base; 92rem at - >=1500px), and the .doc-raw + badge classes, the .doc-md reading column (72rem — the .container + width; inside the modal the 1100px panel stays its effective + ceiling), and the .doc-raw pre — this block only adds the chrome (backdrop, panel, header, actions, scroll container). Phase-08 tokens only; NO blur (the phase-08 no-blur perf anchor); no new assets; system fonts. @@ -3886,9 +3898,10 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } } /* The scroll container: vertical scroll lives HERE, never the viewport. - .doc-md keeps its centered reading column inside (46rem base; - 92rem at >=1500px, capped there by the 1100px panel); .doc-raw keeps - its own overflow-x. tabindex="-1" in the markup is the JS focus target. */ + .doc-md keeps its centered reading column inside (72rem — wider + than the 1100px panel's inner width, which stays its effective + ceiling); .doc-raw keeps its own overflow-x. tabindex="-1" in the + markup is the JS focus target. */ .doc-modal-content { flex: 1; min-height: 0; @@ -3906,16 +3919,14 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } /doc-edit.html: the admin-gated edit screen for a doc draft (title, in-repo path, markdown body) — a FLOW page, not one of the app's pages, so the header is SLIM (brand + "← Back to chat" only). The - 46rem base column is HARD-CODED: a form column, not a reading - column — it does not ride --chat-column, so phase 58's wide-desktop - doubling never stretches the form. The .sources-gate gate is reused - verbatim (phases 16/35/50). Every pair reuses the Phase-08 AA - palette; touch targets >=44px; :focus-visible via the global 3px - outline rule. No CDN, system fonts. */ + shell rides the full 72rem container like every other page (owner + instruction 2026-09-12 — the phase-59 form-column cap is + superseded). The .sources-gate gate is reused verbatim + (phases 16/35/50). Every pair reuses the Phase-08 AA palette; + touch targets >=44px; :focus-visible via the global 3px outline + rule. No CDN, system fonts. */ .doc-edit-shell { width: 100%; - max-width: 46rem; /* the 46rem base column (hard-coded — see above) */ - margin-inline: auto; display: flex; flex-direction: column; gap: 1.25rem; @@ -4385,9 +4396,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } .theme-reset { width: 100%; } /* Phase 51: the shared page squeezes like the chat column — the title and the note step down (the empty-state-title family); the - shell keeps its base 46rem column (the >=1500px 92rem override - never applies here — it is already the narrowest box on the - page) and .msg-body's 92% override above applies. */ + shell is full-width here (the 72rem cap never binds on a phone — + the container is already 100%) and .msg-body's 92% override + above applies. */ #shared-title { font-size: 1.35rem; } .shared-note { font-size: 0.88rem; } .footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; } @@ -4418,24 +4429,6 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } } } -/* Phase 58: wide desktops (viewport >=1500px) read the 2x column — - --chat-column doubles to 92rem for the four reading shells (chat, - shared, the document viewer's .doc-md and its summary panel). The - chat and shared shells ARE their .container (the token max-width - overrides the .container's 72rem cap — same specificity, later in - the file), but the document page's .container.doc-shell WRAPS the - column, so the 72rem cap would bind first and pin .doc-md at - ~1112px: the wide block lifts the shell's cap to the column plus - the container's two 1.25rem gutters, letting .doc-md's own 92rem - cap bind (1472px at the 16px root). Everything below 1500px - renders exactly as before, and .tuning-shell (a form, not a - reading surface) keeps its hard-coded 46rem at every width - (owner instruction 2026-08-31, TODO.md L5 / D2). */ -@media (min-width: 1500px) { - :root { --chat-column: 92rem; } - .doc-shell { max-width: calc(var(--chat-column) + 2 * 1.25rem); } -} - /* Phase 46: prefers-reduced-motion stills the mobile menu — no 180ms slide+fade; open/close snaps (the visibility/opacity flip applies instantly) and stays correct. BOTH states are named: the diff --git a/tests/e2e/mock_llm.py b/tests/e2e/mock_llm.py index eacdbe2..9d5b681 100644 --- a/tests/e2e/mock_llm.py +++ b/tests/e2e/mock_llm.py @@ -327,7 +327,8 @@ Implements just enough of the aipi surface: a deliberately wide 5-column table — byte-stable, so the story E2E can assert the rendered ``
`` shape, the escaped XSS line, and the wrapper's horizontal scroll inside the - 46rem column. Checked BEFORE the ``DEFLECT_MODE`` branch (a + 72rem container column (phase 100). Checked BEFORE the + ``DEFLECT_MODE`` branch (a deflection prompt never carries the marker, same reasoning as ``SUMMARY_MODE``), so a marker question always gets the table answer; the E2E asks it against an on-topic fixture (HIGH gate) and @@ -584,7 +585,8 @@ TABLE_TRIGGER = "show me a table" #: The fixed table answer (phase 44) — byte-stable on purpose: the story #: E2E asserts the rendered table shape, the escaped ```` #: line (the XSS payload must survive the mock byte-for-byte), and the -#: wide table's ``scrollWidth > clientWidth`` inside the 46rem column. +#: wide table's ``scrollWidth > clientWidth`` inside the 72rem container +#: column (phase 100). #: Phase 74 (chat history, TODO L4): a user message containing this #: substring (case-insensitive) gets the deterministic HISTORY ECHO #: (``_history_echo`` below — see the module docstring): the prior-turn @@ -1653,7 +1655,8 @@ def compose_answer(body: dict[str, Any]) -> str: # XSS probe line (it must survive the mock # byte-for-byte so the E2E can prove the renderer neutralizes # it), and a wide 5-column table (guarantees scrollWidth > - # clientWidth inside the 46rem column). Byte-stable. Checked + # clientWidth inside the 72rem container column, phase 100). + # Byte-stable. Checked # BEFORE the DEFLECT_MODE branch: a deflection prompt never # carries the marker (it lives in the user message, same # reasoning as SUMMARY_MODE), so a marker question always gets diff --git a/tests/e2e/test_document_viewer.py b/tests/e2e/test_document_viewer.py index 0979cbe..348b890 100644 --- a/tests/e2e/test_document_viewer.py +++ b/tests/e2e/test_document_viewer.py @@ -24,7 +24,9 @@ Test → story mapping (Playwright Mapping Rule): renders as escaped text; no dialog fires. 6. ``test_standalone_page_still_works`` — the dedicated ``/document.html`` page keeps its phase-10 contract (title/content/badges, not-found, - dark theme, no-CDN, a11y frame, ≤736px md column). + dark theme, no-CDN, a11y frame, the 72rem-container md column — + phase 100: ≈1112px at the 1280px fixture viewport, the container's + inner content). 7. ``test_modal_theme_and_no_cdn`` — dark page background, the panel on the Phase-08 surface colour, every asset same-origin or ``data:``. """ @@ -406,9 +408,16 @@ def test_standalone_page_still_works( expect(page.locator(".doc-shell")).to_have_attribute("aria-live", "polite") assert page.evaluate("() => document.activeElement && document.activeElement.id") == "main" - # Markdown column centered and capped at 46rem (736px at 16px root). + # Markdown column: the 72rem container's inner content (phase 100 — + # the 46rem cap and its wide-desktop doubling are retired): at the + # 1280px fixture viewport the container is 1152px border-box, so + # .doc-md (width:100% inside it) measures 1152 − 2×1.25rem = 1112px. box = page.locator("#doc-content .doc-md").bounding_box() - assert box is not None and box["width"] <= 736 + 1 + assert box is not None, "the standalone .doc-md column is not rendered" + assert abs(box["width"] - 1112) <= 4, ( + f"the standalone .doc-md column is {box['width']:.0f}px, " + f"want 1112px (the 72rem container's inner content) ±4px" + ) assert errors == [], f"console crashes: {errors}" diff --git a/tests/e2e/test_markdown_tables.py b/tests/e2e/test_markdown_tables.py index bb1da62..389c531 100644 --- a/tests/e2e/test_markdown_tables.py +++ b/tests/e2e/test_markdown_tables.py @@ -22,7 +22,8 @@ Test → story mapping (Playwright Mapping Rule): NOT deflected (the honesty-gate interplay is part of the contract). 2. ``test_wide_table_scrolls`` — the wide table's wrapper has ``scrollWidth > clientWidth`` and horizontal scroll moves it; the - page itself has no horizontal overflow (the 46rem column holds). + page itself has no horizontal overflow (the 72rem container column + holds — phase 100). 3. ``test_table_xss_safe`` — the ```` line renders as visible, escaped text: zero injected ```` nodes, no dialog. 4. ``test_viewer_table_renders`` — the fixture's pipe table opens from @@ -219,8 +220,8 @@ def test_wide_table_scrolls( after = wrap.evaluate("el => el.scrollLeft") assert after > before, "the wrapper must scroll horizontally" - # The 46rem chat column must not break the page: no horizontal - # document overflow (PLAN §7.1). + # The 72rem container column must not break the page: no horizontal + # document overflow (PLAN §7.1, as revised by phase 100). page_scroll, page_client = page.evaluate( "() => [document.documentElement.scrollWidth, document.documentElement.clientWidth]" ) diff --git a/tests/e2e/test_responsive_polish.py b/tests/e2e/test_responsive_polish.py index 0114669..5defd76 100644 --- a/tests/e2e/test_responsive_polish.py +++ b/tests/e2e/test_responsive_polish.py @@ -12,12 +12,13 @@ Test → story mapping: 1. ``test_no_horizontal_overflow_at_viewports`` — 360/375/768/1280/1600 on both pages: ``documentElement.scrollWidth <= clientWidth``. 2. ``test_chat_column_capped_and_centered`` — the reading column rides - --chat-column (46rem base; 92rem at >=1500px, phase 58 / owner - instruction 2026-08-31 TODO L5): at 1600px (a wide desktop) the - ``.chat-shell`` is 92rem (1472px, ±2%) and horizontally centered - (±2%); at 1280px (below the wide breakpoint) it stays ≤ 46rem - (736px, +2%); at 768px the column uses most of the width (no - mid-column dead zones). + --chat-column (72rem — EQUAL to the .container's cap at every + viewport, phase 100 / owner instruction 2026-09-12: the 46rem base + + the >=1500px 92rem doubling are retired): at 1600px (a wide + desktop) the ``.chat-shell`` is the 72rem container (1152px, ±2%) + and horizontally centered (±2%); at 1280px the same 1152px (the + container cap binds, not the viewport); at 768px the column is + full-width (no mid-column dead zones). 3. ``test_sources_table_full_width`` — at 1280px ``.table-wrap`` ≥ 80% of the container; below 640px the table keeps its 640px min-width and the wrapper scrolls horizontally instead of squeezing. @@ -55,8 +56,11 @@ REPO = Path(__file__).resolve().parents[2] FIXTURES = REPO / "tests" / "fixtures" / "docs" VIEWPORTS = ((360, 740), (375, 812), (768, 1024), (1280, 800), (1600, 900)) -CHAT_SHELL_CAP_PX = 46 * 16 # 736px — the --chat-column base (PLAN §7.1 lineage) -CHAT_SHELL_WIDE_PX = 92 * 16 # 1472px — the 2x wide override (phase 58, >=1500px) +# Phase 100: the ONE width — the 72rem container, border-box (the +# 2×1.25rem gutters are inside the measured box). The 46rem base and +# the 92rem wide-desktop doubling are retired (owner instruction +# 2026-09-12: "match the width of the RAG page for all other pages"). +CHAT_SHELL_CONTAINER_PX = 72 * 16 # 1152px — the --chat-column = container cap # Mock-LLM marker for a 3s pre-token window (see tests/e2e/mock_llm.py). SLOW_QUESTION = "pretend to think slowly, please" @@ -225,18 +229,19 @@ def test_no_horizontal_overflow_at_viewports( def test_chat_column_capped_and_centered( browser: Browser, app_url: str, db_ready: None ) -> None: - """AC1 (phase 58 contract): the reading column doubles to 92rem on - wide desktops (>=1500px — 1600px here), stays at the 46rem base - below the breakpoint (1280px), and still uses most of the width on - tablets (no mid-column dead zones).""" + """AC1 (phase 100 contract): the reading column is the 72rem + container at every viewport — 1600px here (the cap binds, not the + viewport) and 1280px alike (1152px, ±2%, centered), and still + full-width on tablets (no mid-column dead zones). The phase-58 + 46rem/92rem contract it used to pin is retired.""" page = browser.new_page(viewport={"width": 1600, "height": 900}) try: page.goto(f"{app_url}/") box = page.locator(".chat-shell").bounding_box() assert box is not None - assert CHAT_SHELL_WIDE_PX * 0.98 <= box["width"] <= CHAT_SHELL_WIDE_PX * 1.02, ( - f"at 1600px (>=1500px) the chat column is {box['width']:.0f}px, " - f"not the 92rem wide override (±2%)" + assert CHAT_SHELL_CONTAINER_PX * 0.98 <= box["width"] <= CHAT_SHELL_CONTAINER_PX * 1.02, ( + f"at 1600px the chat column is {box['width']:.0f}px, " + f"not the 72rem container (1152px, ±2%)" ) center = box["x"] + box["width"] / 2 assert abs(center - 1600 / 2) <= 0.02 * 1600, ( @@ -250,9 +255,11 @@ def test_chat_column_capped_and_centered( narrow.goto(f"{app_url}/") box = narrow.locator(".chat-shell").bounding_box() assert box is not None - assert box["width"] <= CHAT_SHELL_CAP_PX * 1.02, ( - f"at 1280px (<1500px) the chat column {box['width']:.0f}px exceeds " - f"the 46rem base cap (+2%)" + # 1280 > 1152: the container cap binds — the SAME 72rem width as + # at 1600px (the cap no longer depends on the viewport at all). + assert box["width"] <= CHAT_SHELL_CONTAINER_PX * 1.02, ( + f"at 1280px the chat column {box['width']:.0f}px exceeds " + f"the 72rem container cap (+2%)" ) finally: narrow.close() diff --git a/tests/e2e/test_save_share_ux.py b/tests/e2e/test_save_share_ux.py index 9ea3af5..9b9e99a 100644 --- a/tests/e2e/test_save_share_ux.py +++ b/tests/e2e/test_save_share_ux.py @@ -41,7 +41,8 @@ unshare, layout) are unchanged. * **Layout (L6 / A5)** — desktop 1280×800: ``#new-chat-btn`` and ``#share-chat-btn`` share one horizontal row inside ``.chat-actions`` (overlapping y bands, Share's x beyond New chat's x + width, each - pill at intrinsic width — never the full 46rem column); mobile + pill at intrinsic width — never the full 72rem container column); + mobile 390×844: stacked vertically (Share below New chat, full-width); 360px wide: no horizontal page overflow; * **Admin still works (A1 sanity)** — the same auto-save machinery @@ -502,7 +503,7 @@ def test_action_row_layout(page: Page, app_url: str, mock_llm: int, db_ready: No # Desktop (1280×800): one horizontal row — overlapping y bands, # Share to the right of New chat, each pill at its INTRINSIC width - # (never the full 46rem chat column). + # (never the full 72rem container column — phase 100). nb = new_btn.bounding_box() sb = share_btn.bounding_box() assert nb is not None and sb is not None diff --git a/tests/e2e/test_sticky_navbar.py b/tests/e2e/test_sticky_navbar.py index 75d78a2..7b2b3a4 100644 --- a/tests/e2e/test_sticky_navbar.py +++ b/tests/e2e/test_sticky_navbar.py @@ -265,7 +265,7 @@ def test_doc_header_stuck_at_top_on_long_document( expect(page.locator("#doc-content .doc-md")).not_to_be_empty() # The doc must actually scroll (the ~40,000-char body fills many - # viewports in the 46rem column). + # viewports in the 72rem container column — phase 100). sh = page.evaluate("() => document.documentElement.scrollHeight") assert sh > VIEWPORT_H, ( f"the long document must make the page scroll " diff --git a/tests/e2e/test_theme_semantic_completion.py b/tests/e2e/test_theme_semantic_completion.py index 0bfa7fd..0e0bfc7 100644 --- a/tests/e2e/test_theme_semantic_completion.py +++ b/tests/e2e/test_theme_semantic_completion.py @@ -979,10 +979,11 @@ def test_reset_removes_tag_byte_identical(page: Page, app_url: str, db_ready: No # The tag is GONE from the live document (theme.js reconciles the # #bor-theme tag's DOM text to the settled, now-default values — # the no-op case removes the tag and clears the - # overrides)… - assert 'id="bor-theme"' not in page.content(), ( - "the reset document must be tag-free" - ) + # overrides). The removal lands in the post-PUT refetch's + # reconcile (the result line above only signals the PUT landed), + # so WAIT for the settled tag-free state rather than racing the + # refetch round-trip… + expect(page.locator("#bor-theme")).to_have_count(0, timeout=30_000) # …and a FRESH load serves NO tag at all (the all-NULL row is the # no-op injection). r = httpx.get(app_url + "/", timeout=10) diff --git a/tests/e2e/test_wide_desktop_column.py b/tests/e2e/test_wide_desktop_column.py index 40a0159..e0d5f2e 100644 --- a/tests/e2e/test_wide_desktop_column.py +++ b/tests/e2e/test_wide_desktop_column.py @@ -1,16 +1,33 @@ -"""Phase 58 E2E (Playwright): the 2x reading column on wide desktops — -measured bounding-box widths, not CSS pins. +"""Phase 100 E2E (Playwright): every page matches the RAG page's width — +the full 72rem container at every viewport, measured bounding boxes. -TODO.md L5 (owner instruction 2026-08-31, roadmap confirmation D2 + -expansion): "The chat response needs to be 2x wider on wide desktops. -there's a lot of unused space." — extended to the document view. The -owner-locked contract: viewport >=1500px doubles ``--chat-column`` to -92rem (1472px at the 16px root) for the four reading shells — -``.chat-shell``, ``.shared-shell``, ``.doc-md`` and -``.doc-summary:has(+ .doc-md)`` — while everything below the breakpoint -renders exactly as before (46rem / 736px) and ``.tuning-shell`` (a -form, not a reading surface) never widens (CSS-pinned by the unit -suite, task 01 — the browser proof of the MEASURED width is this file). +Owner request (chat, 2026-09-12): "The theme, tuning, and chat pages +are still pretty narrow, I want you to match the width of the RAG page +for all other pages to keep things consistent." — SUPERSEDES the +phase-58 2026-08-31 46rem/92rem reading-column contract (recorded per +the phase-94/96/97 convention in `.agents/phases/todo/ +100_page_width_consistency/00_phase.md`, decisions D1–D4). The phase-58 +suite this file used to be updates IN PLACE to the new contract (the +phase-97 task-07/08 precedent: a completed phase's suite follows its +changed contract; its seeding helpers are kept). + +The measured contract (the 16px root, the ±4px tolerance is the task +spec): + +* the container is ``min(100%, 72rem)`` border-box with the 2×1.25rem + gutters INSIDE the box — at any viewport ≥ ~1200px every shell + (``.container.*-shell``) measures **1152px** (72rem), identical on + the chat page, Tuning, Theme, and the RAG page; +* the standalone ``document.html`` page's ``.doc-md`` is the container's + inner content — **1112px** (1152 − 2×1.25rem); +* the same-page document MODAL is untouched (D2): the panel stays + **~1100px** and the panel binds the ``.doc-md`` inside it — + **1058px** (1100 − 2×1px borders − 2×1.25rem of + ``.doc-modal-content`` padding), NOT 1152; +* below the container cap everything is full-width as before: at 360px + no horizontal overflow and the shell is the viewport width; at 900px + chat/tuning/theme all measure 900px (equal — the cap never bound + below 72rem anyway). Run in isolation (DB must be up: ``podman compose up -d db``): @@ -18,33 +35,30 @@ Run in isolation (DB must be up: ``podman compose up -d db``): Test → contract mapping: -1. ``test_chat_column_wide_vs_base`` — ``/`` at 1920×1080: the - ``.chat-shell`` bounding box is 1472px (92rem, ±4px) and centered; - at 1280×800 (below the wide breakpoint) it is back to 736px - (46rem, ±4px). -2. ``test_document_column_wide_vs_base`` — a seeded markdown document - that carries a summary: at 1920 both ``.doc-md`` and the adjacent - ``.doc-summary:has(+ .doc-md)`` panel are 1472px; at 1280 - ``.doc-md`` is 736px. -3. ``test_shared_column_wide`` — a real auto-saved conversation shared - by token: ``/shared/`` at 1920 renders ``.shared-shell`` at - 1472px in a fresh anonymous context. -4. ``test_narrow_unchanged`` — 360px: no horizontal overflow and the - ``.chat-shell`` is the existing mobile rule — full-bleed at the - viewport width (the shell IS its .container; the 0.9rem mobile - gutters live in its own padding, inside the measured box), 900px: - the shell holds the 46rem base (736px, not the 1472px wide rule — - at 900px a leaked wide override would pin the shell to the 860px - content box instead, so 736px is the discriminator) — the wide - rule does not leak below 1500px. +1. ``test_all_columns_match_the_rag_page`` — the core pin: at + 1280×800 the ``.chat-shell`` (``/``), ``.tuning-shell`` + (``/tuning.html``), ``.theme-shell`` (``/theme.html``) and the RAG + page's ``.sources-shell`` (``/sources.html``) bounding boxes are ALL + EQUAL (±4px) — the owner's "match the width of the RAG page" as one + assertion; at 1920×1080 the same four are still equal to each other + and each measures ≈1152px (72rem, ±4px). +2. ``test_reader_columns_wide`` — the token's consumers at 1920×1080: + the modal panel is still ~1100px (D2: the modal is UNCHANGED) with + its ``.doc-md`` at ~1058px; ``/shared/``'s ``.shared-shell`` + is ≈1152px; the standalone ``document.html`` page's ``.doc-md`` is + ≈1112px (the 72rem container's inner content). +3. ``test_narrow_unchanged`` — the no-regression leg: at 360×800 no + horizontal overflow and ``.chat-shell`` is the viewport width + (full-bleed — the mobile gutters sit in the container's padding, + inside the measured box); at 900×600 chat/tuning/theme are all + 900px wide, equal to each other. DB isolation: the shared-chat row is deleted in a ``finally`` (admin cookie — the house pattern of test_share_chat.py, whose distinctive question text keeps the auto-title unique); the fixture document rows follow the story-fixture convention of the sibling suites (truncate + -re-import; the summary document is a direct row insert, the -test_document_viewer.py XSS-fixture pattern — the viewer is -database-only). +re-import; the viewer fixture is a direct row insert — the viewer is +database-only, the test_document_viewer.py XSS-fixture pattern). """ from __future__ import annotations @@ -70,14 +84,22 @@ from e2e.auth_helpers import login REPO = Path(__file__).resolve().parents[2] FIXTURES = REPO / "tests" / "fixtures" / "docs" -# The two proof viewports (task spec): wide desktop vs just below the -# 1500px breakpoint, at the 16px root the phase-58 rem contract: -# 92rem = 1472px, 46rem = 736px. ±4px tolerance (task spec). -WIDE_PX = 92 * 16 # 1472px — the 2x wide override (>=1500px) -BASE_PX = 46 * 16 # 736px — the --chat-column base -TOL_PX = 4 -WIDE_VIEWPORT: ViewportSize = {"width": 1920, "height": 1080} -BASE_VIEWPORT: ViewportSize = {"width": 1280, "height": 800} +# The phase-100 measured contract (16px root, the task spec): +# * CONTAINER_PX — 72rem border-box, the 2×1.25rem gutters INSIDE the +# box; every .container.*-shell measures this at any viewport +# >= ~1200px (the RAG page's width — the owner's ask). +# * DOC_MD_STANDALONE_PX — the standalone document page's .doc-md: the +# container's inner content (CONTAINER_PX minus the gutters). +# * MODAL_PANEL_PX / DOC_MD_MODAL_PX — the SAME-PAGE modal is untouched +# (D2): the 1100px border-box panel (minus its 1px borders) and +# .doc-modal-content's 2×1.25rem padding bind the .doc-md inside it. +CONTAINER_PX = 72 * 16 # 1152px — the 72rem container (border-box) +DOC_MD_STANDALONE_PX = CONTAINER_PX - 2 * 20 # 1112px — inner content +MODAL_PANEL_PX = 1100 # the phase-26 panel ceiling (D2: unchanged) +DOC_MD_MODAL_PX = MODAL_PANEL_PX - 2 - 2 * 20 # 1058px — panel-inner +TOL_PX = 4 # the task spec's ±4px tolerance +VIEWPORT_1280: ViewportSize = {"width": 1280, "height": 800} +VIEWPORT_1920: ViewportSize = {"width": 1920, "height": 1080} MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E" SHARE_URL_RE = re.compile(r"^/shared/[0-9a-f-]{36}$") @@ -139,11 +161,11 @@ def _reset_db(mock_port: int, seed: bool) -> ImportSummary | None: def _seed_summary_doc() -> None: - """One markdown document carrying a stored summary — the ONLY shape - that renders both reading-column pins on one page: ``.doc-summary`` - (the phase-36 panel) directly above ``.doc-md`` (the ``:has( - + .doc-md)`` sibling match). Direct row insert — the viewer is - database-only (the test_document_viewer.py XSS-fixture pattern).""" + """One markdown document carrying a stored summary — the viewer + fixture the phase-100 suite opens in BOTH the same-page modal and + the standalone document page (the test_document_viewer.py + XSS-fixture pattern: a direct row insert, the viewer is + database-only).""" with SessionLocal() as db: db.add( Document( @@ -153,11 +175,11 @@ def _seed_summary_doc() -> None: title=FIXTURE_TITLE, content=( "# Wide Column Fixture\n\n" - "Phase-58 width pin: a markdown document with a " - "summary, so the viewer renders the .doc-summary " - "panel directly above the .doc-md column." + "Phase-100 width pin: a markdown document with a " + "summary, opened in the same-page modal and the " + "standalone document page." ), - summary="A phase-58 fixture summary for the wide-column pin.", + summary="A phase-100 fixture summary for the width pin.", content_hash="w" * 64, indexed_at=datetime.now(UTC), ) @@ -170,14 +192,29 @@ def _seed_summary_doc() -> None: # --------------------------------------------------------------------------- +def _drill(page: Page, *names: str) -> None: + """Phase 97: the catalog is the drill-down tree — click through the + source/folder rows (exact name match, one per name) to the level + that holds the asserted file.""" + for name in names: + page.click(f'#folders-tbody a.folder-link:text-is("{name}")') + + +def _box_width(page: Page, selector: str, label: str) -> float: + """The element's bounding-box width (the measured rendered width, + not the computed style) — None means the element is not rendered, + which is a test error, not a measurement.""" + box = page.locator(selector).first.bounding_box() + assert box is not None, f"{selector} not rendered ({label})" + return box["width"] + + def _assert_width(page: Page, selector: str, expected_px: int, label: str) -> None: """The element's bounding-box width is ``expected_px`` ±4px (task spec) — the measured rendered width, not the computed style.""" - box = page.locator(selector).first.bounding_box() - assert box is not None, f"{selector} not rendered ({label})" - assert abs(box["width"] - expected_px) <= TOL_PX, ( - f"{label}: {selector} is {box['width']:.1f}px, " - f"want {expected_px}px ±{TOL_PX}px" + width = _box_width(page, selector, label) + assert abs(width - expected_px) <= TOL_PX, ( + f"{label}: {selector} is {width:.1f}px, want {expected_px}px ±{TOL_PX}px" ) @@ -265,170 +302,206 @@ def _delete_chat(app_url: str, cookies: dict[str, str], chat_id: str) -> None: httpx.delete(f"{app_url}/api/chats/{chat_id}", timeout=10, cookies=cookies) +def _measure_shells( + browser: Browser, app_url: str, viewport: ViewportSize +) -> dict[str, float]: + """One signed-in page at ``viewport``; measure the bounding-box + width of every shell under test after its view is shown: + + * ``.chat-shell`` on ``/`` (the chat view — the div IS + ``.container.chat-shell``); + * ``.tuning-shell`` on ``/tuning.html``, ``.theme-shell`` on + ``/theme.html``, and the RAG page's ``.sources-shell`` on + ``/sources.html`` (admin deep-links — A11's one shell document). + """ + page = browser.new_page(viewport=viewport) + widths: dict[str, float] = {} + try: + login(page, app_url, next="/") + page.locator(".chat-shell").wait_for(state="visible", timeout=10_000) + widths[".chat-shell"] = _box_width(page, ".chat-shell", "chat") + for path, selector in ( + ("/tuning.html", ".tuning-shell"), + ("/theme.html", ".theme-shell"), + ("/sources.html", ".sources-shell"), + ): + page.goto(app_url + path) + page.locator(selector).wait_for(state="visible", timeout=10_000) + widths[selector] = _box_width(page, selector, path) + finally: + page.close() + return widths + + # --------------------------------------------------------------------------- -# 1. Chat: 1472px at 1920, back to 736px at 1280 (centered both ways) +# 1. The core pin: chat == tuning == theme == RAG page (±4px), at both +# proof viewports; each ≈1152px (the 72rem container) at 1920 # --------------------------------------------------------------------------- -def test_chat_column_wide_vs_base( +def test_all_columns_match_the_rag_page( browser: Browser, app_url: str, db_ready: None ) -> None: - """The chat page's .chat-shell doubles at the 1500px breakpoint: - 92rem (1472px, ±4px) at 1920×1080, centered; 46rem (736px, ±4px) - at 1280×800 — the base below the breakpoint.""" - wide = browser.new_page(viewport=WIDE_VIEWPORT) - try: - login(wide, app_url, next="/") # phase 79: the chips need a session - wide.locator("#suggestions .suggestion-chip").first.wait_for( - state="visible", timeout=10_000 + """The owner's "match the width of the RAG page" as ONE assertion: + at 1280×800 the four shells are all EQUAL (±4px); at 1920×1080 + they are still equal to each other and each measures ≈1152px + (72rem at the 16px root, ±4px — the box includes the container's + 2×1.25rem padding, border-box).""" + # --- 1280×800 (just above the container cap — the owner's report) --- + widths = _measure_shells(browser, app_url, VIEWPORT_1280) + ref_selector, ref = next(iter(widths.items())) + for selector, width in widths.items(): + assert abs(width - ref) <= TOL_PX, ( + f"@1280px {selector} is {width:.1f}px but {ref_selector} is " + f"{ref:.1f}px — the columns must match the RAG page (±{TOL_PX}px)" ) - _assert_width(wide, ".chat-shell", WIDE_PX, "chat @ 1920px") - _assert_centered(wide, ".chat-shell", 1920, "chat @ 1920px") - finally: - wide.close() - base = browser.new_page(viewport=BASE_VIEWPORT) - try: - login(base, app_url, next="/") # phase 79: the chips need a session - base.locator("#suggestions .suggestion-chip").first.wait_for( - state="visible", timeout=10_000 + # --- 1920×1080: equal AND each the 72rem container (≈1152px) ------ + widths = _measure_shells(browser, app_url, VIEWPORT_1920) + ref_selector, ref = next(iter(widths.items())) + for selector, width in widths.items(): + assert abs(width - ref) <= TOL_PX, ( + f"@1920px {selector} is {width:.1f}px but {ref_selector} is " + f"{ref:.1f}px — the columns must match the RAG page (±{TOL_PX}px)" + ) + assert abs(width - CONTAINER_PX) <= TOL_PX, ( + f"@1920px {selector} is {width:.1f}px, want " + f"{CONTAINER_PX}px (the 72rem container) ±{TOL_PX}px" ) - _assert_width(base, ".chat-shell", BASE_PX, "chat @ 1280px") - _assert_centered(base, ".chat-shell", 1280, "chat @ 1280px") - finally: - base.close() # --------------------------------------------------------------------------- -# 2. Document viewer: .doc-md (and its .doc-summary panel) at both -# widths — the owner-expanded surface +# 2. The token's consumers at 1920×1080: the modal (unchanged, D2), the +# shared page, and the standalone document page # --------------------------------------------------------------------------- -def test_document_column_wide_vs_base( +def test_reader_columns_wide( browser: Browser, app_url: str, mock_llm: int, db_ready: None ) -> None: - """A seeded md document (with a summary) renders .doc-md at 1472px - at 1920 and 736px at 1280 — and the adjacent .doc-summary panel - matches the column at 1920 (.doc-summary:has(+ .doc-md)).""" + """At 1920×1080: the same-page document MODAL is untouched (D2 — + the panel is still ~1100px and IT binds the ``.doc-md`` inside, + ~1058px, not 1152); ``/shared/``'s ``.shared-shell`` is + ≈1152px; the standalone ``document.html`` page's ``.doc-md`` is + ≈1112px (the 72rem container's inner content).""" _reset_db(mock_llm, seed=True) _seed_summary_doc() - wide = browser.new_page(viewport=WIDE_VIEWPORT) + page = browser.new_page(viewport=VIEWPORT_1920) try: - login(wide, app_url, next="/") # phase 79: the viewer content is gated - wide.goto(app_url + FIXTURE_DOC_URL) - expect(wide.locator("#doc-title")).to_have_text(FIXTURE_TITLE, timeout=15_000) - expect(wide.locator("#doc-content .doc-md")).to_be_visible(timeout=15_000) - expect( - wide.locator(".doc-summary:has(+ .doc-md)") - ).to_have_count(1, timeout=15_000) - _assert_width(wide, "#doc-content .doc-md", WIDE_PX, "document @ 1920px") + # (a) the same-page modal — the 1100px panel stays its effective + # ceiling (D2: pin the PANEL, not the 1152 the column would be). + login(page, app_url, next="/sources.html") + page.locator("#folders-tbody .folder-link").first.wait_for( + state="visible", timeout=15_000 + ) + _drill(page, FIXTURE_SOURCE, "notes") + row = page.locator("#docs-tbody tr", has_text="wide-column-fixture.md") + expect(row).to_have_count(1) + row.locator("td:nth-child(2) a.doc-link").click() + expect(page.locator(".doc-modal")).to_be_visible() + expect(page.locator("#doc-modal-title")).to_have_text(FIXTURE_TITLE) + _assert_width(page, "#doc-modal-panel", MODAL_PANEL_PX, "modal panel @ 1920px") _assert_width( - wide, ".doc-summary:has(+ .doc-md)", WIDE_PX, "summary panel @ 1920px" + page, + "#doc-modal-content .doc-md", + DOC_MD_MODAL_PX, + "modal .doc-md @ 1920px", ) - finally: - wide.close() - base = browser.new_page(viewport=BASE_VIEWPORT) - try: - login(base, app_url, next="/") # phase 79: the viewer content is gated - base.goto(app_url + FIXTURE_DOC_URL) - expect(base.locator("#doc-title")).to_have_text(FIXTURE_TITLE, timeout=15_000) - expect(base.locator("#doc-content .doc-md")).to_be_visible(timeout=15_000) - _assert_width(base, "#doc-content .doc-md", BASE_PX, "document @ 1280px") + # (b) the standalone document page: the 72rem container's inner + # content (1152 − 2×1.25rem gutters). + page.goto(app_url + FIXTURE_DOC_URL) + expect(page.locator("#doc-title")).to_have_text(FIXTURE_TITLE, timeout=15_000) + expect(page.locator("#doc-content .doc-md")).to_be_visible(timeout=15_000) + _assert_width( + page, + "#doc-content .doc-md", + DOC_MD_STANDALONE_PX, + "standalone .doc-md @ 1920px", + ) + + # (c) the shared page: .shared-shell at the full 72rem container. + page.goto(app_url + "/") + q = "How is my Kubernetes cluster set up? (wide-column)" + _ask(page, q) + cookies = _admin_cookies(page) + saved_row = _wait_saved_row(app_url, cookies, _auto_title(q)) + chat_id: str = saved_row["id"] + anon_ctx = None + try: + # Public since phase 55 — the share endpoint takes no session. + r = httpx.post(f"{app_url}/api/chats/{chat_id}/share", timeout=10) + assert r.status_code == 200 + share_url = r.json()["share_url"] + assert SHARE_URL_RE.fullmatch(share_url), f"bad share_url shape: {share_url}" + + anon_ctx = browser.new_context(viewport=VIEWPORT_1920) + anon = anon_ctx.new_page() + anon.set_default_timeout(30_000) + anon.goto(app_url + share_url) + expect(anon.locator("#shared-title")).to_have_text( + _auto_title(q), timeout=15_000 + ) + expect(anon.locator(".msg.brain .bubble")).to_contain_text( + MOCK_ANSWER_MARKER, timeout=30_000 + ) + _assert_width(anon, ".shared-shell", CONTAINER_PX, "shared @ 1920px") + _assert_centered(anon, ".shared-shell", 1920, "shared @ 1920px") + finally: + if anon_ctx is not None: + anon_ctx.close() + _delete_chat(app_url, cookies, chat_id) finally: - base.close() + page.close() # --------------------------------------------------------------------------- -# 3. Shared page: .shared-shell at 1472px for a guest at 1920 -# --------------------------------------------------------------------------- - - -def test_shared_column_wide( - page: Page, - browser: Browser, - app_url: str, - mock_llm: int, - db_ready: None, -) -> None: - """A real auto-saved conversation, shared by token, renders its - .shared-shell at 1472px (±4px) at 1920 in a FRESH anonymous - context (no cookies — the guest's only credential is the token).""" - _reset_db(mock_llm, seed=True) - page.set_default_timeout(30_000) - - login(page, app_url, next="/") - expect(page).to_have_url(app_url + "/", timeout=30_000) - - q = "How is my Kubernetes cluster set up? (wide-column)" - _ask(page, q) - - cookies = _admin_cookies(page) - row = _wait_saved_row(app_url, cookies, _auto_title(q)) - chat_id: str = row["id"] - anon_ctx = None - try: - # Public since phase 55 — the share endpoint takes no session. - r = httpx.post(f"{app_url}/api/chats/{chat_id}/share", timeout=10) - assert r.status_code == 200 - share_url = r.json()["share_url"] - assert SHARE_URL_RE.fullmatch(share_url), f"bad share_url shape: {share_url}" - - anon_ctx = browser.new_context(viewport=WIDE_VIEWPORT) - anon = anon_ctx.new_page() - anon.set_default_timeout(30_000) - anon.goto(app_url + share_url) - expect(anon.locator("#shared-title")).to_have_text( - _auto_title(q), timeout=15_000 - ) - expect(anon.locator(".msg.brain .bubble")).to_contain_text( - MOCK_ANSWER_MARKER, timeout=30_000 - ) - _assert_width(anon, ".shared-shell", WIDE_PX, "shared @ 1920px") - _assert_centered(anon, ".shared-shell", 1920, "shared @ 1920px") - finally: - if anon_ctx is not None: - anon_ctx.close() - _delete_chat(app_url, cookies, chat_id) - - -# --------------------------------------------------------------------------- -# 4. Below the breakpoint: 360px and 900px are byte-for-byte the old -# rules — no overflow, no leaked wide column +# 3. Below the container cap: full-width as today — 360px overflow-free, +# 900px chat == tuning == theme (the cap never bound below 72rem) # --------------------------------------------------------------------------- def test_narrow_unchanged( browser: Browser, app_url: str, db_ready: None ) -> None: - """The min-width:1500px override must not leak below the - breakpoint: at 360px no horizontal overflow and the shell is the - existing mobile rule — full-bleed at the viewport width (the shell - IS its .container, so the 0.9rem mobile gutters sit in its own - padding, inside the measured box); at 900px the shell holds the - 46rem base (736px — a leaked 92rem rule would pin it to the 860px - content box instead, so 736px is the discriminator).""" - phone = browser.new_page(viewport={"width": 360, "height": 740}) + """The mobile/tablet layouts are UNCHANGED (D3): at 360×800 no + horizontal overflow and ``.chat-shell`` is the viewport width + (full-bleed — the shell IS its .container, so the mobile gutters sit + in its padding, inside the measured box); at 900×600 the + chat/tuning/theme shells are ALL 900px wide, equal to each other + (the cap never bound below 72rem anyway).""" + phone = browser.new_page(viewport={"width": 360, "height": 800}) try: - login(phone, app_url, next="/") # phase 79: the chips need a session - phone.locator("#suggestions .suggestion-chip").first.wait_for( - state="visible", timeout=10_000 - ) + login(phone, app_url, next="/") + phone.locator(".chat-shell").wait_for(state="visible", timeout=10_000) _assert_no_doc_overflow(phone, "chat @ 360px") _assert_width(phone, ".chat-shell", 360, "chat @ 360px") finally: phone.close() - tablet = browser.new_page(viewport={"width": 900, "height": 800}) + tablet = browser.new_page(viewport={"width": 900, "height": 600}) try: - login(tablet, app_url, next="/") # phase 79: the chips need a session - tablet.locator("#suggestions .suggestion-chip").first.wait_for( - state="visible", timeout=10_000 - ) + login(tablet, app_url, next="/") + tablet.locator(".chat-shell").wait_for(state="visible", timeout=10_000) _assert_no_doc_overflow(tablet, "chat @ 900px") - _assert_width(tablet, ".chat-shell", BASE_PX, "chat @ 900px") - _assert_centered(tablet, ".chat-shell", 900, "chat @ 900px") + widths = {".chat-shell": _box_width(tablet, ".chat-shell", "chat @ 900px")} + for path, selector in ( + ("/tuning.html", ".tuning-shell"), + ("/theme.html", ".theme-shell"), + ): + tablet.goto(app_url + path) + tablet.locator(selector).wait_for(state="visible", timeout=10_000) + widths[selector] = _box_width(tablet, selector, f"{path} @ 900px") + ref_selector, ref = next(iter(widths.items())) + for selector, width in widths.items(): + assert abs(width - ref) <= TOL_PX, ( + f"@900px {selector} is {width:.1f}px but {ref_selector} is " + f"{ref:.1f}px — the cap must not bind below the container cap" + ) + assert abs(width - 900) <= TOL_PX, ( + f"@900px {selector} is {width:.1f}px, want the full 900px " + f"viewport width ±{TOL_PX}px" + ) finally: tablet.close() diff --git a/tests/unit/test_doc_edit_screen.py b/tests/unit/test_doc_edit_screen.py index c205cfd..43cb729 100644 --- a/tests/unit/test_doc_edit_screen.py +++ b/tests/unit/test_doc_edit_screen.py @@ -10,8 +10,9 @@ without a browser: * the house shell (AGENTS.md rule 5 + the login.html/shared.html minimal-flow-page lineage): skip-link, the SLIM header (brand + - "Back to chat" — no nav), the 46rem base column (hard-coded — a form - column, NOT ``--chat-column``), the ``container`` frame; + "Back to chat" — no nav), the full 72rem container (the phase-59 + form-column cap is retired — owner instruction 2026-09-12), the + ``container`` frame; * the form contract: ``#draft-title`` / ``#draft-path`` / ``#draft-body`` with visible labels, ``#push-doc-btn`` (the exact "Push to docs branch" copy) + the back link, ``#push-status`` @@ -455,21 +456,25 @@ def test_trim_git_detail_behavior_is_pinned_by_the_markers() -> None: # ---------- styles.css: the new classes ---------- -def test_doc_edit_shell_is_the_hardcoded_46rem_column() -> None: - """.doc-edit-shell: the 46rem base column — HARD-CODED 46rem (a - form column, not a reading column — it must NOT ride - --chat-column, so phase 58's wide-desktop doubling never stretches - the form), centered, a flex column on the container frame.""" +def test_doc_edit_shell_rides_the_full_container() -> None: + """.doc-edit-shell: the full 72rem container (owner instruction + 2026-09-12 — the phase-59 form-column cap is superseded): no + max-width, no margin-inline (the .container ancestor centers), a + flex column on the container frame.""" css = _css() block = re.search(r"\.doc-edit-shell \{([\s\S]*?)\n\}", css) assert block, "styles.css must style .doc-edit-shell" body = block.group(1) - assert "max-width: 46rem" in body, "the 46rem base column (hard-coded)" + assert "max-width" not in body, ( + "no cap — the shell rides the .container's 72rem frame" + ) assert "--chat-column" not in body, ( - "the form column does not ride --chat-column (phase 58 must " - "not stretch it)" + "the shell rides the .container frame, not the reading-column " + "token" + ) + assert "margin-inline" not in body, ( + "the .container ancestor centers (no shell-level centering)" ) - assert "margin-inline: auto" in body assert "flex-direction: column" in body diff --git a/tests/unit/test_markdown_tables.py b/tests/unit/test_markdown_tables.py index e6c4c46..5004ee6 100644 --- a/tests/unit/test_markdown_tables.py +++ b/tests/unit/test_markdown_tables.py @@ -206,7 +206,8 @@ def test_header_comment_notes_the_table_pass() -> None: def test_table_wrap_is_the_horizontal_scroller() -> None: """.md-table-wrap { overflow-x: auto } — the wrapper (not the table) is the scroller, so a wide table scrolls inside the bubble - instead of breaking the 46rem column (story AC5).""" + instead of breaking the 72rem column (story AC5 — the width + contract is now the 72rem container, phase 100).""" css = _text(STYLES_CSS) block = re.search(r"\.md-table-wrap\s*\{([^}]*)\}", css) assert block, "styles.css must define .md-table-wrap" diff --git a/tests/unit/test_pinned_composer.py b/tests/unit/test_pinned_composer.py index ce04187..3ace264 100644 --- a/tests/unit/test_pinned_composer.py +++ b/tests/unit/test_pinned_composer.py @@ -6,8 +6,8 @@ be at the bottom of the screen on an EMPTY chat too, which sticky alone cannot do). The chat page scrolls at the DOCUMENT level and `.chat-shell` (the -centered 46rem column, PLAN §7.1) is the composer's sticky containing -block. The pin is therefore TWO rules, and both are pinned here: +centered 72rem column — the .container width, phase 100) is the +composer's sticky containing block. The pin is therefore TWO rules, and both are pinned here: * `.messages { flex: 1 1 auto }` absorbs the free space of a short page, so the composer's RESTING (in-flow) position is already the bottom of diff --git a/tests/unit/test_remove_confirm_modal.py b/tests/unit/test_remove_confirm_modal.py index 5e537ee..0560a06 100644 --- a/tests/unit/test_remove_confirm_modal.py +++ b/tests/unit/test_remove_confirm_modal.py @@ -450,8 +450,9 @@ def test_modal_css_targets_and_contrast_pairs() -> None: 9.3:1, the err-line border — the .tuning-delete / .steering-delete convention; the hover inverts to --bg on --err-line, 5.2:1); Cancel is the ghost ink-soft family (5.1:1 on --surface); the - error line is the err pair; the panel caps at the 46rem - chat-column width or the viewport.""" + error line is the err pair; the panel caps at the chat-column + width (the --chat-column token — 72rem, owner instruction + 2026-09-12) or the viewport.""" css = _css() btn = css[css.find(".remove-confirm-btn {"):] btn = btn[: btn.find("\n}")] @@ -473,4 +474,6 @@ def test_modal_css_targets_and_contrast_pairs() -> None: assert "var(--err-ink)" in err and "var(--err-bg)" in err panel = css[css.find(".remove-confirm-panel {"):] panel = panel[: panel.find("\n}")] - assert "min(46rem" in panel, "the 46rem chat-column cap (or the viewport)" + assert "min(var(--chat-column)" in panel, ( + "the chat-column cap via the token (or the viewport)" + ) diff --git a/tests/unit/test_shared_page.py b/tests/unit/test_shared_page.py index 5f41b5a..72614ae 100644 --- a/tests/unit/test_shared_page.py +++ b/tests/unit/test_shared_page.py @@ -19,8 +19,8 @@ silent regression is caught without a browser: chat page's interactive builders, and the rendered messages contain no button/form/link — the chips are plain spans, the source chips carry no ``href``; -* the shared shell's reading-column mapping (--chat-column token, - phase 58) + the static-chip and +* the shared shell's reading-column mapping (--chat-column token — + the 72rem container width, phase 100) + the static-chip and invalid-state CSS (the ≤640px squeeze included). """ from __future__ import annotations @@ -384,17 +384,18 @@ def test_brand_note_resolves_at_call_time() -> None: def test_shared_shell_maps_to_the_reading_column() -> None: - """The PLAN §7 column contract (phase 58): .shared-shell is the - centered chat column riding the --chat-column token (46rem base, - 92rem at >=1500px wide desktops — owner instruction 2026-08-31, - TODO L5 / D2). The conversation reads exactly like the chat - page's, so the existing .msg/.bubble CSS applies unchanged.""" + """The 72rem-everywhere contract (phase 100, owner instruction + 2026-09-12): .shared-shell is the centered chat column riding the + --chat-column token — equal to the .container's cap, so the shared + page reads at the RAG page's width. The conversation reads exactly + like the chat page's, so the existing .msg/.bubble CSS applies + unchanged.""" css = _css() block = re.search(r"\.shared-shell \{([\s\S]*?)\n\}", css) assert block, "styles.css must style .shared-shell" body = block.group(1) assert "max-width: var(--chat-column)" in body, ( - "the PLAN §7 centered chat column (phase 58 token)" + "the centered chat column riding the token (the 72rem width)" ) assert "46rem" not in body.split("/*")[0], ( "no hard-coded cap — the width rides the token" diff --git a/tests/unit/test_source_ignore_paths.py b/tests/unit/test_source_ignore_paths.py index 8902970..58a7404 100644 --- a/tests/unit/test_source_ignore_paths.py +++ b/tests/unit/test_source_ignore_paths.py @@ -499,7 +499,8 @@ def test_dialog_button_and_target_contrast_pairs() -> None: brand-soft hover (12.4:1); Save is the solid brand family (--bg text on --brand 5.2:1, the .new-chat-btn convention) with the lightened hover; the error line is the err pair; the panel caps - at the 46rem chat-column width or the viewport; the visible + at the chat-column width (the --chat-column token — 72rem, owner + instruction 2026-09-12) or the viewport; the visible label is ink-soft (5.1:1) — never a label-less textarea.""" css = _css() btn = _css_rule(css, ".ignore-editor-btn") @@ -517,7 +518,9 @@ def test_dialog_button_and_target_contrast_pairs() -> None: err = _css_rule(css, ".ignore-editor-error") assert "var(--err-ink)" in err and "var(--err-bg)" in err panel = _css_rule(css, ".ignore-editor-panel") - assert "min(46rem" in panel, "the 46rem chat-column cap (or the viewport)" + assert "min(var(--chat-column)" in panel, ( + "the chat-column cap via the token (or the viewport)" + ) label = _css_rule(css, ".ignore-editor-label") assert "display: block" in label and "var(--ink-soft)" in label, ( "a visible block label (WCAG — never aria-label-only)" diff --git a/tests/unit/test_wide_column_css.py b/tests/unit/test_wide_column_css.py index e331a2f..a72f085 100644 --- a/tests/unit/test_wide_column_css.py +++ b/tests/unit/test_wide_column_css.py @@ -1,29 +1,33 @@ -"""Unit: the 2x reading column on wide desktops (phase 58, task 01). +"""Unit: every page matches the RAG page's 72rem width (phase 100). -The measured-width browser proof is E2E-gated by the phase-58 story -suite (task 02); like the other frontend-adjacent unit files (the +The measured-width browser proof is E2E-gated by the phase's width +suite (task 03); like the other frontend-adjacent unit files (the test_save_chat_ui.py pattern), this module pins the styles.css markers -the wide-column contract depends on, so a silent regression is caught +the width contract depends on, so a silent regression is caught without a browser: -* the ``--chat-column`` custom property in ``:root`` — 46rem base - (the PLAN §7 column lineage) with the provenance comment (owner - instruction 2026-08-31, TODO L5 / D2); -* the ``@media (min-width: 1500px)`` block at the bottom of the - responsive region — the SINGLE place that doubles the token to - 92rem (2x); +* the ``--chat-column`` custom property in ``:root`` — 72rem, EQUAL to + the ``.container``'s 72rem cap (owner instruction 2026-09-12: + "match the width of the RAG page for all other pages" — supersedes + the 2026-08-31 instruction), with the provenance comment; +* NO ``@media (min-width: 1500px)`` block anywhere in the file — the + phase-58 wide-desktop doubling is deleted in full (the token is + 72rem at every viewport); * the four reading-column selectors — ``.chat-shell``, ``.shared-shell``, ``.doc-md``, ``.doc-summary:has(+ .doc-md)`` — each capped with ``max-width: var(--chat-column)`` and NOTHING else - in the file uses the token (exactly four rules); -* the negative pin — the form columns (``.tuning-shell``; and from - phase 59, task 06, ``.doc-edit-shell`` — forms, not reading - surfaces) are the only literal ``max-width: 46rem`` rules left in - the file, kept hard-coded so the wide-desktop doubling never - stretches a form; -* the "46rem column contract" block comments were updated to name the - base value + the wide override (the stale "≤46rem" contract claims - are gone from the reading-column comments). + in the file uses the token for a max-width (exactly four rules); +* the flipped negative pin — ZERO literal ``max-width: 46rem`` rules + remain (the phase-27/59/91 form-column caps are retired): the form + shells (``.tuning-shell``, ``.theme-shell``, ``.doc-edit-shell``) + carry no cap, no token, and no ``margin-inline`` — structurally + ``.sources-shell``; +* the two source-page dialogs (phase 69 remove-confirm, phase 89 + ignore-editor) cap at the chat-column width THROUGH THE TOKEN — + ``min(var(--chat-column), calc(100vw - 2rem))`` — or the viewport; +* the stale width comments are gone — no "46rem base" / "92rem at + >=1500px" / "1500px" width claims remain (the "46rem column + contract" wording was rewritten to the 72rem contract, 2026-09-12). """ from __future__ import annotations @@ -58,57 +62,52 @@ def _rule_block(css: str, selector: str) -> str: # ---------- the --chat-column token ---------- -def test_root_declares_chat_column_46rem_base() -> None: - """:root declares --chat-column: 46rem (the PLAN §7 base) with the - owner-provenance comment (instruction 2026-08-31, TODO L5).""" +def test_root_declares_chat_column_72rem_equal_to_the_container() -> None: + """:root declares --chat-column: 72rem — EQUAL to the .container's + 72rem cap (one width for everything) — with the owner-provenance + comment (instruction 2026-09-12).""" css = _css() root = _rule_block(css, ":root") - assert "--chat-column: 46rem" in root, ( - ":root must declare the --chat-column base (46rem)" + assert "--chat-column: 72rem" in root, ( + ":root must declare the --chat-column width (72rem)" ) - pre = css[: css.index("--chat-column: 46rem")] + # The token equals the container's cap (the RAG page's width). + container = _rule_block(css, ".container") + assert "max-width: 72rem" in container, ( + ".container's 72rem cap is the width the token now equals" + ) + pre = css[: css.index("--chat-column: 72rem")] comment = pre[pre.rindex("/*") : pre.rindex("*/")] - assert "owner instruction 2026-08-31" in comment, ( + assert "owner instruction 2026-09-12" in comment, ( "the token's comment must cite the owner instruction " - "(2026-08-31, TODO L5)" + "(2026-09-12 — match the width of the RAG page)" ) -def test_wide_media_block_doubles_the_token() -> None: - """A @media (min-width: 1500px) block sets --chat-column: 92rem on - :root — the single wide override (2x the base).""" +def test_the_wide_desktop_doubling_is_retired() -> None: + """No @media (min-width: 1500px) block remains (the phase-58 2x + override is deleted in full, 2026-09-12) and the token is never + assigned the old wide value — 72rem at every viewport.""" css = _css() - m = re.search(r"@media \(min-width: 1500px\) \{", css) - assert m, "styles.css must carry the @media (min-width: 1500px) block" - start = css.index("{", m.start()) - depth = 0 - for i in range(start, len(css)): - if css[i] == "{": - depth += 1 - elif css[i] == "}": - depth -= 1 - if depth == 0: - block = css[m.start() : i + 1] - break - else: - raise AssertionError("unbalanced braces in the wide media block") - assert ":root { --chat-column: 92rem; }" in block, ( - "the wide block must set :root { --chat-column: 92rem; }" + assert "@media (min-width: 1500px)" not in css, ( + "the wide-desktop media block must be deleted (retired 2026-09-12)" + ) + assert "--chat-column: 92rem" not in css, ( + "the token is never doubled to the old wide value" ) - # The wide block is the ONLY min-width:1500 media in the file and - # the only place 92rem is assigned to the token. - assert css.count("@media (min-width: 1500px)") == 1 - assert css.count("--chat-column: 92rem") == 1 -def test_wide_block_lives_in_the_bottom_responsive_region() -> None: - """The min-width sibling sits alongside the max-width responsive - blocks at the bottom of the file (after the <=640px block).""" +def test_no_literal_46rem_width_remains() -> None: + """Flipped negative pin (phase 100 D1): ZERO literal + max-width: 46rem rules remain in the file — the phase-27/59/91 + form-column caps are retired, and the dialog panels ride the + token, not a 46rem literal.""" css = _css() - wide = css.index("@media (min-width: 1500px)") - mobile = css.rindex("@media (max-width: 640px)") - assert wide > mobile, ( - "the wide override belongs in the bottom media-query region" + assert css.count("max-width: 46rem") == 0, ( + "no literal max-width: 46rem rule may remain (retired 2026-09-12)" + ) + assert "min(46rem" not in css, ( + "the dialog panels must ride the token, not a 46rem literal" ) @@ -119,7 +118,7 @@ def test_the_four_reading_columns_use_the_token() -> None: """.chat-shell, .shared-shell, .doc-md and .doc-summary:has(+ .doc-md) each cap with max-width: var(--chat-column) — and exactly those four rules use - the token (no other selector).""" + the token for a max-width (no other selector).""" css = _css() for selector in ( ".chat-shell", @@ -135,73 +134,89 @@ def test_the_four_reading_columns_use_the_token() -> None: ) -def test_shared_shell_keeps_the_centered_column_comment() -> None: - """.shared-shell's inline comment keeps the "centered chat column" - wording and notes the wide override (task 01 work item).""" - css = _css() - rule = css[css.index(".shared-shell {") : css.index(".shared-shell {") + 400] - assert "the PLAN §7 centered chat column" in rule - assert "92rem at >=1500px" in rule, "the comment must note the wide override" - - def test_doc_md_keeps_width_100_under_the_cap() -> None: """.doc-md stays width:100% under the token cap (the modal's - 1100px panel remains its effective ceiling there).""" + 1100px panel remains its effective ceiling there — phase 100 D2).""" assert "width: 100%" in _rule_block(_css(), ".doc-md") -# ---------- the negative pins ---------- +# ---------- the form shells + the dialog panels ---------- -def test_tuning_shell_stays_hardcoded_46rem() -> None: - """.tuning-shell (the form column, out of scope) keeps its - hard-coded max-width: 46rem at every width — it never widens.""" +def test_the_form_shells_ride_the_full_container() -> None: + """The form shells (.tuning-shell — phase 27, .theme-shell — + phase 91, .doc-edit-shell — phase 59) carry NO cap, NO token, and + NO margin-inline: the .container ancestor centers them, so they + ride the 72rem frame like .sources-shell (flex column, + gap 1.25rem, flex: 1 — the owner instruction 2026-09-12 retires + the form-column caps).""" css = _css() - tuning = _rule_block(css, ".tuning-shell") - assert "max-width: 46rem" in tuning, ( - ".tuning-shell must stay hard-coded 46rem (negative pin)" - ) - assert "var(--chat-column)" not in tuning, ( - ".tuning-shell must NOT reference the reading-column token" - ) + for selector in (".tuning-shell", ".theme-shell", ".doc-edit-shell"): + block = _rule_block(css, selector) + assert "max-width" not in block, ( + f"{selector} must carry no cap (retired 2026-09-12)" + ) + assert "var(--chat-column)" not in block, ( + f"{selector} rides the .container frame, not the token" + ) + assert "margin-inline" not in block, ( + f"{selector} is centered by the .container ancestor" + ) + for decl in ( + "display: flex", + "flex-direction: column", + "gap: 1.25rem", + "flex: 1", + ): + assert decl in block, ( + f"{selector} keeps the .sources-shell shape ({decl})" + ) -def test_no_other_hardcoded_46rem_rule_remains() -> None: - """After the switch, the form columns are the ONLY rules with a - literal max-width: 46rem: .tuning-shell (phase 27), - .doc-edit-shell (phase 59, task 06 — the doc edit screen is a - FORM column, not a reading column, so it must not ride - --chat-column and phase 58's wide-desktop doubling must never - stretch the form), and .theme-shell (phase 91 task 04 — the - admin Theme editor is a form column too: the palette grid + - fieldsets must never ride the wide-desktop doubling). Every - reading column rides the token (the --chat-column base - declaration is the other non-rule occurrence of 46rem).""" +def test_the_dialog_panels_ride_the_token() -> None: + """The two source-page dialogs (phase 69 remove-confirm, phase 89 + ignore-editor) cap at the chat-column width THROUGH THE TOKEN — + min(var(--chat-column), calc(100vw - 2rem)) — or the viewport, + whichever is narrower (the 46rem literals ride no more).""" css = _css() - assert css.count("max-width: 46rem") == 3, ( - "only the form columns (.tuning-shell, .doc-edit-shell, " - ".theme-shell) may keep a literal max-width: 46rem" - ) - assert "max-width: 46rem" in _rule_block(css, ".tuning-shell") - assert "max-width: 46rem" in _rule_block(css, ".doc-edit-shell") - assert "max-width: 46rem" in _rule_block(css, ".theme-shell") + for selector in (".remove-confirm-panel", ".ignore-editor-panel"): + block = _rule_block(css, selector) + assert "min(var(--chat-column), calc(100vw - 2rem))" in block, ( + f"{selector} must ride the token (the chat-column width " + "or the viewport)" + ) -def test_comments_cite_the_wide_override_with_provenance() -> None: - """The block comments that claimed the "46rem column contract" now - name base 46rem + the 2x wide override, with the owner - instruction (2026-08-31, TODO L5) as the provenance at the token - and the media block.""" +# ---------- the comment contract ---------- + + +def test_comments_carry_the_72rem_contract() -> None: + """The stale width claims are gone from the file: no "≤46rem", + "46rem base", "92rem at >=1500px" or "1500px" wording (the + 2026-08-31 contract was rewritten to the 72rem contract), with the + owner instruction (2026-09-12) as the provenance at the token and + the reading-column comments naming 72rem.""" css = _css() - # The stale "≤46rem" contract claims are gone from the file. assert "≤46rem" not in css, ( "the stale '≤46rem' contract wording must be updated" ) - # Provenance at the two authoritative spots (token + wide block). - token_idx = css.index("--chat-column: 46rem") - wide_idx = css.index("@media (min-width: 1500px)") - assert "owner instruction 2026-08-31" in css[max(0, token_idx - 400) : token_idx] - assert "owner instruction 2026-08-31" in css[max(0, wide_idx - 500) : wide_idx] - # The chat-shell comment names base + override. + assert "46rem base" not in css, "no stale '46rem base' claim may remain" + assert "92rem at >=1500px" not in css, ( + "no stale wide-override claim may remain" + ) + assert "1500px" not in css, ( + "the wide-desktop breakpoint wording is retired (2026-09-12)" + ) + # Provenance at the authoritative spot (the token). + token_idx = css.index("--chat-column: 72rem") + assert "owner instruction 2026-09-12" in css[max(0, token_idx - 400) : token_idx] + # The chat-shell + shared-shell comments name the 72rem contract. chat_comment = css[: css.index(".chat-shell {")] - assert "46rem base" in chat_comment and "92rem" in chat_comment + assert "72rem" in chat_comment, ( + "the chat-shell comment must name the 72rem contract" + ) + shared_idx = css.index(".shared-shell {") + shared_rule = css[shared_idx : shared_idx + 400] + assert "72rem" in shared_rule, ( + "the shared-shell comment must name the 72rem contract" + )