Files
VoiceStudio/tests/test_translation_install_command.py
522bbddccf feat(translate): highlighted Install affordance for uninstalled engines + dismissable/auto-clearing error banner (#847)
Two related Dub-tab translation-flow fixes, one PR.

TASK 1 — proactive, highlighted Install affordance in the translate engine
selector (replaces "find out only via a translate-time 400"):

- FROM-SOURCE lane (activeEngineUnavailable && !enginesSandboxed): the muted
  install chip is promoted to a HIGHLIGHTED brand-accent Install button, still
  wired to handleInstallEngine(translateProvider) with the installing/disabled
  state. Selecting any uninstalled engine surfaces it immediately.
- FROZEN lane (enginesSandboxed): pip install is impossible in the read-only,
  signed packaged env, so the disabled "needs dev install" span becomes an
  equally highlighted button opening a popover with (1) the exact install
  command + copy-to-clipboard, (2) one-click "Switch to Argos (bundled,
  offline)" — the guaranteed importable escape hatch, and (3) a Docs link via
  the existing Tauri shell.open path. Gated on the existing `sandboxed` flag,
  not platform.
- Single-source install command: new translation_engines.install_command()
  is the one source of truth; list_engines() stamps `install_command` per
  engine and BOTH the argos + deep_translator translate-time 400 messages build
  their command from it, so the proactive button and the 400 can't drift.
  engines.ts gains `install_command: string | null`.

TASK 2 — the translation error banner now dismisses and clears (class fix):

- Root cause: handleTranslateAll never cleared dubError, so a stale 400
  survived even a successful retry. It now clears at the start of every
  attempt.
- Corrective-action clears (whole class): changing the engine and installing
  the package both clear dubError (wrapped setTranslateProvider +
  handleInstallEngine in DubTab).
- DubFooter's banner gains a × dismiss and a guarded auto-timeout (skipped
  while generating so live per-segment errors persist).

i18n: 8 new dub.* keys translated across all 21 locales. Docs: new
docs/dubbing/translation-engines.md (from-source vs packaged build) linked from
the popover Docs button + a troubleshooting cross-reference. Tests: FE
regression for both lanes + never-installs-when-sandboxed + banner
dismiss/auto-clear; BE regression that list_engines() install_command is
embedded verbatim in the dub_translate 400s.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:16:13 +05:30

72 lines
3.0 KiB
Python

"""Single-source install command for translation engines.
The proactive Install affordance in the Dub Engine selector (fed by
``list_engines()['install_command']``) and the translate-time 400 error
(dub_translate.py) must both read the SAME command string, so a user is never
told two different things. These tests fail-before / pass-after the
``translation_engines.install_command`` extraction and its use in the 400s.
"""
import asyncio
import json
import os
import sys
os.environ.setdefault("OMNIVOICE_DISABLE_FILE_LOG", "1")
from services import translation_engines as te
def test_list_engines_emits_install_command():
engines = {e["id"]: e for e in te.list_engines()}
# deep_translator-backed online engines all share the same command.
for eid in ("google", "deepl", "microsoft", "mymemory"):
assert engines[eid]["install_command"] == "uv pip install deep_translator", eid
assert engines["argos"]["install_command"] == "uv pip install argostranslate"
assert engines["openai"]["install_command"] == "uv pip install openai"
# NLLB rides on the core `transformers` dep — no separate install line.
assert engines["nllb"]["install_command"] is None
def test_install_command_helper_matches_registry():
assert te.install_command("google") == "uv pip install deep_translator"
assert te.install_command("nllb") is None
assert te.install_command("does-not-exist") is None
# Accepts a registry entry dict too (used by list_engines).
assert te.install_command(te.get_engine("openai")) == "uv pip install openai"
def _translate_400_body(monkeypatch, provider, missing_module):
"""Force the optional dep to be unimportable, run one translate, return the
400 JSON body. ``sys.modules[name] = None`` makes ``import name`` raise
ImportError even when the package is actually installed — deterministic on
dev + CI regardless of what's in the venv."""
from api.routers.dub_translate import dub_translate
from schemas.requests import TranslateRequest, TranslateSegment
monkeypatch.setitem(sys.modules, missing_module, None)
req = TranslateRequest(
segments=[TranslateSegment(id="1", text="hello world")],
target_lang="es",
provider=provider,
source_lang="en",
)
resp = asyncio.run(dub_translate(req))
assert resp.status_code == 400, resp
return json.loads(resp.body)
def test_deep_translator_400_embeds_registry_install_command(monkeypatch):
body = _translate_400_body(monkeypatch, "google", "deep_translator")
cmd = te.install_command("google")
assert cmd == "uv pip install deep_translator"
# The exact command from list_engines appears verbatim in the 400 — they
# cannot drift.
assert cmd in body["error"], body
def test_argos_400_embeds_registry_install_command(monkeypatch):
body = _translate_400_body(monkeypatch, "argos", "argostranslate")
cmd = te.install_command("argos")
assert cmd == "uv pip install argostranslate"
assert cmd in body["error"], body