diff --git a/.agents/phases/todo/86_history_page_width/00_phase.md b/.agents/phases/complete/86_history_page_width/00_phase.md similarity index 100% rename from .agents/phases/todo/86_history_page_width/00_phase.md rename to .agents/phases/complete/86_history_page_width/00_phase.md diff --git a/.agents/phases/todo/86_history_page_width/01_table_wrap_fix.md b/.agents/phases/complete/86_history_page_width/01_table_wrap_fix.md similarity index 100% rename from .agents/phases/todo/86_history_page_width/01_table_wrap_fix.md rename to .agents/phases/complete/86_history_page_width/01_table_wrap_fix.md diff --git a/.agents/phases/todo/86_history_page_width/02_e2e_story_suite.md b/.agents/phases/complete/86_history_page_width/02_e2e_story_suite.md similarity index 100% rename from .agents/phases/todo/86_history_page_width/02_e2e_story_suite.md rename to .agents/phases/complete/86_history_page_width/02_e2e_story_suite.md diff --git a/.agents/phases/todo/86_history_page_width/03_verify_and_commit.md b/.agents/phases/complete/86_history_page_width/03_verify_and_commit.md similarity index 100% rename from .agents/phases/todo/86_history_page_width/03_verify_and_commit.md rename to .agents/phases/complete/86_history_page_width/03_verify_and_commit.md diff --git a/.agents/reports/86_history_page_width/86_history_page_width__00_phase.a1.err b/.agents/reports/86_history_page_width/86_history_page_width__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/86_history_page_width/86_history_page_width__00_phase.a1.md b/.agents/reports/86_history_page_width/86_history_page_width__00_phase.a1.md new file mode 100644 index 0000000..4e82fb2 --- /dev/null +++ b/.agents/reports/86_history_page_width/86_history_page_width__00_phase.a1.md @@ -0,0 +1,9 @@ +All criteria verified. Final report: + +**Phase 86 final verification pass — all green, no defects found.** + +- **Verified (previously implemented):** `position: relative` on shared `.table-wrap` (the one-rule A3 fix, +12 lines in `frontend/assets/styles.css`); unit pin `tests/unit/test_table_wrap_stacking.py` (3 tests); E2E story `tests/e2e/test_history_page_width.py` (5 tests: History 626→≤375, Tokens 618→≤375, SPA switch with window-sentinel, RAG regression, desktop 1280×800) +- **Tests/lint (this pass):** `uv run pytest` → 1720 passed · `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90% floor) · E2E in isolation: `test_history_page_width.py` 5 passed, `test_chat_history.py` 5 passed, `test_api_tokens.py` 9 passed, `test_smoke.py` 3 passed · `uv run ruff check . && uv run pyright` → clean, 0 errors +- **Completion criteria:** ① 375px direct+SPA `scrollWidth ≤ innerWidth` on History/Tokens — PASS (E2E pins) ② in-card scroll preserved + full-width + a11y spans in DOM — PASS ③ Sources + desktop regression — PASS ④ full suite/coverage/E2E/lint gates — PASS ⑤ `git diff --stat` limited to `styles.css` (+12) + new test files + phase files, no `app/`/markup/JS — PASS ⑥ commit/move — left to harness per executor rules (working tree intact, `todo/` dir removal already reflected) +- **Notable:** red→green (CSS reverted → 626px failure) and manual live check already recorded in `.agents/reports/86_history_page_width/`; pre-existing untracked `.agents/remediation_plan.md` (Sep 7 security audit) untouched +- **Next pending phase:** `87_big_read_progress` diff --git a/.agents/reports/86_history_page_width/86_history_page_width__00_phase.a1.validate b/.agents/reports/86_history_page_width/86_history_page_width__00_phase.a1.validate new file mode 100644 index 0000000..bb6cf10 --- /dev/null +++ b/.agents/reports/86_history_page_width/86_history_page_width__00_phase.a1.validate @@ -0,0 +1,85 @@ +........................................................................ [ 4%] +........................................................................ [ 8%] +........................................................................ [ 12%] +........................................................................ [ 16%] +........................................................................ [ 20%] +........................................................................ [ 25%] +........................................................................ [ 29%] +........................................................................ [ 33%] +........................................................................ [ 37%] +........................................................................ [ 41%] +........................................................................ [ 46%] +........................................................................ [ 50%] +........................................................................ [ 54%] +........................................................................ [ 58%] +........................................................................ [ 62%] +........................................................................ [ 66%] +........................................................................ [ 71%] +........................................................................ [ 75%] +........................................................................ [ 79%] +........................................................................ [ 83%] +........................................................................ [ 87%] +........................................................................ [ 92%] +........................................................................ [ 96%] +................................................................ [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 178 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 7 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 50 0 100% +app/api/git_sources.py 212 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 98 0 100% +app/api/tokens.py 28 0 100% +app/config.py 141 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 108 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 19 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 64 0 100% +app/models.py 94 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 222 0 100% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 180 3 98% +app/rag/llm.py 217 0 100% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 232 0 100% +-------------------------------------------------- +TOTAL 3237 12 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +validation OK diff --git a/.agents/reports/86_history_page_width/86_history_page_width__01_table_wrap_fix.a1.err b/.agents/reports/86_history_page_width/86_history_page_width__01_table_wrap_fix.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/86_history_page_width/86_history_page_width__01_table_wrap_fix.a1.md b/.agents/reports/86_history_page_width/86_history_page_width__01_table_wrap_fix.a1.md new file mode 100644 index 0000000..4ddf145 --- /dev/null +++ b/.agents/reports/86_history_page_width/86_history_page_width__01_table_wrap_fix.a1.md @@ -0,0 +1,13 @@ +All gates green. Final state verified: + +**Report — Phase 86, Task 01 (table-wrap containing-block fix)** + +- Added `position: relative;` (first declaration) to the shared `.table-wrap` rule in `frontend/assets/styles.css`, with a root-cause comment (positioned `.visually-hidden` spans → initial containing block → document scroll leak; TODO.md L4) — the one-rule A3 fix +- New unit pin `tests/unit/test_table_wrap_stacking.py` (house CSS-source pattern): `position: relative` present + first, `overflow-x: auto` preserved, and the fix scoped to `.table-wrap` only (`.md-table-wrap` / `#git-sources-table-wrap` untouched); docstring carries the measured bug basis (626/618/375 at 375px, 2026-09-07) +- Live browser verification (375px, throwaway script, deleted after): History scrollWidth 626→375, Tokens 618→375, Sources 375 (unchanged); in-card scroll intact (wraps 688/640 > 344 client); a11y spans still in DOM +- `uv run pytest tests/unit/test_table_wrap_stacking.py -v` → 3 passed +- `uv run pytest --cov=app --cov-report=term-missing` → 1720 passed, TOTAL **99%** (>90%) +- `uv run ruff check . && uv run pyright` → All checks passed / 0 errors +- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → 3 passed (sanity, app functional) +- `git diff --stat -- frontend/` → only `frontend/assets/styles.css` (+12: comment + 1 property); no markup/JS changes; no commit made (harness commits) +- Next pending task: `02_e2e_story_suite.md` (the `tests/e2e/test_history_page_width.py` story suite) diff --git a/.agents/reports/86_history_page_width/86_history_page_width__01_table_wrap_fix.a1.validate b/.agents/reports/86_history_page_width/86_history_page_width__01_table_wrap_fix.a1.validate new file mode 100644 index 0000000..bb6cf10 --- /dev/null +++ b/.agents/reports/86_history_page_width/86_history_page_width__01_table_wrap_fix.a1.validate @@ -0,0 +1,85 @@ +........................................................................ [ 4%] +........................................................................ [ 8%] +........................................................................ [ 12%] +........................................................................ [ 16%] +........................................................................ [ 20%] +........................................................................ [ 25%] +........................................................................ [ 29%] +........................................................................ [ 33%] +........................................................................ [ 37%] +........................................................................ [ 41%] +........................................................................ [ 46%] +........................................................................ [ 50%] +........................................................................ [ 54%] +........................................................................ [ 58%] +........................................................................ [ 62%] +........................................................................ [ 66%] +........................................................................ [ 71%] +........................................................................ [ 75%] +........................................................................ [ 79%] +........................................................................ [ 83%] +........................................................................ [ 87%] +........................................................................ [ 92%] +........................................................................ [ 96%] +................................................................ [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 178 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 7 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 50 0 100% +app/api/git_sources.py 212 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 98 0 100% +app/api/tokens.py 28 0 100% +app/config.py 141 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 108 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 19 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 64 0 100% +app/models.py 94 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 222 0 100% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 180 3 98% +app/rag/llm.py 217 0 100% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 232 0 100% +-------------------------------------------------- +TOTAL 3237 12 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +validation OK diff --git a/.agents/reports/86_history_page_width/86_history_page_width__02_e2e_story_suite.a1.err b/.agents/reports/86_history_page_width/86_history_page_width__02_e2e_story_suite.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/86_history_page_width/86_history_page_width__02_e2e_story_suite.a1.md b/.agents/reports/86_history_page_width/86_history_page_width__02_e2e_story_suite.a1.md new file mode 100644 index 0000000..2346821 --- /dev/null +++ b/.agents/reports/86_history_page_width/86_history_page_width__02_e2e_story_suite.a1.md @@ -0,0 +1,17 @@ +Task 02 complete. Report: + +**Implemented** +- `tests/e2e/test_history_page_width.py` (new) — 5 isolated Playwright tests: History viewport-width at 375px (THE 626→≤375 pin + in-card scroll + a11y-span presence), Tokens (618 fold-in), SPA switch history→tokens (real `#nav-toggle` click + phase-76 window-sentinel same-document proof), RAG regression (viewport-width + full-width docs table), desktop 1280×800 no-overflow / container-width tables. +- RAG + desktop tests seed the KB house-way (truncate + 13 fixture docs) — the RAG view hides its table on an empty KB; History/Tokens pins hold empty, as the task assumed. + +**Results** +- `uv run pytest tests/e2e/test_history_page_width.py -v --no-cov` → **5 passed** (in isolation, DB up) +- Red→green verified: with task-01 CSS reverted, `test_history_page_is_viewport_width` fails at the scrollWidth pin (**626px at a 375px viewport** — the exact reported defect); restored → green (recorded in the session above) +- `uv run pytest --cov=app --cov-report=term-missing` → **1720 passed, 99% coverage** (>90%) +- `uv run ruff check .` → clean; `uv run pyright` → **0 errors** + +**Decisions** +- Reused the phase-76 window-sentinel pattern verbatim for the same-document proof (per the task's ASSUMPTION) instead of a navigation-timeline count. +- Card-width pin computes container content width minus the card's own 1px borders (clientWidth excludes borders) — a ±1px tolerance caught a first-draft miscalculation. + +**Next pending task:** `03_verify_and_commit.md` (full gate + atomic commit). diff --git a/.agents/reports/86_history_page_width/86_history_page_width__02_e2e_story_suite.a1.validate b/.agents/reports/86_history_page_width/86_history_page_width__02_e2e_story_suite.a1.validate new file mode 100644 index 0000000..bb6cf10 --- /dev/null +++ b/.agents/reports/86_history_page_width/86_history_page_width__02_e2e_story_suite.a1.validate @@ -0,0 +1,85 @@ +........................................................................ [ 4%] +........................................................................ [ 8%] +........................................................................ [ 12%] +........................................................................ [ 16%] +........................................................................ [ 20%] +........................................................................ [ 25%] +........................................................................ [ 29%] +........................................................................ [ 33%] +........................................................................ [ 37%] +........................................................................ [ 41%] +........................................................................ [ 46%] +........................................................................ [ 50%] +........................................................................ [ 54%] +........................................................................ [ 58%] +........................................................................ [ 62%] +........................................................................ [ 66%] +........................................................................ [ 71%] +........................................................................ [ 75%] +........................................................................ [ 79%] +........................................................................ [ 83%] +........................................................................ [ 87%] +........................................................................ [ 92%] +........................................................................ [ 96%] +................................................................ [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 178 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 7 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 50 0 100% +app/api/git_sources.py 212 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 98 0 100% +app/api/tokens.py 28 0 100% +app/config.py 141 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 108 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 19 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 64 0 100% +app/models.py 94 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 222 0 100% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 180 3 98% +app/rag/llm.py 217 0 100% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 232 0 100% +-------------------------------------------------- +TOTAL 3237 12 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +validation OK diff --git a/.agents/reports/86_history_page_width/86_history_page_width__03_verify_and_commit.a1.err b/.agents/reports/86_history_page_width/86_history_page_width__03_verify_and_commit.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/86_history_page_width/86_history_page_width__03_verify_and_commit.a1.md b/.agents/reports/86_history_page_width/86_history_page_width__03_verify_and_commit.a1.md new file mode 100644 index 0000000..5882f15 --- /dev/null +++ b/.agents/reports/86_history_page_width/86_history_page_width__03_verify_and_commit.a1.md @@ -0,0 +1,18 @@ +Task 03 (full gate) complete — all green. Per my harness rules I did **not** commit or move the phase dir (the task's steps 3–4 are the harness's job on success). + +**Gate results (exact commands):** +- `uv run pytest` → 1720 passed +- `uv run pytest --cov=app --cov-report=term-missing` → 1720 passed, **99%** coverage (>90% floor held, no `app/` change) +- `uv run pytest tests/e2e/test_history_page_width.py -v --no-cov` → 5 passed (in isolation) +- `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` → 5 passed; `test_api_tokens.py` → 9 passed; `test_smoke.py` → 3 passed +- `uv run ruff check . && uv run pyright` → All checks passed / 0 errors + +**Manual live check (throwaway Playwright script, E2E-style app boot; output kept in this session log):** 375px — `/history.html` scrollWidth **375** (626 pre-fix), `/tokens.html` 375 (618 pre-fix), `/sources.html` 375; dragging right at the edge moved nothing (scrollLeft stayed 0); swiping the card scrolled its columns internally (296px) while the document stayed put. 1280px — all three cards at 1110px container width, 0px internal scroll. **ALL CHECKS PASSED.** + +**Scope:** working tree holds exactly `frontend/assets/styles.css` (+12: `position: relative` + root-cause comment), the two new test files, and phase files (01/02 already in `complete/`; `00_phase.md` + `03` still in `todo/` for the harness). No markup/JS/`app/` changes. + +**Deviations/flags:** +- No `git commit` / no `mv` of the phase dir — harness owns both (overrides task steps 3–4). +- `.agents/remediation_plan.md` (untracked, 2026-09-07 security-audit artifact) is **not** part of this phase — exclude it from the phase-86 commit. + +**Next pending:** `87_big_read_progress` (separate phase). diff --git a/.agents/reports/86_history_page_width/86_history_page_width__03_verify_and_commit.a1.validate b/.agents/reports/86_history_page_width/86_history_page_width__03_verify_and_commit.a1.validate new file mode 100644 index 0000000..bb6cf10 --- /dev/null +++ b/.agents/reports/86_history_page_width/86_history_page_width__03_verify_and_commit.a1.validate @@ -0,0 +1,85 @@ +........................................................................ [ 4%] +........................................................................ [ 8%] +........................................................................ [ 12%] +........................................................................ [ 16%] +........................................................................ [ 20%] +........................................................................ [ 25%] +........................................................................ [ 29%] +........................................................................ [ 33%] +........................................................................ [ 37%] +........................................................................ [ 41%] +........................................................................ [ 46%] +........................................................................ [ 50%] +........................................................................ [ 54%] +........................................................................ [ 58%] +........................................................................ [ 62%] +........................................................................ [ 66%] +........................................................................ [ 71%] +........................................................................ [ 75%] +........................................................................ [ 79%] +........................................................................ [ 83%] +........................................................................ [ 87%] +........................................................................ [ 92%] +........................................................................ [ 96%] +................................................................ [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 178 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 7 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 50 0 100% +app/api/git_sources.py 212 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 98 0 100% +app/api/tokens.py 28 0 100% +app/config.py 141 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 108 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 19 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 64 0 100% +app/models.py 94 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 222 0 100% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 180 3 98% +app/rag/llm.py 217 0 100% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 24 0 100% +app/schemas.py 232 0 100% +-------------------------------------------------- +TOTAL 3237 12 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +validation OK diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index d5291d0..ac939a4 100644 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -1741,7 +1741,19 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; } other door: the admin password login (the static ?next=/ is the no-JS fallback). */ +/* Phase 86: the card is the containing block for the + position:absolute .visually-hidden elements it hosts (the table + + the Actions header span in #view-history / + #view-tokens). Without a positioned ancestor their 1px boxes lay + out against the INITIAL containing block — at the 640px table's + right edge — and leak into the DOCUMENT's scrollable overflow + (the page appeared "the width of the table", TODO.md L4; the + Tokens view carried the identical defect). Zero-offset positioning + changes no layout: the spans stay clipped by their own + clip: rect(0 0 0 0) + 1px box, and cards without such spans (the + RAG .docs-table, the git-sources table) render unchanged. */ .table-wrap { + position: relative; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); diff --git a/tests/e2e/test_history_page_width.py b/tests/e2e/test_history_page_width.py new file mode 100644 index 0000000..9ad1452 --- /dev/null +++ b/tests/e2e/test_history_page_width.py @@ -0,0 +1,427 @@ +"""Phase 86 E2E (Playwright): the table pages are viewport-width — the +owner's "the page is the width of the table" bug, pinned. + +Source: ``TODO.md`` L4 (owner bug report, 2026-09-07): "The history page +appears to be the width of the table despite the table being scrollable. +On mobile this results in half the page being blank and awkwardly +scrollable." +Story: n/a (owner bug report — ``TODO.md`` L4 is the story; no +``.agents/user_stories/`` file). + +Run in isolation (DB must be up: ``podman compose up -d db``): + + uv run pytest tests/e2e/test_history_page_width.py -v --no-cov + +The bug (confirmed by live reproduction, 375×812, signed-in admin): +the History and Tokens tables carry ``position: absolute`` +``.visually-hidden`` elements (the ```` + the Actions column +header span). No ancestor in the chain was positioned, so their 1px +boxes were laid out against the INITIAL containing block — the Actions +span at the 640px table's right edge — and leaked into the DOCUMENT's +scrollable overflow, bypassing the card's own ``overflow-x: auto`` +scrolling. Measured ``documentElement.scrollWidth`` at 375px: +``/history.html`` **626**, ``/tokens.html`` **618** (the byte-identical +defect, folded in per owner decision A3), ``/sources.html`` **375** +(clean — the RAG table's headers are visible text). The fix is ONE CSS +property (phase 86, task 01): ``position: relative`` on the shared +``.table-wrap`` card, which becomes the containing block for those +spans. This suite pins the FIXED contract: the document never pans +past the viewport, the 640px tables still scroll INSIDE their cards +(the phase-07 / AGENTS.md-rule-5 full-width contract), the a11y spans +stay in the DOM, and the RAG view + the desktop layout are unchanged. + +Counterfactual (verified red→green in the phase session): with the +task-01 rule reverted (no ``position: relative`` on ``.table-wrap``), +``test_history_page_is_viewport_width`` fails at its +``scrollWidth <= innerWidth`` pin (626 > 375); restored, it is green. + +Test → contract mapping (Playwright Mapping Rule): + +1. ``test_history_page_is_viewport_width`` — THE TODO.md L4 pin: admin + at 375×812, direct ``/history.html`` — + ``documentElement.scrollWidth <= innerWidth`` (626 → ≤375), the + table still scrolls INSIDE ``#history-table-wrap`` + (``scrollWidth > clientWidth`` — the 640px ``min-width``), the card + is the container's content width (≤ the viewport), and the + visually-hidden Actions header + caption survived the fix (the a11y + names are intact). +2. ``test_tokens_page_is_viewport_width`` — the folded-in identical + defect (618 pre-fix): the same two pins on direct ``/tokens.html`` + for ``#tokens-table-wrap``. +3. ``test_spa_switch_keeps_page_width`` — the invariant survives the + router, not just direct loads: at 375px on ``/history.html`` the + mobile menu (a REAL ``#nav-toggle`` click — the phase-85 contract) + opens, the Tokens link switches the view IN THE SAME DOCUMENT + (the phase-76 window-sentinel proof — a real navigation would wipe + ``window`` globals), and the Tokens page is still viewport-width + with its table still scrolling in-card. +4. ``test_rag_view_regression`` — ``/sources.html`` at 375px: no + overflow (it was already clean at 375 pre-fix — pinned so the + shared-rule change cannot regress it) AND the RAG table is still + full-width inside its card (in-card scroll — the 640px + ``min-width`` still engages). +5. ``test_desktop_unchanged`` — 1280×800 regression on ALL THREE + views: no document overflow at any width, and the tables render at + container width (the cards need no internal scroll — + ``scrollWidth <= clientWidth + 1``, the card is far wider than the + mobile 346px). + +House conventions: a fresh 375×812 page per mobile test via the +session ``browser`` fixture (the conftest ``page`` is 1280×800), +``e2e.auth_helpers.login`` for the real form login, ``_wait_settled_admin`` +copied from ``test_mobile_hamburger_nav.py`` (suites stay +self-contained — helpers copied, only ``auth_helpers`` imported). +Data: the History/Tokens contract holds with EMPTY tables (static +thead + the 640px ``min-width`` — no rows needed, and the suite never +depends on saved chats or issued tokens). The RAG view, by contrast, +HIDES its table and shows the "Nothing indexed yet" empty state on an +empty KB, so tests 4 and 5 seed the KB the house way (truncate + +import the 13 fixture docs, deterministic mock embeddings) and pin +nothing about the rows themselves — only that the table is up and +full-width. +""" +from __future__ import annotations + +import asyncio +from pathlib import Path +from threading import Thread +from typing import Any + +from playwright.sync_api import Browser, JSHandle, Page, ViewportSize, expect +from sqlalchemy import text + +from app.config import Settings +from app.db import SessionLocal +from app.rag.importer import ImportSummary, import_sources +from app.rag.llm import LLMClient +from e2e.auth_helpers import login + +REPO = Path(__file__).resolve().parents[2] +FIXTURES = REPO / "tests" / "fixtures" / "docs" + +MOBILE: ViewportSize = {"width": 375, "height": 812} # the bug-report phone viewport +DESKTOP: ViewportSize = {"width": 1280, "height": 800} # the conftest page size + + +async def _import_fixtures(mock_port: int) -> ImportSummary: + """The house KB seed (copied from ``test_api_tokens.py``): import + the fixture docs against the mock LLM's embeddings.""" + kwargs: dict[str, Any] = {"_env_file": None, "llm_base_url": f"http://127.0.0.1:{mock_port}/v1"} + settings = Settings(**kwargs) # pyright: ignore[reportCallIssue] + return await import_sources([FIXTURES], LLMClient(settings)) + + +def _run_in_thread(coro: Any) -> Any: + """Run a coroutine on a worker thread. + + Playwright's sync API keeps an asyncio loop running on the test + thread, so ``asyncio.run`` cannot be called directly from a test + body. + """ + box: dict[str, Any] = {} + + def runner() -> None: + try: + box["value"] = asyncio.run(coro) + except BaseException as e: # noqa: BLE001 — re-raised on the test thread + box["error"] = e + + t = Thread(target=runner) + t.start() + t.join() + if "error" in box: + raise box["error"] + return box["value"] + + +def _seed_kb(mock_port: int) -> ImportSummary: + """House reset (truncate the KB + the prompt-shaping tables so the + mock answers stay deterministic) + the fixture re-import — the + RAG view hides its table on an empty KB, so the two tests that + pin it need at least the fixture docs present. ``saved_chats`` and + ``api_tokens`` are deliberately NOT touched (the house pattern).""" + with SessionLocal() as db: + db.execute(text("TRUNCATE chunks, documents, query_log, steering_notes")) + db.commit() + return _run_in_thread(_import_fixtures(mock_port)) + + +def _mobile_page(browser: Browser) -> Page: + """A fresh 375×812 page (the conftest ``page`` is 1280×800).""" + return browser.new_page(viewport=MOBILE) + + +def _wait_settled_admin(page: Page) -> None: + """Wait until whoami has resolved for the admin: the whoami reveal + has un-hidden the admin-only nav links — the viewport-independent + settled signal (copied from ``test_mobile_hamburger_nav.py``; the + sign-out control is viewport-dependent and not a cross-viewport + probe).""" + page.wait_for_function( + "() => !document.querySelector('#nav-sources').hasAttribute('hidden')", + timeout=10_000, + ) + + +def _open_menu(page: Page) -> None: + """Open the mobile menu with a REAL click on the toggle (the + phase-85 contract: the gate sits below the header, so a real tap + reaches the toggle; for a signed-in admin the gate is hidden + anyway).""" + page.click("#nav-toggle") + expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true") + + +def _wrap_handle(page: Page, view: str) -> JSHandle: + """The table card (``.table-wrap``) of ``view``: the id'd cards for + history/tokens, the RAG card found through its ``.docs-table`` + (that card is the only one without an id).""" + if view == "sources": + return page.evaluate_handle( + "() => document.querySelector('.docs-table').closest('.table-wrap')" + ) + return page.evaluate_handle(f"() => document.querySelector('#{view}-table-wrap')") + + +def _assert_viewport_width(page: Page, label: str) -> None: + """THE pin (TODO.md L4): the document itself never pans past the + viewport — pre-fix, /history.html measured 626 and /tokens.html + 618 at this 375px viewport.""" + report = page.evaluate( + "() => ({ doc: document.documentElement.scrollWidth, inner: window.innerWidth })" + ) + assert report["doc"] <= report["inner"], ( + f"{label}: the document panned to {report['doc']}px at a " + f"{report['inner']}px viewport — the TODO.md L4 defect " + '(the page is "the width of the table"); the .table-wrap card ' + "must be the containing block for the positioned " + ".visually-hidden spans (position: relative, phase 86)" + ) + + +def _assert_in_card_scroll(page: Page, wrap: JSHandle, label: str) -> None: + """The phase-07 / AGENTS.md-rule-5 contract the fix preserves: the + 640px table scrolls INSIDE the card (``overflow-x: auto``), the + card is the container's content width (≤ the viewport), and the + table's ``min-width: 640px`` is what the in-card scroll is over.""" + report = wrap.evaluate( + """el => { + const c = el.closest('.container'); + const cs = getComputedStyle(c); + const ws = getComputedStyle(el); + return { + scroll: el.scrollWidth, + client: el.clientWidth, + // The container's content width minus the card's own + // borders (clientWidth excludes them) — the card fills + // the row edge to edge. + content: c.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight) + - parseFloat(ws.borderLeftWidth) - parseFloat(ws.borderRightWidth), + inner: window.innerWidth, + table: el.querySelector('table').scrollWidth, + }; + }""" + ) + assert report["table"] >= 640, ( + f"{label}: the table must keep its 640px min-width, got {report['table']}px" + ) + assert report["scroll"] > report["client"], ( + f"{label}: the 640px table must still scroll INSIDE the card " + f"(card {report['client']}px < table {report['table']}px)" + ) + assert report["client"] <= report["inner"], ( + f"{label}: the card must not exceed the viewport " + f"({report['client']}px > {report['inner']}px)" + ) + assert abs(report["client"] - report["content"]) <= 1, ( + f"{label}: the card must fill the container's content width " + f"(card {report['client']}px vs container {report['content']}px)" + ) + + +def _assert_hidden_a11y_spans(page: Page, wrap_sel: str) -> None: + """The a11y names survived the fix: the visually-hidden Actions + column header and the table caption are still in the DOM (the fix + repositioned their CONTAINING BLOCK — it did not remove them).""" + expect(page.locator(f"{wrap_sel} th .visually-hidden")).to_have_count(1) + expect(page.locator(f"{wrap_sel} th .visually-hidden")).to_have_text("Actions") + expect(page.locator(f"{wrap_sel} caption.visually-hidden")).to_have_count(1) + + +# --------------------------------------------------------------------------- +# 1. THE TODO.md L4 pin: /history.html is viewport-width at 375px +# --------------------------------------------------------------------------- + + +def test_history_page_is_viewport_width( + browser: Browser, app_url: str, db_ready: None +) -> None: + """THE TODO.md L4 pin: admin at 375×812, direct ``/history.html`` — + the document is viewport-width (pre-fix ``scrollWidth`` 626 — the + page panned ~250px into a blank region, "half the page being + blank"), the 640px table still scrolls INSIDE + ``#history-table-wrap`` (the phase-07 / AGENTS.md-rule-5 + full-width contract), and the visually-hidden Actions header + + caption are still in the DOM (the a11y names survived the fix).""" + page = _mobile_page(browser) + try: + login(page, app_url, next="/history.html") + _wait_settled_admin(page) + expect(page.locator("#history-table-wrap")).to_be_visible(timeout=15_000) + + _assert_viewport_width(page, "/history.html (direct)") + _assert_in_card_scroll(page, _wrap_handle(page, "history"), "/history.html") + _assert_hidden_a11y_spans(page, "#history-table-wrap") + finally: + page.close() + + +# --------------------------------------------------------------------------- +# 2. The folded-in identical defect: /tokens.html (618 pre-fix) +# --------------------------------------------------------------------------- + + +def test_tokens_page_is_viewport_width( + browser: Browser, app_url: str, db_ready: None +) -> None: + """The owner-confirmed A3 fold-in: the Tokens view carries the + byte-identical defect (measured ``scrollWidth`` 618 pre-fix — the + same visually-hidden Actions header + caption, no positioned + ancestor) and is fixed by the SAME one-rule ``.table-wrap`` fix. + Same two pins as test 1, on direct ``/tokens.html`` for + ``#tokens-table-wrap``.""" + page = _mobile_page(browser) + try: + login(page, app_url, next="/tokens.html") + _wait_settled_admin(page) + expect(page.locator("#tokens-table-wrap")).to_be_visible(timeout=15_000) + + _assert_viewport_width(page, "/tokens.html (direct)") + _assert_in_card_scroll(page, _wrap_handle(page, "tokens"), "/tokens.html") + _assert_hidden_a11y_spans(page, "#tokens-table-wrap") + finally: + page.close() + + +# --------------------------------------------------------------------------- +# 3. The invariant survives the router (not just direct loads) +# --------------------------------------------------------------------------- + + +def test_spa_switch_keeps_page_width( + browser: Browser, app_url: str, db_ready: None +) -> None: + """At 375px on ``/history.html``: open the mobile menu (a REAL + ``#nav-toggle`` click — the phase-85 contract; at this width the + nav links live in the dropdown), click the Tokens link — a + CLIENT-SIDE view switch in the phase-76 shell, so the same-document + proof is the phase-76 canonical one: the window sentinel set + before the click is still readable after it (a real document load + would wipe ``window`` globals; the navigation-timeline count is + deliberately NOT used — a real load resets it, so it cannot + distinguish pushState from a reload). On arrival the Tokens page + keeps BOTH pins: viewport-width AND the table still scrolling + inside its card.""" + page = _mobile_page(browser) + try: + login(page, app_url, next="/history.html") + _wait_settled_admin(page) + expect(page.locator("#history-table-wrap")).to_be_visible(timeout=15_000) + _assert_viewport_width(page, "/history.html (pre-switch)") + + # THE SWITCH (mobile menu → Tokens link), same document: + page.evaluate("() => { window.__phase86_width = 'phase86'; }") + _open_menu(page) + page.click("#nav-tokens") + expect(page).to_have_url(app_url + "/tokens.html", timeout=15_000) + assert page.evaluate("() => window.__phase86_width") == "phase86", ( + "a real document load would have wiped the window sentinel — " + "the view switch must be same-document (the phase-76 router)" + ) + + expect(page.locator("#tokens-table-wrap")).to_be_visible(timeout=15_000) + _assert_viewport_width(page, "/tokens.html (SPA switch)") + _assert_in_card_scroll(page, _wrap_handle(page, "tokens"), "/tokens.html (SPA)") + finally: + page.close() + + +# --------------------------------------------------------------------------- +# 4. Regression: the RAG view (already clean) stays clean + full-width +# --------------------------------------------------------------------------- + + +def test_rag_view_regression( + browser: Browser, app_url: str, mock_llm: int, db_ready: None +) -> None: + """``/sources.html`` at 375px (admin): the RAG view measured clean + pre-fix (``scrollWidth`` 375 — the ``.docs-table`` headers are + visible text, no positioned hidden spans at the table's right + edge). Pinned so the shared ``.table-wrap`` rule change cannot + regress it — AND its table is still full-width inside the card: + the 640px ``min-width`` still engages, so the card keeps its + in-card scroll (the shared rule added a containing block, not a + width).""" + summary = _seed_kb(mock_llm) + assert summary is not None and summary.added == 13 # A9 formats (phase 47 added quadlet+j2) + page = _mobile_page(browser) + try: + login(page, app_url, next="/sources.html") + _wait_settled_admin(page) + expect(page.locator(".docs-table")).to_be_visible(timeout=15_000) + + _assert_viewport_width(page, "/sources.html (direct)") + _assert_in_card_scroll(page, _wrap_handle(page, "sources"), "/sources.html") + finally: + page.close() + + +# --------------------------------------------------------------------------- +# 5. Desktop regression: no overflow anywhere, tables at container width +# --------------------------------------------------------------------------- + + +def test_desktop_unchanged( + browser: Browser, app_url: str, mock_llm: int, db_ready: None +) -> None: + """1280×800 regression on ALL THREE table views: no document + overflow at any width, and each table renders at container width — + the cards need no internal scroll + (``wrap.scrollWidth <= wrap.clientWidth + 1``) and are far wider + than the mobile 346px card. The zero-offset + ``position: relative`` changed no layout, so desktop is + byte-identical in behavior. The KB is seeded the house way for + the RAG view's table (it hides itself on an empty KB).""" + summary = _seed_kb(mock_llm) + assert summary is not None and summary.added == 13 + page = browser.new_page(viewport=DESKTOP) + try: + login(page, app_url, next="/history.html") + _wait_settled_admin(page) + + views = ( + ("history", "/history.html"), + ("tokens", "/tokens.html"), + ("sources", "/sources.html"), + ) + for i, (view, path) in enumerate(views): + if i: # the first view is the login landing + page.goto(app_url + path) + _wait_settled_admin(page) + marker = ".docs-table" if view == "sources" else f"#{view}-table-wrap" + expect(page.locator(marker)).to_be_visible(timeout=15_000) + + _assert_viewport_width(page, f"{path} (desktop)") + report = _wrap_handle(page, view).evaluate( + "el => ({ scroll: el.scrollWidth, client: el.clientWidth })" + ) + assert report["client"] > 346, ( + f"{path} (desktop): the table card must render at container " + f"width, got {report['client']}px (the mobile card is 346px)" + ) + assert report["scroll"] <= report["client"] + 1, ( + f"{path} (desktop): no in-card scroll is needed at this width " + f"(table {report['scroll']}px in a {report['client']}px card)" + ) + finally: + page.close() diff --git a/tests/unit/test_table_wrap_stacking.py b/tests/unit/test_table_wrap_stacking.py new file mode 100644 index 0000000..7d38e3a --- /dev/null +++ b/tests/unit/test_table_wrap_stacking.py @@ -0,0 +1,115 @@ +"""Unit: the .table-wrap containing-block contract (phase 86, task 01). + +TODO.md L4 (owner bug report, 2026-09-07): "The history page appears to +be the width of the table despite the table being scrollable. On mobile +this results in half the page being blank and awkwardly scrollable." + +Bug basis (confirmed by live reproduction, 2026-09-07 — headless +Chromium, 375×812, signed-in admin): the History and Tokens tables +carry ``class="visually-hidden"`` on their ```` and their +Actions column header — a ``position: absolute`` 1px clipped box. No +ancestor in the chain (th → table → .table-wrap → the view shell → +#main → body) was positioned, so the span's containing block was the +INITIAL containing block: its 1px box sat at the 640px table's right +edge and contributed to the DOCUMENT's scrollable overflow, bypassing +the card's own scroll clipping. Measured ``documentElement.scrollWidth`` +at a 375px viewport: /history.html **626** (the page panned ~250px +into a blank region — the owner's "half the page being blank"), +/tokens.html **618** (the byte-identical defect, folded in per the +owner-confirmed A3 scope), /sources.html **375** (the RAG view is +clean — its table headers are visible text, no positioned hidden +spans). Hiding the span, OR making the card ``position: relative``, +both bring the scrollWidth back to 375. + +The fix is ONE CSS property (owner decision A3): ``position: relative`` +on the shared ``.table-wrap`` card — it becomes the containing block +for the ``.visually-hidden`` elements it hosts, so History AND Tokens +are fixed by the same rule with no markup change (the accessible +column name / caption stay) and no JS change. Zero-offset positioning +changes no layout: the spans stay clipped by their own +``clip: rect(0 0 0 0)`` + 1px box, and cards without such spans (the +RAG ``.docs-table``, the git-sources table) render unchanged. + +This module pins the source-level contract (the house pattern of +``tests/unit/test_hamburger_nav.py``: read ``styles.css`` as text, no +browser) so the regression that omitted the containing block cannot +return. +""" +from __future__ import annotations + +import re +from pathlib import Path + +FRONTEND = Path(__file__).resolve().parents[2] / "frontend" +STYLES_CSS = FRONTEND / "assets" / "styles.css" + + +def _text(path: Path) -> str: + assert path.is_file(), f"missing frontend file: {path}" + return path.read_text(encoding="utf-8") + + +def _css() -> str: + """styles.css with comments stripped (a comment may legally carry + braces — the brace-matching helper below must never see them).""" + return re.sub(r"/\*.*?\*/", "", _text(STYLES_CSS), flags=re.S) + + +def _rule_block(css: str, selector: str) -> str: + """The first rule body for the EXACT selector ``selector`` (e.g. + ``.table-wrap``). The lookbehind before the selector rejects + decorated variants: ``.table-wrap`` must NOT match ``.md-table-wrap`` (the + chat-answer markdown tables) nor ``.history-table-wrap`` / + ``.tokens-table-wrap`` (the cards' extra classes), and an id rule + like ``#git-sources-table-wrap`` is never a match for the class + selector.""" + m = re.search(r"(? None: + """THE regression pin: the shared .table-wrap card carries + position:relative — as its FIRST declaration — making it the + containing block for the position:absolute .visually-hidden + elements it hosts (the table caption + the Actions header span in + #view-history / #view-tokens). Without a positioned ancestor their + 1px boxes are laid out against the initial containing block at the + 640px table's right edge and leak into the document's scrollable + overflow (TODO.md L4 — the page appeared "the width of the + table": 626px on History, 618px on Tokens, at 375px).""" + block = _rule_block(_css(), ".table-wrap") + assert re.search(r"position:\s*relative", block), ( + "the .table-wrap card must be position:relative (the " + "containing block for the .visually-hidden spans)" + ) + assert block.lstrip().startswith("position: relative"), ( + "position:relative must be the FIRST declaration of .table-wrap " + "(the containing-block contract precedes the card look)" + ) + + +def test_table_wrap_keeps_the_horizontal_scroll_contract() -> None: + """The fix preserves the phase-07 scroll contract: the card keeps + overflow-x:auto — at any viewport narrower than the tables' 640px + min-width the table scrolls INSIDE the card (AGENTS.md rule 5: + full-width tables stay) while the document itself no longer pans.""" + block = _rule_block(_css(), ".table-wrap") + assert "overflow-x: auto" in block, ( + "the .table-wrap card must keep overflow-x:auto (the in-card " + "scroll the fix protects)" + ) + + +def test_the_fix_touched_only_the_shared_card() -> None: + """The one-rule scope (A3): the fix lives on .table-wrap ONLY — + .md-table-wrap (chat answer tables — different class, different + context) and the #git-sources-table-wrap id card keep their + pre-phase-86 rules (no position declaration acquired).""" + css = _css() + for sel in (".md-table-wrap", "#git-sources-table-wrap"): + block = _rule_block(css, sel) + assert not re.search(r"position\s*:", block), ( + f"{sel} must not acquire a position declaration (the fix is " + "the ONE .table-wrap rule)" + )