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
+119
View File
@@ -0,0 +1,119 @@
"""Unit tests: the shared error-string sanitizer (phase 84, task 01).
``app.core.errors.sanitize_error`` is the verbatim lift of the phase-32
sync masker — a NARROW ``user:pass@`` userinfo regex (git/HTTP
convention) with the ``*****@`` replacement. These pins fix both sides
of the contract:
* masking — every pinned ``user:pass@`` occurrence is rewritten to
``*****@`` (a single userinfo, two in one string, and one inside a
realistic git push-failure line — where ``fatal:`` / ``exit 128`` /
the host survive);
* byte-identity — credential-free text (a bare https URL without
userinfo, prose with a colon + space, a plain email, the empty
string) comes back unchanged, character for character;
* idempotence — sanitizing an already-sanitized (masked) string is a
no-op.
Plus the refactor pin: ``app.api.sync._sanitize_error`` is the shared
function under the private alias name (the phase-32 callers —
``app/api/git_sources.py``, ``app/api/docs.py`` — see the identical
function through the import).
"""
from __future__ import annotations
import app.api.sync as sync_api
from app.core.errors import sanitize_error
TWO_USERINFO = "https://alice:secret1@repo.example.com/a.git and https://bob:secret2@repo.example.com/b.git"
GIT_PUSH_FAILURE = (
"git push failed (exit 128): fatal: Authentication failed for "
"'https://bot:tok@github.com/o/r.git/'"
)
# --- The masked forms (the regex must fire) --------------------------------
def test_masks_single_userinfo_in_url() -> None:
"""The canonical shape: ``user:pass@`` → ``*****@``, host intact."""
assert sanitize_error("https://user:pass@host/repo.git") == "https://*****@host/repo.git"
def test_masks_every_userinfo_occurrence() -> None:
"""Two userinfo runs in one string → both masked, nothing else touched."""
masked = sanitize_error(TWO_USERINFO)
assert masked == "https://*****@repo.example.com/a.git and https://*****@repo.example.com/b.git"
assert "secret1" not in masked
assert "secret2" not in masked
def test_masks_token_in_git_push_failure_line() -> None:
"""The SEC-08 shape: git's auth-failure stderr with a token-bearing
remote URL → the token is masked, the actionable context survives."""
masked = sanitize_error(GIT_PUSH_FAILURE)
assert masked == (
"git push failed (exit 128): fatal: Authentication failed for "
"'https://*****@github.com/o/r.git/'"
)
assert "tok" not in masked
assert "exit 128" in masked # the reason survives
assert "fatal:" in masked
assert "github.com/o/r.git" in masked # host + repo path survive
# --- The byte-identity contract (credential-free text) ---------------------
def test_plain_url_without_userinfo_is_unchanged() -> None:
"""A bare https URL (no userinfo) plus a realistic git failure line
→ byte-identical."""
message = (
"git push failed (exit 128): fatal: repository "
"'https://github.com/owner/repo.git/' not found"
)
assert sanitize_error(message) == message
def test_prose_with_colon_and_space_is_unchanged() -> None:
"""Colons in prose are followed by spaces, not by userinfo runs —
the narrow regex must not fire (the sync tests pin the same copy)."""
model_message = (
"The embedding model ('embed') is not available — check the model endpoint and retry."
)
assert sanitize_error(model_message) == model_message
sentence = "note: the sync failed: see the log for details"
assert sanitize_error(sentence) == sentence
def test_email_in_prose_is_unchanged() -> None:
"""``owner@example.com`` carries no colon userinfo run before the
``@`` — the pattern does not match it, so it stays byte-identical
(pinned for determinism: the masker only rewrites ``user:pass@``)."""
message = "please contact owner@example.com about the failing push"
assert sanitize_error(message) == message
def test_empty_string_is_unchanged() -> None:
assert sanitize_error("") == ""
# --- Idempotence (masked forms) ---------------------------------------------
def test_idempotent_on_masked_forms() -> None:
"""Sanitizing an already-sanitized string is a no-op — the ``*****@``
replacement never forms a new ``user:pass@`` run in the pinned cases."""
for message in ("https://user:pass@host/repo.git", TWO_USERINFO, GIT_PUSH_FAILURE):
once = sanitize_error(message)
assert sanitize_error(once) == once
# --- The sync alias refactor (phase 84, task 01) ----------------------------
def test_sync_keeps_the_private_alias_of_the_shared_function() -> None:
"""``app.api.sync._sanitize_error`` IS the shared sanitizer under the
private name — the phase-32 callers (``git_sources.py``, ``docs.py``)
import it through ``app.api.sync`` and see the identical function."""
assert sync_api._sanitize_error is sanitize_error
assert sync_api._sanitize_error(GIT_PUSH_FAILURE) == sanitize_error(GIT_PUSH_FAILURE)