phase: 84_docs_push_error_sanitization

**Phase 84 — final verification pass: all green, no defects found**

- Verified implementation: `app/core/errors.py` (verbatim lift of sync masker), `app/api/sync.py` alias import, docs-push 502 `detail=sanitize_error(str(exc))`, all five `llm.py` error sites sanitized; new/extended test pins in place
- Tests: `uv run pytest` → **1714 passed, 0 failed**; targeted pins (new unit ×2 + integration ×1, existing 502 pin) → 13 passed; sync/git-sources regression → 67 passed
- Coverage: `uv run pytest --cov=app --cov-report=term-missing` → **99%** (`app/core/errors.py` 100%, `app/rag/llm.py` 100%) — >90% met
- E2E isolation: `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → **3 passed**
- Lint/types: `uv run ruff check .` → clean; `uv run pyright` → **0 errors**
- Criteria: 502 masks `*****@`/never token + row untouched ✅; LLM base-URL masked, credential-free strings byte-identical ✅; `_CREDS_RE` only in `app/core/errors.py` (working-tree grep) ✅; full gate green ✅; `git diff --stat` limited to the 4 app files + 2 modified test files + 3 phase task files (untracked: new module, new unit test, complete/ dir, reports, audit plan) ✅
- Commit/phase move left to the harness per instructions (task files already in `complete/`)
- No deviations; nothing to fix
- Next pending phase: **85_mobile_menu_gate_overlap**
This commit is contained in:
2026-09-08 01:11:45 -04:00
parent fa189dede7
commit f4150421bb
23 changed files with 732 additions and 29 deletions
+46
View File
@@ -189,6 +189,36 @@ def test_http_error_surfaces_status() -> None:
assert llm.embed_batches == 0
def test_embed_transport_failure_masks_credentials_in_base_url() -> None:
"""Phase 84 (SEC-13): a base URL configured with embedded
``user:pass@`` credentials never reaches the error message — the URL
in the raised :class:`EmbeddingError` is sanitized (``*****@``),
while the failure context stays readable."""
llm, _ = _make_client(
_FakeEmbeddingsService(fail=RuntimeError("connection refused")),
llm_base_url="https://svc:topsecret@llm.local/v1",
)
with pytest.raises(EmbeddingError) as exc:
asyncio.run(llm.embed(["hello"]))
msg = str(exc.value)
assert "https://*****@llm.local/v1" in msg
assert "topsecret" not in msg
assert "connection refused" in msg
assert llm.embed_batches == 0
# The credential-free pin stays byte-identical (sanitize is a no-op
# without userinfo): the default base URL appears verbatim.
llm_plain, _ = _make_client(
_FakeEmbeddingsService(fail=RuntimeError("connection refused"))
)
with pytest.raises(EmbeddingError) as exc_plain:
asyncio.run(llm_plain.embed(["hello"]))
assert (
str(exc_plain.value)
== "embeddings request to https://aipi.reeseapps.com/v1 failed: "
"connection refused"
)
def test_missing_vector_row_is_rejected() -> None:
llm, _ = _make_client(_FakeEmbeddingsService(drop_index=1))
with pytest.raises(EmbeddingError, match="returned 1 vectors for 2 inputs"):
@@ -925,6 +955,22 @@ def test_chat_transport_failure_wrapped_as_llm_error_with_base_url() -> None:
assert "aipi.reeseapps.com" in str(exc.value)
def test_chat_transport_failure_masks_credentials_in_base_url() -> None:
"""Phase 84 (SEC-13): the chat failure f-string sanitizes the base
URL the same way the embed path does (the five sites share the
construction; this is the chat representative)."""
llm, _ = _make_chat_client(
fail=RuntimeError("HTTP 502 Bad Gateway"),
llm_base_url="https://svc:topsecret@llm.local/v1",
)
with pytest.raises(LLMError) as exc:
asyncio.run(llm.chat([{"role": "user", "content": "q"}]))
msg = str(exc.value)
assert "https://*****@llm.local/v1" in msg
assert "topsecret" not in msg
assert "HTTP 502 Bad Gateway" in msg
def test_chat_llm_error_passes_through_unwrapped() -> None:
llm, _ = _make_chat_client(fail=LLMError("already wrapped"))
with pytest.raises(LLMError, match="already wrapped"):