* docs(plan-04): spec + plan for pipeline error transparency (#131) speckit spec/plan/research/data-model/contract/quickstart for plan-04. Grounds the fix in the real code map: shared failure-event builder (backend/core/failure.py) feeding tasks.py + dub_pipeline.py + dub_core.py, non-empty reason guarantee, sanitized diagnostic block, frontend renderer with docs deeplink. Closes-target: #131 (children #122, #63). Design only — no code changes yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(pipeline): structured, non-empty failure events + logged tracebacks (#131) plan-04 backend: no more silent "unknown error". A shared failure helper guarantees a non-empty reason at every emit site and a sanitized, copyable diagnostic block. - backend/core/failure.py: build_failure()/build_failure_event() (reason falls back to the exception class name), sanitize() (reuses the logging_filter HF-token regex + redacts *TOKEN*/*KEY*/*SECRET* env values + home→~), diagnostic() (reuses the env capture), classify() reusing the error_docs_map 5-class taxonomy for the docs deeplink + hint. - core/tasks.py worker: structured event instead of bare str(e); keeps the logged traceback. - services/dub_pipeline.py: enrich download/extract error yields; ADD the missing outer `except Exception` (the #122 path — unhandled ingest errors were never surfaced with stage context); surface the previously-silent demucs/scene/thumbnail degradations as non-fatal `warning` events. - api/routers/batch.py: guaranteed non-empty batch failure reason. SSE payload is additive (legacy `error`/`stage`/`detail` keys preserved), so existing frontends keep working and already show the specific reason. Tests (TDD, fail-before/pass-after): 14 cases — non-empty-reason guarantee, redaction, diagnostic sanitization, and the 3 Test-matrix triggers (worker / extract / url). 483 passed, 0 regressions. Closes #131. Refs #122, #63. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dub-ui): show specific cause + docs deeplink + copyable diagnostic (#131) plan-04 frontend. The backend now sends a structured, non-empty failure; surface it to the user instead of "extract: unknown error". - dubSlice: DubFailure type + dubFailure state/setter. - useDubWorkflow: capture the structured failure on the SSE error event (reason/error_class/stage/hint/docs_topic/diagnostic); clear on new runs. - DubTab: DubFailureNotice renders the actionable hint, an "Open docs" deeplink (via the existing errorDocsMap classifier), and a "Copy diagnostic" button — shown beneath the error badge in both failure banners. typecheck + build clean; 66 frontend tests pass. Refs #131, #122, #63. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(failure): annotate intentional best-effort excepts (CodeQL) The new security workflow's CodeQL flagged 5 bare `except: pass` blocks. All are deliberate best-effort guards (sanitize/diagnostic must never throw on the failure path; the test cancels the worker to tear it down). Added explanatory comments per CodeQL's py/empty-except rule. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
3.6 KiB
Python
93 lines
3.6 KiB
Python
"""plan-04 (#131) — unit tests for the shared failure helper.
|
|
|
|
These are the foundational (Phase 2) tests: the non-empty-reason guarantee,
|
|
redaction, classification, and the diagnostic block. Written RED before
|
|
`core/failure.py` exists.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
from core import failure
|
|
|
|
|
|
# ── Non-empty reason guarantee (the core fix) ───────────────────────────────
|
|
|
|
def test_reason_non_empty_when_exception_message_empty():
|
|
evt = failure.build_failure(ValueError(""), stage="extract")
|
|
assert evt["reason"], "reason must never be empty"
|
|
assert evt["error_class"] == "ValueError"
|
|
assert evt["stage"] == "extract"
|
|
# Backward-compat mirror used by older frontends.
|
|
assert evt["error"] == evt["reason"]
|
|
|
|
|
|
def test_reason_uses_message_when_present():
|
|
evt = failure.build_failure(FileNotFoundError("no such file: clip.mp4"), stage="extract")
|
|
assert "no such file" in evt["reason"]
|
|
assert evt["error_class"] == "FileNotFoundError"
|
|
|
|
|
|
def test_accepts_plain_string_message():
|
|
evt = failure.build_failure("preflight: ffmpeg not found", stage="preflight")
|
|
assert evt["reason"] == "preflight: ffmpeg not found"
|
|
assert evt["stage"] == "preflight"
|
|
|
|
|
|
def test_build_failure_event_carries_type():
|
|
evt = failure.build_failure_event(RuntimeError("boom"), stage="task")
|
|
assert evt["type"] == "error"
|
|
assert evt["reason"] == "boom"
|
|
# warning variant for non-fatal degradations
|
|
warn = failure.build_failure_event(RuntimeError("demucs down"), stage="demucs", event_type="warning")
|
|
assert warn["type"] == "warning"
|
|
|
|
|
|
# ── Redaction (Constitution I) ──────────────────────────────────────────────
|
|
|
|
def test_sanitize_redacts_hf_token():
|
|
tok = "hf_" + "A" * 36
|
|
out = failure.sanitize(f"auth failed using {tok} on download")
|
|
assert tok not in out
|
|
|
|
|
|
def test_sanitize_redacts_secret_env_values(monkeypatch):
|
|
monkeypatch.setenv("OPENAI_API_KEY", "sk-supersecretvalue123456")
|
|
out = failure.sanitize("request died: sk-supersecretvalue123456 rejected")
|
|
assert "sk-supersecretvalue123456" not in out
|
|
|
|
|
|
def test_sanitize_strips_home_path():
|
|
home = str(Path.home())
|
|
out = failure.sanitize(f"could not open {home}/Movies/clip.mp4")
|
|
assert home not in out
|
|
assert "~" in out
|
|
|
|
|
|
# ── Diagnostic block (US3) ──────────────────────────────────────────────────
|
|
|
|
def test_diagnostic_has_context_and_no_secrets(monkeypatch):
|
|
leaked = "hf_" + "B" * 36
|
|
monkeypatch.setenv("HF_TOKEN", leaked)
|
|
evt = failure.build_failure(RuntimeError("ffprobe exploded"), stage="extract")
|
|
diag = evt["diagnostic"]
|
|
assert "extract" in diag
|
|
assert "RuntimeError" in diag
|
|
assert leaked not in diag
|
|
# carries an environment summary
|
|
assert ("OS" in diag) or ("Python" in diag)
|
|
|
|
|
|
# ── Classification → docs topic + hint (US1, FR-005) ────────────────────────
|
|
|
|
def test_docs_topic_and_hint_for_known_class():
|
|
evt = failure.build_failure(
|
|
ModuleNotFoundError("No module named 'pkg_resources'"), stage="task"
|
|
)
|
|
assert evt["docs_topic"] == "PKG_RESOURCES_MISSING"
|
|
assert evt["hint"], "known classes must carry an actionable hint"
|
|
|
|
|
|
def test_unknown_cause_has_empty_topic_but_still_non_empty_reason():
|
|
evt = failure.build_failure(RuntimeError("totally novel failure xyz"), stage="task")
|
|
assert evt["docs_topic"] == ""
|
|
assert evt["reason"]
|