diff --git a/CHANGELOG.md b/CHANGELOG.md index 96d03ad0..3aa25212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ the frozen-backend fallback mirror it for their toolchains. ## [Unreleased] **Highlights** +- A rejected dubbing source language now names the code it rejected (#1960) - `bun run desktop` reclaims port 3900 from a backend the app itself left running, instead of refusing to start (#1974) - A dictation shortcut another app already owns now says so, instead of silently doing nothing (#1858) - Quitting on Windows is no longer reported as a crash on the next launch (#1898) diff --git a/backend/api/routers/dub_core.py b/backend/api/routers/dub_core.py index 3206a064..5e338b1f 100644 --- a/backend/api/routers/dub_core.py +++ b/backend/api/routers/dub_core.py @@ -540,12 +540,30 @@ _DUB_SOURCE_LANG_CODES = frozenset({ def _source_lang_override(value: str | None) -> str | None: - """Normalize a user-selected source language; auto/und means detect.""" + """Normalize a user-selected source language; auto/und means detect. + + A rejection NAMES the code it rejected. "Invalid source language code" on + its own cannot be acted on or reported usefully: it does not say which of + the ninety-odd codes was wrong, so neither the user nor a maintainer + reading the auto-filed issue can tell whether the picker offered something + the backend does not accept, or a stale preference from an older build is + still being sent (#1960). + + The value is a language code the user chose from a menu — not private + data — and the neighbouring engine validator already echoes its input the + same way. + """ code = (value or "").strip().lower() if code in {"", "auto", "und"}: return None if code not in _DUB_SOURCE_LANG_CODES: - raise HTTPException(status_code=400, detail="Invalid source language code") + raise HTTPException( + status_code=400, + detail=( + f"Invalid source language code: {code!r}. Pick a language from " + "the Dubbing source-language menu, or leave it on auto-detect." + ), + ) return code diff --git a/tests/test_dub_source_lang_1960.py b/tests/test_dub_source_lang_1960.py new file mode 100644 index 00000000..8b975a87 --- /dev/null +++ b/tests/test_dub_source_lang_1960.py @@ -0,0 +1,68 @@ +"""#1960 — a rejected source language must say WHICH code it rejected. + +The report was "400 Bad Request: Invalid source language code" and nothing +else. That cannot be acted on or triaged: it does not say which of the ninety +or so codes was wrong, so neither the user nor a maintainer reading the +auto-filed issue can tell whether the picker offered something the backend +does not accept, or a stale preference from an older build is still being +sent. I could not determine the cause from the report — which is the point. + +The value is a language code chosen from a menu, not private data, and the +neighbouring engine validator already echoes its input the same way. + +The last test is the durable one: it reads the picker's own list and asserts +the backend accepts all of it, so a code added to the menu cannot silently +become a 400. +""" +import pathlib +import re + +import pytest +from fastapi import HTTPException + +from api.routers.dub_core import _DUB_SOURCE_LANG_CODES, _source_lang_override + +_REPO = pathlib.Path(__file__).resolve().parents[1] +_LANGUAGES_JS = _REPO / "frontend" / "src" / "utils" / "languages.js" + + +def test_the_rejection_names_the_code(): + with pytest.raises(HTTPException) as caught: + _source_lang_override("zz-XX") + detail = caught.value.detail + assert "zz-xx" in detail + assert caught.value.status_code == 400 + + +def test_the_rejection_says_what_to_do(): + with pytest.raises(HTTPException) as caught: + _source_lang_override("nope") + assert "auto-detect" in caught.value.detail + + +@pytest.mark.parametrize("code", ["", " ", "auto", "und", None]) +def test_detect_is_still_detect(code): + assert _source_lang_override(code) is None + + +def test_accepted_codes_are_unchanged(): + # This only improved a message; it must not start accepting or rejecting + # anything different. + for code in sorted(_DUB_SOURCE_LANG_CODES): + assert _source_lang_override(code) == code + + +def test_every_code_the_picker_offers_is_accepted(): + src = _LANGUAGES_JS.read_text(encoding="utf-8") + codes = re.findall(r"code:\s*['\"]([A-Za-z-]+)['\"]", src) + assert len(codes) > 50, "LANG_CODES did not parse; update this extraction" + rejected = [] + for code in codes: + try: + _source_lang_override(code) + except HTTPException: + rejected.append(code) + assert rejected == [], ( + "the Dubbing source-language menu offers codes the backend rejects, so " + f"picking them returns 400: {rejected}" + )