mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
make i18n-translate addressed the engine directly, so a run could never reach more than the 47 languages Argos ships models for, while meta/languages.yml names 183. service_url now resolves web-svc-translate, which routes a pair the engine does not serve to a chat model, and the deploy brings both roles up. The gateway advertised only what its backends declared, and the chat backend declared nothing, so /languages reported 51 targets of 183 and the readiness wait spun to its hour-long timeout without translating a line. The chat backend now declares every language the table names. Routing offered libretranslate for every pair, because an undeclared catalogue read as any pair, so a language it holds no model for was answered with HTTP 400 instead of falling through to the next backend. serves() now reads the backend's own /languages, which catalogue() already exposed for the advertisement. Declaring the pairs in the role was the first attempt and carried a second copy that was already stale: the engine reports zh-Hans where the role's list says zh. A small model copies the prompt markers back, echoes the source line and labels its answer with the target language. All three reached the catalog, and an entry carrying its own source scores below the oracle's middle level. clean() strips them, and only from an answer that carried scaffolding, because on its own a short word and a colon is a translation, not a label. server() had lost its redeploy parameter while cli/build/i18n kept passing it, so every run raised TypeError before translating a line and the only symptom was an empty log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
247 lines
9.4 KiB
Python
247 lines
9.4 KiB
Python
import os
|
|
import subprocess
|
|
import unittest
|
|
import unittest.mock as mock
|
|
import urllib.error
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
|
|
from utils.i18n.libretranslate import (
|
|
LibreTranslate,
|
|
accelerated,
|
|
deploying,
|
|
server,
|
|
)
|
|
|
|
|
|
class TestParallelDeployIsRefused(unittest.TestCase):
|
|
def test_a_running_deploy_stops_the_translation(self) -> None:
|
|
with (
|
|
mock.patch("utils.i18n.libretranslate.deploying", return_value=True),
|
|
mock.patch("utils.i18n.libretranslate.deploy") as started,
|
|
self.assertRaises(RuntimeError),
|
|
server(Path("/repo")),
|
|
):
|
|
pass
|
|
started.assert_not_called()
|
|
|
|
def test_a_requested_redeploy_replaces_the_gateway_that_answers(self) -> None:
|
|
"""``make i18n-translate redeploy=true`` reaches here as a keyword.
|
|
|
|
The parameter went missing once while the caller kept passing it, so
|
|
every run raised TypeError before translating a line, and the only
|
|
symptom was an empty log.
|
|
"""
|
|
with (
|
|
mock.patch("utils.i18n.libretranslate.deploying", return_value=False),
|
|
mock.patch("utils.i18n.libretranslate.deployed") as probed,
|
|
mock.patch(
|
|
"utils.i18n.libretranslate.deploy", return_value="http://rebuilt"
|
|
) as started,
|
|
server(Path("/repo"), redeploy=True) as url,
|
|
):
|
|
self.assertEqual(url, "http://rebuilt")
|
|
probed.assert_not_called()
|
|
started.assert_called_once()
|
|
|
|
def test_a_live_pid_that_is_not_the_router_reports_no_deploy(self) -> None:
|
|
"""This process is alive and is not a deploy.
|
|
|
|
The guard used to answer yes to any live pid. Every sandboxed command
|
|
runs in its own pid namespace where the low numbers belong to that
|
|
sandbox, so a stale file naming pid 8 blocked every translation run.
|
|
"""
|
|
self.assertFalse(deploying(self._root_holding(os.getpid())))
|
|
|
|
def test_a_live_router_pid_reports_a_deploy(self) -> None:
|
|
root = Path(self._tmp.name)
|
|
router = root / "scripts" / "deploy" / "main.sh"
|
|
router.parent.mkdir(parents=True, exist_ok=True)
|
|
router.write_text("echo started\nread -r _\n", encoding="utf-8")
|
|
held = subprocess.Popen(
|
|
["bash", str(router)],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
self.addCleanup(held.kill)
|
|
self.addCleanup(held.stdout.close)
|
|
self.assertEqual(
|
|
held.stdout.readline(),
|
|
b"started\n",
|
|
"the router has to reach its first line before its command line is read; "
|
|
"/proc/<pid>/cmdline is empty until the child execs, which it is for "
|
|
"376 of 400 reads taken right after Popen returns",
|
|
)
|
|
|
|
self.assertTrue(deploying(self._root_holding(held.pid)))
|
|
|
|
def test_a_stale_pid_file_reports_no_deploy(self) -> None:
|
|
root = self._root_holding(4242)
|
|
with mock.patch("os.kill", side_effect=ProcessLookupError):
|
|
self.assertFalse(deploying(root))
|
|
|
|
def test_a_missing_pid_file_reports_no_deploy(self) -> None:
|
|
self.assertFalse(deploying(Path(self._tmp.name)))
|
|
|
|
def _root_holding(self, pid: int) -> Path:
|
|
root = Path(self._tmp.name)
|
|
(root / "build").mkdir(exist_ok=True)
|
|
(root / "build" / "deploy.pid").write_text(str(pid))
|
|
return root
|
|
|
|
def setUp(self) -> None:
|
|
self._tmp = TemporaryDirectory()
|
|
self.addCleanup(self._tmp.cleanup)
|
|
|
|
|
|
class TestTranslateFailures(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.client = LibreTranslate("http://libretranslate", 1)
|
|
|
|
def test_an_unreachable_server_stops_the_run(self) -> None:
|
|
refused = urllib.error.URLError(ConnectionRefusedError())
|
|
with (
|
|
mock.patch.object(self.client, "_call", side_effect=refused),
|
|
self.assertRaises(OSError),
|
|
):
|
|
self.client.translate(["Hello", "World"], "de")
|
|
|
|
def test_a_rejected_request_discards_only_its_texts(self) -> None:
|
|
rejected = urllib.error.HTTPError(
|
|
"http://libretranslate", 500, "boom", {}, None
|
|
)
|
|
with (
|
|
mock.patch("utils.i18n.libretranslate.time.sleep"),
|
|
mock.patch.object(self.client, "_call", side_effect=rejected),
|
|
):
|
|
self.assertEqual(
|
|
self.client.translate(["Hello", "World"], "de").values, [None, None]
|
|
)
|
|
|
|
|
|
class TestRefusalsAreRetried(unittest.TestCase):
|
|
"""A server that buckles under the lanes must not cost entries for good."""
|
|
|
|
def setUp(self) -> None:
|
|
self.client = LibreTranslate("http://libretranslate", 1)
|
|
self.refusal = urllib.error.HTTPError(
|
|
"http://libretranslate", 500, "boom", {}, None
|
|
)
|
|
|
|
def test_a_transient_refusal_is_retried_until_it_succeeds(self) -> None:
|
|
answers = [self.refusal, self.refusal, {"translatedText": ["Hallo"]}]
|
|
with (
|
|
mock.patch("utils.i18n.libretranslate.time.sleep"),
|
|
mock.patch.object(self.client, "_call", side_effect=answers),
|
|
):
|
|
outcome = self.client.translate(["Hello"], "de")
|
|
|
|
self.assertEqual(outcome.values, ["Hallo"])
|
|
self.assertEqual(outcome.refused, 2)
|
|
self.assertEqual(outcome.damaged, 0)
|
|
|
|
def test_a_permanent_refusal_is_counted_and_named(self) -> None:
|
|
with (
|
|
mock.patch("utils.i18n.libretranslate.time.sleep"),
|
|
mock.patch.object(self.client, "_call", side_effect=self.refusal),
|
|
):
|
|
outcome = self.client.translate(["Hello"], "de")
|
|
|
|
self.assertEqual(outcome.values, [None])
|
|
self.assertTrue(outcome.refused)
|
|
self.assertIn("HTTPError", outcome.refusal)
|
|
|
|
def test_a_catalog_never_inherits_another_lane_s_counts(self) -> None:
|
|
answers = [
|
|
self.refusal,
|
|
{"translatedText": ["Hallo"]},
|
|
{"translatedText": ["Hi"]},
|
|
]
|
|
with (
|
|
mock.patch("utils.i18n.libretranslate.time.sleep"),
|
|
mock.patch.object(self.client, "_call", side_effect=answers),
|
|
):
|
|
first = self.client.translate(["Hello"], "de")
|
|
second = self.client.translate(["Hello"], "de")
|
|
|
|
self.assertEqual(first.refused, 1)
|
|
self.assertEqual(second.refused, 0)
|
|
|
|
|
|
class TestScriptedTargetCodes(unittest.TestCase):
|
|
"""LibreTranslate offers Chinese as zh-Hans/zh-Hant, never as bare zh."""
|
|
|
|
def setUp(self) -> None:
|
|
self.client = LibreTranslate("http://libretranslate", 1)
|
|
self.served = [{"code": "en", "targets": ["de", "zh-Hans", "zh-Hant"]}]
|
|
|
|
def test_the_catalog_code_is_sent_as_the_code_the_server_serves(self) -> None:
|
|
with mock.patch.object(
|
|
self.client, "_call", return_value={"translatedText": ["你好"]}
|
|
) as call:
|
|
self.client.translate(["Hello"], "zh")
|
|
|
|
self.assertEqual(call.call_args.args[1]["target"], "zh-Hans")
|
|
|
|
def test_waiting_accepts_a_server_that_only_names_the_script(self) -> None:
|
|
with mock.patch.object(self.client, "_call", return_value=self.served):
|
|
self.client.wait(["zh", "de"], timeout=0)
|
|
|
|
def test_waiting_still_reports_a_target_nobody_serves(self) -> None:
|
|
with (
|
|
mock.patch.object(self.client, "_call", return_value=self.served),
|
|
self.assertRaises(TimeoutError) as raised,
|
|
):
|
|
self.client.wait(["fr"], timeout=0)
|
|
|
|
self.assertIn("fr", str(raised.exception))
|
|
|
|
|
|
class TestAccelerated(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
patched = mock.patch.dict(os.environ, {}, clear=False)
|
|
patched.start()
|
|
os.environ.pop("INFINITO_GPU_COUNT", None)
|
|
self.addCleanup(patched.stop)
|
|
|
|
def _runtimes(self, stdout: str) -> mock.Mock:
|
|
return mock.Mock(spec=subprocess.CompletedProcess, stdout=stdout)
|
|
|
|
def test_the_reserved_count_overrides_the_daemon(self) -> None:
|
|
listed = self._runtimes('{"nvidia":{"path":"nvidia-container-runtime"}}')
|
|
os.environ["INFINITO_GPU_COUNT"] = "0"
|
|
|
|
with mock.patch("subprocess.run", return_value=listed):
|
|
self.assertFalse(accelerated())
|
|
|
|
def test_a_reserved_count_enables_the_gpu_without_asking_the_daemon(self) -> None:
|
|
os.environ["INFINITO_GPU_COUNT"] = "all"
|
|
|
|
with mock.patch("subprocess.run", side_effect=AssertionError("asked docker")):
|
|
self.assertTrue(accelerated())
|
|
|
|
def test_an_empty_count_still_asks_the_daemon(self) -> None:
|
|
listed = self._runtimes('{"nvidia":{"path":"nvidia-container-runtime"}}')
|
|
os.environ["INFINITO_GPU_COUNT"] = " "
|
|
|
|
with mock.patch("subprocess.run", return_value=listed):
|
|
self.assertTrue(accelerated())
|
|
|
|
def test_a_registered_nvidia_runtime_enables_the_gpu(self) -> None:
|
|
listed = self._runtimes('{"nvidia":{"path":"nvidia-container-runtime"}}')
|
|
with mock.patch("subprocess.run", return_value=listed):
|
|
self.assertTrue(accelerated())
|
|
|
|
def test_a_host_without_the_toolkit_falls_back_to_the_cpu(self) -> None:
|
|
listed = self._runtimes('{"runc":{"path":"runc"}}')
|
|
with mock.patch("subprocess.run", return_value=listed):
|
|
self.assertFalse(accelerated())
|
|
|
|
def test_an_unavailable_docker_falls_back_to_the_cpu(self) -> None:
|
|
with mock.patch("subprocess.run", return_value=self._runtimes("")):
|
|
self.assertFalse(accelerated())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|