"""No refusal answers in a way the client cannot say. The UI is bilingual and translates a structured `detail` (`{code, reason, …}`), but a string detail it can only print as it came — so ~150 of them reached the Hungarian UI verbatim, in the blocking error dialog and on every inline surface that shows `HttpError.detail`. `app/refusal.py` is the family they became; this test is what keeps a new one from being written the old way. It reads the same slots the refusal scan does (`refusal_sites.detail_slots`), so the two cannot disagree on where a detail is sent. Pure: it reads the sources. """ import textwrap from tests.refusal_sites import Scan, app_sources # Answers that are no refusal of the app's, by the function they are sent from: the client shows a # detail only when it is an object (`lib/api/core.ts`), so these read as its generic line, and no screen # needs more from them. NO_REFUSAL = { ("main.py", "setup_gate"): "the 503 before setup: the setup screen is what the user sees", ("main.py", "spa_fallback"): "a bare 404 for an api path no route matches: no screen calls one", ("main.py", "_validation_error"): "FastAPI's own 422 list: a body the client itself got wrong", ("routes/diagnostics.py", "_parsed_events"): "the client-events beacon's 422: nothing shows it", } def _offenders(sources: dict[str, str]) -> list[str]: return [ f"{path}:{line}" for path, line, fn in Scan(sources).other_details() if (path, fn) not in NO_REFUSAL ] def test_every_detail_is_one_the_client_can_say(): offenders = _offenders(app_sources()) assert offenders == [], ( "a refusal the client can only answer with its generic line — send `{code, reason}` " f"(`refused(status, reason)`), or list it in NO_REFUSAL with why: {offenders}" ) def test_the_detail_scan_sees_each_shape_the_client_cannot_say(): """A guard that matches nothing is green over nothing.""" src = textwrap.dedent( """ def f(x): raise HTTPException(409, detail={"reason": "view_name_taken"}) def g(x): raise HTTPException(409, dict(reason="taken")) def h(x): raise HTTPException(status_code=404) def i(x): return JSONResponse({"detail": "This instance isn't set up yet."}, status_code=503) def j(x): return JSONResponse(content={"detail": {"reason": "x"}}, status_code=409) def k(x): raise HTTPException(400, detail=f"bad {x}") def l(x): raise HTTPException(404, None) def n(x, msg): raise HTTPException(400, detail=msg) def o(x): raise HTTPException(400, detail=["a sentence"]) def p(x): raise HTTPException(400, detail=describe(x)) def q(x): raise HTTPException(400, detail={"code": "a"} if x else "text") def r(x): raise HTTPException(400, detail=nothing(x)) def nothing(x): pass def ok(x): raise HTTPException(409, {"code": "refused", "reason": "x"}) raise HTTPException(409, detail=quota(x)) raise HTTPException(409, detail={"code": "a"} if x else {"code": "b"}) return JSONResponse({"detail": {"code": "too_large"}}, status_code=413) return JSONResponse({"ok": True}) def quota(x): return {"code": "quota", "reason": "files"} def setup_gate(x): return JSONResponse({"detail": "listed"}, status_code=503) """ ) lines = (3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25) assert _offenders({"main.py": src}) == [f"main.py:{n}" for n in lines] # A file whose code dicts are data still answers its refusals in a way the client can say. data = 'def f():\n raise HTTPException(400, "text")\n' assert _offenders({"plex/tmdb.py": data}) == ["plex/tmdb.py:2"]