phase: 181_svg_document_preview
Build and Push Containers / build-and-push-app (push) Successful in 1m57s
Build and Push Containers / build-and-push-db (push) Successful in 15s

**Phase 181 — final verification pass (all tasks already complete; verified + docs pass)**

- Verified: `app/rag/svg_text.py` (100 % cov), vector branch in `_index_image_file`, `_row_kind_mismatch` text→image migration, `IMAGE_MIMES[".svg"]`, sandboxed CSP on `GET /api/documents/{id}/image`, `.doc-image-img--vector` in `document.js`/`styles.css`, figure/thumbnail deliberate non-changes.
- Tests/lint (exact commands): `uv run pytest --cov=app --cov-report=term-missing` → **4265 passed, TOTAL 99 %** (exit 0) · `uv run pytest tests/e2e/test_svg_document_preview.py -v --no-cov` → **5 passed in isolation** · `uv run ruff check .` → clean · `uv run pyright` → **0 errors**.
- Regression (each run in isolation, `--no-cov`, all rc=0): `test_image_documents`, `test_chat_image_questions`, `test_answer_images`, `test_chat_answer_clusters`, `test_image_anchor_context`, `test_image_upload_feedback`, `test_markdown_inline_links_images`, `test_prose_word_wrap`, `test_pdf_support`, `test_pictorial_pdf`, `test_garbage_pdf`, `test_pdf_image_extraction`, `test_pdf_image_context`, `test_pdf_pages_in_sources`, `test_pdf_strip_additive_selfheal`, `test_sync_image_progress`.
- Completion criteria: ①SVG→`is_image`+`image_path`, no vision call, own `<title>/<desc>/<text>` — **met**; ②`image/svg+xml` + `default-src 'none'; sandbox` + `private, max-age=3600`, raster headers byte-unchanged — **met**; ③chat figure → modal `<img>`, no XML — **met**; ④`/document.html` full-width vector in `.doc-image` + text below — **met**; ⑤`![alt](/api/documents/<id>/image)` paints — **met**; ⑥raster/PDF unchanged — **met**; ⑦all gates green — **met**; ⑧commit + phase-dir move — harness-owned.
- Documentation: found and fixed README inconsistencies — stale forward reference "left to phase 181 / is phase 181's job" → completed-tense sizing note; "Updating the documents" standalone-image list missing `svg`; new **Raster only** bullet for question uploads (`.svg` → 422) documenting the `chat_images` guard; missing `svg_indexed=N` / `import: svg indexed` log contract; env-table heading `122/123/124` → `122/123/124/181`; new troubleshooting entry for an `.svg` showing raw XML. Full audit: every `Settings` field and every `.env.example` var is documented; pages/commands/scripts all exist. AGENTS.md verified accurate (phase-181 §5 clause present) — no changes needed beyond what the phase wrote.
- Notable deviation (kept from task 02): `app/api/chat_images.py` subtracts `_VECTOR_IMAGE_EXTENSIONS` from the question-image accept set — a required consequence of LOCKED A7 (`svg` in the default image set) since `_CHAT_IMAGE_FILENAME_RE` refuses to *serve* `.svg`; the regex itself is untouched, as the phase specified.
- Note for the commit: changes include `README.md`, `AGENTS.md` and `.env.example` — the phase file's `git add app/ frontend/ tests/ .env.example .agents/phases/` line omits the two markdown files; stage them too.
- Next pending phase: `182_verbatim_show_text`.
This commit is contained in:
2026-10-14 22:54:43 -04:00
parent 6880bd0866
commit 814a400e6d
70 changed files with 4460 additions and 52 deletions
+39 -7
View File
@@ -68,6 +68,7 @@ from pydantic import ValidationError
from app.api import chat as chat_api
from app.api import chat_images
from app.api.chat_images import _VECTOR_IMAGE_EXTENSIONS
from app.config import Settings
from app.main import app as fastapi_app
from app.models import Document
@@ -354,6 +355,32 @@ def test_upload_rejects_a_non_image_extension_with_the_fixed_detail(
assert not image_dir.exists() # the dir is created on the accepted path only
def test_upload_rejects_a_vector_svg_even_though_it_indexes(
client: TestClient, image_dir: Path
) -> None:
"""Phase 181: ``svg`` joins the DEFAULT image set for the DOCUMENT
walk, and question attachments stay RASTER-only — the upload 422s
with the six-extension detail and writes nothing.
Without the subtraction this would be a silent trap: the stored
``<uuid>.svg`` is refused by the serve route's filename regex (a
broken figure in the user's own bubble) and an OpenAI-compatible
vision endpoint rejects ``image/svg+xml``, so the turn could never
be answered. The vector family belongs to INDEXING, not to
attachments."""
assert ".svg" in Settings(_env_file=None).image_extension_set # pyright: ignore[reportCallIssue]
assert frozenset({".svg"}) == _VECTOR_IMAGE_EXTENSIONS
r = _upload(
client, "diagram.svg", b'<svg xmlns="http://www.w3.org/2000/svg"></svg>',
mime="image/svg+xml",
)
assert r.status_code == 422
assert r.json()["detail"] == (
"only .bmp, .gif, .jpeg, .jpg, .png, .webp images are accepted"
)
assert not image_dir.exists()
def test_upload_rejects_a_name_without_an_extension(
client: TestClient, image_dir: Path
) -> None:
@@ -916,7 +943,10 @@ def test_the_attach_reveal_is_gated_on_the_config_flag() -> None:
def test_the_client_precheck_matches_the_servers_six_extensions() -> None:
"""ONE list of the six, on both sides: the client pre-check (the
file NAME's extension — the accept attribute is advisory) and the
server's phase-122 frozenset (the upload endpoint's authority).
server's phase-122 frozenset MINUS the vector members (the upload
endpoint's authority — phase 181: question images stay RASTER-only,
a vision endpoint rejects ``image/svg+xml`` and the serve route's
filename regex would never serve one).
The client list must stay the code default's six — a drift here
would either block a valid pick or waste a round-trip on a 422."""
js = _app_js()
@@ -931,12 +961,14 @@ def test_the_client_precheck_matches_the_servers_six_extensions() -> None:
"png",
"webp",
}
# and it equals the server's default set (dots stripped — the
# client list is dotless, the server's is dotted).
server_set = {
e.lstrip(".")
for e in Settings(_env_file=None).image_extension_set # pyright: ignore[reportCallIssue]
}
# and it equals the server's default raster set (dots stripped — the
# client list is dotless, the server's is dotted). ``svg`` is in the
# DEFAULT image set for the document walk (phase 181) and
# deliberately NOT attachable here (see
# ``chat_images._VECTOR_IMAGE_EXTENSIONS``).
raw_default = Settings(_env_file=None).image_extension_set # pyright: ignore[reportCallIssue]
assert ".svg" in raw_default # the vector member (phase 181)
server_set = {e.lstrip(".") for e in raw_default - _VECTOR_IMAGE_EXTENSIONS}
assert client_set == server_set
# the file input's accept attribute names the same six (mime form).
m = re.search(r'<input type="file" id="attach-file"\s+accept="([^"]+)"', _index_html())