diff --git a/.agents/phases/todo/138_markdown_blockquotes/00_phase.md b/.agents/phases/complete/138_markdown_blockquotes/00_phase.md similarity index 100% rename from .agents/phases/todo/138_markdown_blockquotes/00_phase.md rename to .agents/phases/complete/138_markdown_blockquotes/00_phase.md diff --git a/.agents/phases/todo/138_markdown_blockquotes/01_blockquote_pass.md b/.agents/phases/complete/138_markdown_blockquotes/01_blockquote_pass.md similarity index 100% rename from .agents/phases/todo/138_markdown_blockquotes/01_blockquote_pass.md rename to .agents/phases/complete/138_markdown_blockquotes/01_blockquote_pass.md diff --git a/.agents/phases/todo/138_markdown_blockquotes/02_blockquote_css.md b/.agents/phases/complete/138_markdown_blockquotes/02_blockquote_css.md similarity index 100% rename from .agents/phases/todo/138_markdown_blockquotes/02_blockquote_css.md rename to .agents/phases/complete/138_markdown_blockquotes/02_blockquote_css.md diff --git a/.agents/phases/todo/138_markdown_blockquotes/03_unit_and_e2e.md b/.agents/phases/complete/138_markdown_blockquotes/03_unit_and_e2e.md similarity index 100% rename from .agents/phases/todo/138_markdown_blockquotes/03_unit_and_e2e.md rename to .agents/phases/complete/138_markdown_blockquotes/03_unit_and_e2e.md diff --git a/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__00_phase.a1.err b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__00_phase.a1.md b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__00_phase.a1.md new file mode 100644 index 0000000..4f49248 --- /dev/null +++ b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__00_phase.a1.md @@ -0,0 +1,9 @@ +**Phase 138 final verification pass — all green, no defects found.** + +- Verified: blockquote pass in `frontend/assets/markdown.js` (step 2b, between escape+inline and paragraphs), guard extended to `|blockquote`, docstring updated; `.md-quote` CSS (left border + padding + `--ink-soft`, no fill, palette vars only, contrast comments, user-bubble override); unit pins + node behavior; dedicated E2E (chat / viewer / restore); mock `QUOTE_TRIGGER`/`QUOTE_ANSWER`; fixture `tests/fixtures/quote_docs/`. Thinking block confirmed on the same shared renderer (app.js L2025/L3095). +- `uv run pytest tests/e2e/test_markdown_blockquotes.py -v --no-cov` → 3 passed (isolation) +- `uv run pytest --cov=app --cov-report=term-missing` → 3268 passed, 9 skipped; TOTAL **99%** (>90%) +- `uv run ruff check . && uv run pyright` → All checks passed; 0 errors +- Criteria: quotes render in chat/viewer/restore ✅ (E2E) + thinking ✅ (shared renderer + unscoped AA CSS); `>>` collapses, fence/table `>` untouched ✅ (unit+e2e); non-quote byte-identical ✅ (exact fixture; phase-44/renderer suites pass **unedited**, confirmed by empty `git diff --stat`) +- No deviations; commit + phase-dir move left to the harness per pipeline rules (working tree clean of stray changes — only phase-138 diffs present) +- Next pending phase: **139_read_range_tool_lines** diff --git a/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__00_phase.a1.validate b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__00_phase.a1.validate new file mode 100644 index 0000000..5ebbbc4 --- /dev/null +++ b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__00_phase.a1.validate @@ -0,0 +1,117 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 6%] +........................................................................ [ 8%] +........................................................................ [ 10%] +........................................................................ [ 13%] +........................................................................ [ 15%] +........................................................................ [ 17%] +........................................................................ [ 19%] +........................................................................ [ 21%] +........................................................................ [ 24%] +........................................................................ [ 26%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 32%] +........................................................................ [ 35%] +........................................................................ [ 37%] +........................................................................ [ 39%] +........................................................................ [ 41%] +........................................................................ [ 43%] +........................................................................ [ 46%] +........................................................................ [ 48%] +........................................................................ [ 50%] +........................................................................ [ 52%] +...........................................................s.....sss...s [ 54%] +s....................................................................... [ 57%] +........................................................................ [ 59%] +........................................................................ [ 61%] +........................................................................ [ 63%] +........................................................................ [ 65%] +........................................................................ [ 68%] +........................................................................ [ 70%] +........................................................................ [ 72%] +........................................................................ [ 74%] +........................................................................ [ 76%] +........................................................................ [ 79%] +........................................................................ [ 81%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 87%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 94%] +....................................................................sss. [ 96%] +........................................................................ [ 98%] +..................................... [100%] +=============================== warnings summary =============================== +.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain-of-reese/.venv/lib64/python3.14/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.14.7-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 268 1 99% +app/api/chat_images.py 50 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 179 1 99% +app/api/git_sources.py 259 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 183 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 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 44 0 100% +app/db.py 22 0 100% +app/main.py 68 0 100% +app/models.py 133 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 412 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 220 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 134 0 100% +app/rag/git_sources.py 56 0 100% +app/rag/importer.py 671 9 99% +app/rag/llm.py 246 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 119 0 100% +app/rag/prompts.py 154 0 100% +app/rag/retriever.py 248 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 49 0 100% +app/schemas.py 422 0 100% +-------------------------------------------------- +TOTAL 5430 22 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/138_markdown_blockquotes/138_markdown_blockquotes__01_blockquote_pass.a1.err b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__01_blockquote_pass.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__01_blockquote_pass.a1.md b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__01_blockquote_pass.a1.md new file mode 100644 index 0000000..c7ff6bd --- /dev/null +++ b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__01_blockquote_pass.a1.md @@ -0,0 +1,14 @@ +Task complete. All completion criteria verified. + +**Report — phase 138, task 01 (blockquote pass)** + +- `frontend/assets/markdown.js`: new step-2b pass between `inline(escapeHtml(text))` and the paragraph pass — maximal run of consecutive `^\s*(?:>)+ ?` lines → one `
`, marker run + one optional space stripped per member, members joined with `
`, blank line terminates the group +- Step-3 paragraph guard extended to `/^<(h\d|ul|ol|pre|li|blockquote)/`; module docstring + step-2b comment document pass order (fences → tables → escape+inline → blockquotes → paragraphs → restore), the group rule, and the in-quote structure limit +- Node repro: `renderMarkdown("> a\n> b")` → `a` (exact); `> quoted` no longer emits literal `>` +- Byte-identity: 14-case non-quote corpus (paragraph, lists, headings, table, fence, XSS, blanks) renders byte-identical to pre-task baseline; fences/tables keep precedence (verified under node) +- `uv run pytest` — exit 0, no failures (unit + integration, renderer-dependent suites unedited) +- `uv run pytest --cov=app --cov-report=term-missing` — TOTAL **99%** (>90% gate) +- `uv run ruff check .` — All checks passed; `uv run pyright` — 0 errors, 0 warnings +- Notable: the task's regex shorthand `>+ ?` binds `+` to the semicolon and would not collapse nested `>>` (breaks locked A1) — implemented as `(?:>)+` to honor the intent; nested collapse, blank-termination, XSS-inert, and inline-in-quote all verified under node +- No test files added (unit pins live in task 03's `tests/unit/test_markdown_blockquotes.py` per the task); only `markdown.js` modified, left uncommitted +- Next pending task: `02_blockquote_css.md` diff --git a/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__01_blockquote_pass.a1.validate b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__01_blockquote_pass.a1.validate new file mode 100644 index 0000000..556eccd --- /dev/null +++ b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__01_blockquote_pass.a1.validate @@ -0,0 +1,117 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 6%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 13%] +........................................................................ [ 15%] +........................................................................ [ 17%] +........................................................................ [ 19%] +........................................................................ [ 22%] +........................................................................ [ 24%] +........................................................................ [ 26%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 37%] +........................................................................ [ 39%] +........................................................................ [ 41%] +........................................................................ [ 44%] +........................................................................ [ 46%] +........................................................................ [ 48%] +........................................................................ [ 50%] +........................................................................ [ 52%] +......................................................s.....sss...ss.... [ 55%] +........................................................................ [ 57%] +........................................................................ [ 59%] +........................................................................ [ 61%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 68%] +........................................................................ [ 70%] +........................................................................ [ 72%] +........................................................................ [ 74%] +........................................................................ [ 77%] +........................................................................ [ 79%] +........................................................................ [ 81%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 94%] +...............................................................sss...... [ 96%] +........................................................................ [ 99%] +................................ [100%] +=============================== warnings summary =============================== +.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain-of-reese/.venv/lib64/python3.14/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.14.7-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 268 1 99% +app/api/chat_images.py 50 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 179 1 99% +app/api/git_sources.py 259 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 183 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 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 44 0 100% +app/db.py 22 0 100% +app/main.py 68 0 100% +app/models.py 133 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 412 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 220 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 134 0 100% +app/rag/git_sources.py 56 0 100% +app/rag/importer.py 671 9 99% +app/rag/llm.py 246 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 119 0 100% +app/rag/prompts.py 154 0 100% +app/rag/retriever.py 248 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 49 0 100% +app/schemas.py 422 0 100% +-------------------------------------------------- +TOTAL 5430 22 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/138_markdown_blockquotes/138_markdown_blockquotes__02_blockquote_css.a1.err b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__02_blockquote_css.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__02_blockquote_css.a1.md b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__02_blockquote_css.a1.md new file mode 100644 index 0000000..a671f5e --- /dev/null +++ b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__02_blockquote_css.a1.md @@ -0,0 +1,22 @@ +All green. Task 02 complete. + +## Report — Task 02: `.md-quote` styling (theme-safe, WCAG AA) + +**Implemented** +- `.md-quote` rule in `frontend/assets/styles.css` at the phase-44 `.md-table` anchor: `border-left: 3px solid var(--brand-ink)` (9.9:1 on `--surface`), `padding: 0.25rem 0.75rem`, `margin: 0.5rem 0`, `color: var(--ink-soft)` with the 8.2:1-on-`--surface` contrast comment; no background fill (A2), palette vars only, no hex outside `:root` (verified programmatically) +- Scoped override `.msg.user .bubble .md-quote { color: var(--bg) }` (5.2:1 on `--brand`) — user questions run through `renderMarkdown` (app.js:2002) on the `--brand` fill where `--ink-soft` is 1.6:1 (AA failure) + +**Verification (live browser, real `styles.css` + `markdown.js`)** +- Computed contrast in all four contexts: brain bubble 8.22:1, user bubble 5.35:1, thinking block 8.22:1, doc viewer 8.22:1 — all ≥4.5:1; screenshot confirms visually distinct quote in every context +- Deflected bubble (`--accent-bg`) also holds: 6.9:1 + +**Test / lint / coverage** +- `uv run pytest --cov=app --cov-report=term-missing` → 3263 passed, 9 skipped; TOTAL **99%** (>90% ✓) +- `uv run ruff check .` → clean; `uv run pyright` → 0 errors +- `uv run pytest tests/e2e/test_markdown_tables.py -v --no-cov` → 6 passed (phase-44 renderer-dependent E2E green unedited) + +**Notable decision** +- User-bubble override added now (not awaiting task-03 E2E): the clash is provable by contrast math and WCAG 2.1 AA is a binding AGENTS.md rule 5 requirement + +**Next pending task** +- `.agents/phases/todo/138_markdown_blockquotes/03_unit_and_e2e.md` diff --git a/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__02_blockquote_css.a1.validate b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__02_blockquote_css.a1.validate new file mode 100644 index 0000000..556eccd --- /dev/null +++ b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__02_blockquote_css.a1.validate @@ -0,0 +1,117 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 6%] +........................................................................ [ 8%] +........................................................................ [ 11%] +........................................................................ [ 13%] +........................................................................ [ 15%] +........................................................................ [ 17%] +........................................................................ [ 19%] +........................................................................ [ 22%] +........................................................................ [ 24%] +........................................................................ [ 26%] +........................................................................ [ 28%] +........................................................................ [ 30%] +........................................................................ [ 33%] +........................................................................ [ 35%] +........................................................................ [ 37%] +........................................................................ [ 39%] +........................................................................ [ 41%] +........................................................................ [ 44%] +........................................................................ [ 46%] +........................................................................ [ 48%] +........................................................................ [ 50%] +........................................................................ [ 52%] +......................................................s.....sss...ss.... [ 55%] +........................................................................ [ 57%] +........................................................................ [ 59%] +........................................................................ [ 61%] +........................................................................ [ 63%] +........................................................................ [ 66%] +........................................................................ [ 68%] +........................................................................ [ 70%] +........................................................................ [ 72%] +........................................................................ [ 74%] +........................................................................ [ 77%] +........................................................................ [ 79%] +........................................................................ [ 81%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 94%] +...............................................................sss...... [ 96%] +........................................................................ [ 99%] +................................ [100%] +=============================== warnings summary =============================== +.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain-of-reese/.venv/lib64/python3.14/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.14.7-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 268 1 99% +app/api/chat_images.py 50 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 179 1 99% +app/api/git_sources.py 259 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 183 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 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 44 0 100% +app/db.py 22 0 100% +app/main.py 68 0 100% +app/models.py 133 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 412 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 220 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 134 0 100% +app/rag/git_sources.py 56 0 100% +app/rag/importer.py 671 9 99% +app/rag/llm.py 246 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 119 0 100% +app/rag/prompts.py 154 0 100% +app/rag/retriever.py 248 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 49 0 100% +app/schemas.py 422 0 100% +-------------------------------------------------- +TOTAL 5430 22 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/138_markdown_blockquotes/138_markdown_blockquotes__03_unit_and_e2e.a1.err b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__03_unit_and_e2e.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__03_unit_and_e2e.a1.md b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__03_unit_and_e2e.a1.md new file mode 100644 index 0000000..b97f063 --- /dev/null +++ b/.agents/reports/138_markdown_blockquotes/138_markdown_blockquotes__03_unit_and_e2e.a1.md @@ -0,0 +1,17 @@ +Everything is green. Final report: + +**Phase 138, task 03 — unit pins, node behavior, dedicated E2E — complete** + +Implemented: +- `tests/unit/test_markdown_blockquotes.py` — source pins (pass order: after `inline(escapeHtml(text))`, before paragraph split; guard includes `blockquote`; exact `
b` marker; group rule test+strip literals; `.md-quote` CSS references `:root` palette vars only + contrast-ratio note) and a node behavior check: single line, 3-line `
` group, `>>`/`>>>` collapse, blank-line termination, inert `\n" + "\n" + "```\n" + "> inside a fence stays fence content\n" + "```\n" + "\n" + "| Cell |\n" + "|---|\n" + "| > x stays a cell |\n" + "\n" + "And the quote ends here — a plain closing line." +) + # --------------------------------------------------------------------------- # Phase 67 (LLM retry, TODO.md L3): deterministic failure injection # --------------------------------------------------------------------------- @@ -2720,6 +2770,16 @@ def compose_answer(body: dict[str, Any]) -> str: # against an on-topic fixture, where the gate is HIGH, and # asserts non-deflection as part of the table test. answer = TABLE_ANSWER + elif QUOTE_TRIGGER in user.lower(): + # Markdown blockquotes (phase 138, TODO.md L3): the story E2E's + # deterministic quote answer — the renderer's blockquote + # contract in one byte-stable stream (single-line quote, + # 3-line group, nested collapse, the ", +] +FENCE_LINE = "> inside a fence stays fence content" +CELL_TEXT = "> x stays a cell" + + +async def _import_fixtures(mock_port: int) -> ImportSummary: + 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 (the established house helper). + """ + 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 _reset_db(mock_port: int) -> ImportSummary: + """Truncate the KB (+ the global prompt-state rows), then re-import + the phase-138 fixture (one document).""" + with SessionLocal() as db: + db.execute( + text("TRUNCATE chunks, documents, query_log, steering_notes, kb_overview") + ) + db.commit() + summary = _run_in_thread(_import_fixtures(mock_port)) + assert summary is not None and summary.added == 1, summary + return summary + + +def _ask_quote_answer(page: Page, app_url: str) -> Any: + """Drive the trigger question and return the brain bubble once the + whole byte-stable quote answer has streamed in (the plain closing + line lands last).""" + login(page, app_url, next="/") # phase 79: chat is require_user-gated + page.fill("#message-input", QUESTION) + page.click("#send-btn") + bubble = page.locator(".msg.brain .bubble").first + bubble.wait_for(state="visible", timeout=30_000) + expect(bubble).to_contain_text(ANSWER_END, timeout=30_000) + # Non-deflection is part of the quote contract (honesty gate + # interplay, like phase 44's table answer). + expect(page.locator(".msg.brain.is-deflected")).to_have_count(0) + return bubble + + +def _assert_blockquotes(bubble: Any, texts: list[str]) -> None: + """The shared assertion: exactly len(texts) blockquote.md-quote + elements with those innerTexts, in order, and no literal quote + marker (the escaped >) at any block's start — the bug from + TODO.md L3.""" + blocks = bubble.locator("blockquote.md-quote") + expect(blocks).to_have_count(len(texts)) + for i, quoted in enumerate(texts): + inner = blocks.nth(i).inner_text() + assert inner == quoted, f"block {i}: {inner!r} != {quoted!r}" + assert not inner.startswith(">"), ( + f"block {i} still starts with a literal quote marker: {inner!r}" + ) + # no nested blockquote (locked A1: >> collapses to one level) + expect(bubble.locator("blockquote.md-quote blockquote")).to_have_count(0) + + +# --------------------------------------------------------------------------- +# 1. Chat: the "> " lines render as semantic blockquotes +# --------------------------------------------------------------------------- + + +def test_chat_quote_renders( + page: Page, app_url: str, mock_llm: int, db_ready: None +) -> None: + _reset_db(mock_llm) + page.set_default_timeout(30_000) + bubble = _ask_quote_answer(page, app_url) + + # The four quote shapes of the byte-stable answer — the single + # line, the 3-line group (
joins), the nested collapse, and the + # inert XSS line (its escaped tag is VISIBLE text). + _assert_blockquotes(bubble, QUOTED_TEXTS) + + # The XSS probe stayed inert: no live ", + [], + ["