phase: 123_chat_image_questions
Build and Push Containers / build-and-push-app (push) Successful in 1m54s
Build and Push Containers / build-and-push-db (push) Failing after 13s

All gates green. Verification complete.

**Phase 123 — final verification pass (all 4 tasks already in `complete/`)**

- Verified the full implementation is in the working tree: `app/api/chat_images.py` (upload/serve pair), `ChatRequest.image`/`ChatMessage.image` (path-validated, omitted-when-None), toggle-off + stale-file hinted error frames, `build_user_content` multimodal build at both sites (chat.py deflected branch + `run_agent`), config-gated composer attach/preview/upload-then-send, restore + shared rendering, CSP `img-src 'self' data:` carve-out, mock-LLM capture buffer.
- `uv run pytest` → **2796 passed**, exit 0 (unit + integration).
- `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (29/4615 missed; phase-123 modules 99–100%).
- `uv run pytest tests/e2e/test_chat_image_questions.py -v --no-cov` → **5 passed** in isolation.
- `uv run ruff check . && uv run pyright` → clean (0 errors).

**Completion criteria:** (1) attach→send→multimodal text+image to the model, bubble/reload/shared all render it, saved chat stores the PATH with `"base64" not in json.dumps(stored)` — **verified** (E2E tests 1–4 + integration round-trip); (2) `BOR_IMAGES=false` — control hidden, exact hinted error frame, zero model calls / no query_log row — **verified** (E2E test 5 + integration); (3) text-only byte-identical (`content` stays a plain `str`) — **verified** (unit + integration); (4) all gates green — **verified**; (5) commit + phase move — left to the harness per pipeline rules (no `git add`/`commit` run).

No defects found; no live-infrastructure changes (repo + local dev DB only). **Next pending phase: none** — 123 is the last phase in `todo/`.
This commit is contained in:
2026-09-25 05:19:18 -04:00
parent a19d78d284
commit bef24e05e2
48 changed files with 3910 additions and 71 deletions
+17 -4
View File
@@ -29,7 +29,10 @@ from app.core.security_headers import CSP, SecurityHeadersMiddleware
#: The exact expected header set (decision A1 for the CSP, A4 for the
#: other two).
EXPECTED_HEADERS = {
"content-security-policy": "default-src 'self'; base-uri 'none'; frame-ancestors 'none'",
"content-security-policy": (
"default-src 'self'; base-uri 'none'; frame-ancestors 'none'; "
"img-src 'self' data:"
),
"x-frame-options": "DENY",
"x-content-type-options": "nosniff",
}
@@ -111,10 +114,19 @@ def _assert_security_headers(start: Message, expected_extra: dict[str, str] | No
def test_csp_constant_is_the_exact_a1_policy() -> None:
"""The owner-approved A1 string, verbatim: same-origin default, no
base-tag hijack, no framing — no 'unsafe-inline', no report sink."""
assert CSP == "default-src 'self'; base-uri 'none'; frame-ancestors 'none'"
"""The owner-approved A1 string (phase 82), verbatim, with the
phase-123 ``img-src`` carve-out (the question-image composer's
data-URL preview + live bubble — see ``security_headers.CSP``):
same-origin default, no base-tag hijack, no framing — no
'unsafe-inline', no report sink, and the ``data:`` allowance is
SCOPED to img-src (never script/style/fetch)."""
assert CSP == (
"default-src 'self'; base-uri 'none'; frame-ancestors 'none'; "
"img-src 'self' data:"
)
assert "unsafe-inline" not in CSP
# the carve-out is img-src ONLY — no other directive gains data:
assert CSP.count("data:") == 1
# ---------------------------------------------------------------------------
@@ -158,6 +170,7 @@ def test_pre_existing_csp_from_an_inner_layer_is_preserved() -> None:
while the other two headers are still added."""
themed = (
"default-src 'self'; base-uri 'none'; frame-ancestors 'none'; "
"img-src 'self' data:; "
"style-src 'self' 'sha256-2rm3wPcQfXmE8q1s9vBzK7hN4tY5uJ6gW3oR0cAeDfH='"
)
wrapped = SecurityHeadersMiddleware(