"""No refusal answers in a hand-written English sentence. 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. Pure: it reads the sources. """ import ast import pathlib APP = pathlib.Path(__file__).resolve().parents[1] / "app" def _is_text(node: ast.AST) -> bool: """A detail that is a sentence: a literal, an f-string, `str(exc)`, or `"…" + x`.""" if isinstance(node, ast.Constant) and isinstance(node.value, str): return True if isinstance(node, ast.JoinedStr): return True if isinstance(node, ast.Call) and getattr(node.func, "id", "") == "str": return True if isinstance(node, ast.BoolOp): return any(_is_text(v) for v in node.values) if isinstance(node, ast.BinOp): return _is_text(node.left) or _is_text(node.right) return False def test_no_http_exception_carries_a_sentence(): offenders = [] for path in sorted(APP.rglob("*.py")): for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))): if not ( isinstance(node, ast.Call) and getattr(node.func, "id", getattr(node.func, "attr", "")) == "HTTPException" ): continue kw = {k.arg: k.value for k in node.keywords} detail = kw.get("detail", node.args[1] if len(node.args) > 1 else None) if detail is not None and _is_text(detail): offenders.append(f"{path.relative_to(APP).as_posix()}:{node.lineno}") assert offenders == [], ( "a refusal with an English sentence for a detail — raise `refused(status, reason)` " f"(app/refusal.py) and name the reason in both locales: {offenders}" ) def test_the_scan_sees_a_sentence_when_there_is_one(): """A guard that matches nothing is green over nothing.""" sample = ast.parse('HTTPException(status_code=400, detail=f"bad {x}")').body[0].value assert _is_text(sample.keywords[1].value) assert not _is_text(ast.parse('{"code": "refused", "reason": "x"}').body[0].value)