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:
+5
-15
@@ -57,12 +57,15 @@ carries the phase-64 per-file progress — ``current_file`` (the
|
||||
``files_done`` / ``files_total`` — null/0/0 before the import starts
|
||||
(clone/pull reports no file yet) and in terminal states, which clear
|
||||
``current_file`` but keep the run's final counts.
|
||||
|
||||
The ``failed`` state's ``error`` string is masked by the shared
|
||||
sanitizer — the ``user:pass@`` masker now lives in :mod:`app.core.errors`
|
||||
(imported here under the private name ``_sanitize_error``).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
@@ -72,6 +75,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.config import get_settings
|
||||
from app.core.auth import require_admin
|
||||
from app.core.errors import sanitize_error as _sanitize_error
|
||||
from app.db import SessionLocal
|
||||
from app.rag.git_sources import effective_sources
|
||||
from app.rag.importer import ImportSummary, import_sources
|
||||
@@ -89,20 +93,6 @@ router = APIRouter(
|
||||
dependencies=[Depends(require_admin)], # phase 16 pattern: admin-only surface
|
||||
)
|
||||
|
||||
#: ``user:pass@`` inside any error text (git stderr, endpoint URLs) —
|
||||
#: masked so a sync failure can never leak credentials into the UI.
|
||||
_CREDS_RE = re.compile(r"[A-Za-z0-9._~%*-]+:[A-Za-z0-9._~%*-]+@")
|
||||
|
||||
|
||||
def _sanitize_error(message: str) -> str:
|
||||
"""Mask credentials embedded in an error string (no secrets in the UI).
|
||||
|
||||
Git's stderr is otherwise surfaced verbatim (phase locked decisions) —
|
||||
it names the failing repo and git's reason, which is what the admin
|
||||
needs to fix things.
|
||||
"""
|
||||
return _CREDS_RE.sub("*****@", message)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SyncStatus:
|
||||
|
||||
Reference in New Issue
Block a user