Files
computer-playbook/tests/unit/python/roles/svc-ai-libretranslate-engine/files/test_prefetch_models.py
T
kevinveenbirkenbachandClaude Opus 5 2ab756982b feat(svc-ai-libretranslate-engine): serve translation without a browser surface
Adds a headless LibreTranslate role for callers that reach the engine over
the container network: no vhost, no reverse proxy, no MCP adapter.

The role is named `-engine` rather than `svc-ai-libretranslate` because
`get_entity_name` strips the category prefix, so that name and
`web-svc-libretranslate` both reduce to `libretranslate`. The entity name
keys the instance directory, the compose project and the container name, so
the two roles overwrote each other's rendered stack and `sys-service-loader`
resolved the engine's own `libretranslate` service key to the web role.
`provides:` cannot fix this: it renames the registry key, not the entity.

`loading_model` selects when models arrive: `lazy` (default) on first
request, `build` into the image, `task` by a post-deploy install. Under
`lazy` and `task` the models sit on a node-local bind so a rebuild reuses
them; `build` is mounted without it, because a bind is never seeded from an
image and would hide what the build baked.

Two lints guard the class:
- entity names must be distinct among roles that render a compose stack,
  which `provides:` cannot satisfy and only a rename can
- a role that reaches a cached upstream another role declares must declare
  it too, so the entry is discoverable from the fetch

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 23:41:57 +02:00

184 lines
6.0 KiB
Python

"""The model prefetch runs inside the LibreTranslate image, whose argostranslate
and libretranslate packages are absent here, so both are stubbed before the
module is executed.
"""
from __future__ import annotations
import importlib.util
import sys
import types
import unittest
import zipfile
from pathlib import Path
from tempfile import TemporaryDirectory
from types import ModuleType
from unittest import mock
from . import PROJECT_ROOT
SCRIPT = (
PROJECT_ROOT
/ "roles"
/ "svc-ai-libretranslate-engine"
/ "files"
/ "python"
/ "prefetch_models.py"
)
class FakePackage:
def __init__(self, from_code: str, to_code: str, path: Path | None = None) -> None:
self.from_code = from_code
self.to_code = to_code
self._path = path
def download(self) -> Path:
return self._path
def _load() -> ModuleType:
package = types.ModuleType("argostranslate.package")
package.update_package_index = lambda: None
package.get_available_packages = list
argos = types.ModuleType("argostranslate")
argos.package = package
language = types.ModuleType("libretranslate.language")
language.iso2model = list
libretranslate = types.ModuleType("libretranslate")
libretranslate.language = language
minisbd = types.ModuleType("minisbd")
minisbd.download_models = lambda *args: None
modules = {
"argostranslate": argos,
"argostranslate.package": package,
"libretranslate": libretranslate,
"libretranslate.language": language,
"minisbd": minisbd,
}
with mock.patch.dict(sys.modules, modules):
spec = importlib.util.spec_from_file_location("prefetch_models", SCRIPT)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(module)
return module
class TestWanted(unittest.TestCase):
def setUp(self) -> None:
self.module = _load()
self.available = [
FakePackage("en", "de"),
FakePackage("de", "en"),
FakePackage("en", "fr"),
FakePackage("fr", "ru"),
]
self.module.package.get_available_packages = lambda: self.available
def _pairs(self, codes: list[str], directions: str) -> list[tuple[str, str]]:
kept, _ = self.module.wanted(codes, directions)
return [(p.from_code, p.to_code) for p in kept]
def test_no_codes_keeps_every_package(self) -> None:
kept, model_codes = self.module.wanted([], "both")
self.assertEqual(kept, self.available)
self.assertEqual(model_codes, [])
def test_a_pair_survives_only_when_both_sides_are_wanted(self) -> None:
self.assertEqual(
self._pairs(["en", "de"], "both"), [("en", "de"), ("de", "en")]
)
def test_from_source_drops_the_return_direction(self) -> None:
self.assertEqual(self._pairs([], "from_source"), [("en", "de"), ("en", "fr")])
def test_from_source_composes_with_a_code_filter(self) -> None:
self.assertEqual(self._pairs(["en", "de"], "from_source"), [("en", "de")])
def test_a_code_no_package_offers_is_refused(self) -> None:
with self.assertRaises(ValueError) as caught:
self.module.wanted(["en", "xx"], "both")
self.assertIn("xx", str(caught.exception))
def test_an_empty_selection_is_refused(self) -> None:
self.available = [FakePackage("de", "en")]
self.module.package.get_available_packages = lambda: self.available
with self.assertRaises(ValueError):
self.module.wanted([], "from_source")
class TestMain(unittest.TestCase):
def setUp(self) -> None:
self.module = _load()
self._tmp = TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
self.root = Path(self._tmp.name)
archive = self.root / "en_zh.argosmodel"
with zipfile.ZipFile(archive, "w") as handle:
handle.writestr("metadata.json", "{}")
self.module.package.get_available_packages = lambda: [
FakePackage("en", "zh-Hans", archive)
]
self.module.package.install_from_path = lambda path: None
self.module.libretranslate.language.iso2model = lambda codes: [
"zh-Hans" if code == "zh" else code for code in codes
]
self.minisbd_got: list = []
self.module.download_models = lambda codes, _: self.minisbd_got.append(codes)
def test_minisbd_gets_the_model_codes_not_the_iso_codes(self) -> None:
with mock.patch.object(
sys, "argv", ["prefetch_models.py", "--load_only_lang_codes", "en,zh"]
):
self.assertEqual(self.module.main(), 0)
self.assertEqual(self.minisbd_got, [["en", "zh-Hans"]])
def test_minisbd_gets_none_when_no_codes_were_requested(self) -> None:
with mock.patch.object(sys, "argv", ["prefetch_models.py"]):
self.assertEqual(self.module.main(), 0)
self.assertEqual(self.minisbd_got, [None])
class TestFetch(unittest.TestCase):
def setUp(self) -> None:
self.module = _load()
self._tmp = TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
self.root = Path(self._tmp.name)
def _archive(self, name: str, *, valid: bool) -> Path:
path = self.root / name
if valid:
with zipfile.ZipFile(path, "w") as archive:
archive.writestr("metadata.json", "{}")
else:
path.write_bytes(b"half a download")
return path
def test_a_complete_archive_is_kept(self) -> None:
path = self._archive("good.argosmodel", valid=True)
self.assertEqual(self.module.fetch(FakePackage("en", "de", path)), path)
self.assertTrue(path.exists())
def test_a_truncated_archive_is_dropped_so_the_cache_refetches_it(self) -> None:
path = self._archive("bad.argosmodel", valid=False)
with self.assertRaises(RuntimeError):
self.module.fetch(FakePackage("en", "de", path))
self.assertFalse(path.exists())
if __name__ == "__main__":
unittest.main()