baseline: initial working version
This commit is contained in:
11
tests/conftest.py
Normal file
11
tests/conftest.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Pytest configuration for local imports."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
61
tests/test_main_cli.py
Normal file
61
tests/test_main_cli.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Tests for CLI parser and translation config wiring."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from main import _build_translation_config, build_parser
|
||||
|
||||
|
||||
def test_parser_accepts_lmstudio_flags():
|
||||
parser = build_parser()
|
||||
|
||||
args = parser.parse_args(
|
||||
[
|
||||
"https://youtube.com/watch?v=demo",
|
||||
"--translation-backend",
|
||||
"lmstudio",
|
||||
"--lmstudio-base-url",
|
||||
"http://localhost:1234/v1",
|
||||
"--lmstudio-model",
|
||||
"gemma-custom",
|
||||
]
|
||||
)
|
||||
|
||||
assert args.translation_backend == "lmstudio"
|
||||
assert args.lmstudio_base_url == "http://localhost:1234/v1"
|
||||
assert args.lmstudio_model == "gemma-custom"
|
||||
|
||||
|
||||
def test_translation_config_prefers_cli_over_env(monkeypatch):
|
||||
monkeypatch.setenv("LM_STUDIO_BASE_URL", "http://env-host:1234/v1")
|
||||
monkeypatch.setenv("LM_STUDIO_MODEL", "env-model")
|
||||
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(
|
||||
[
|
||||
"https://youtube.com/watch?v=demo",
|
||||
"--lmstudio-base-url",
|
||||
"http://cli-host:1234/v1",
|
||||
"--lmstudio-model",
|
||||
"cli-model",
|
||||
]
|
||||
)
|
||||
|
||||
config = _build_translation_config(args)
|
||||
|
||||
assert config.base_url == "http://cli-host:1234/v1"
|
||||
assert config.model == "cli-model"
|
||||
|
||||
|
||||
def test_translation_config_uses_env_defaults(monkeypatch):
|
||||
monkeypatch.setenv("LM_STUDIO_BASE_URL", "http://env-host:1234/v1")
|
||||
monkeypatch.setenv("LM_STUDIO_MODEL", "env-model")
|
||||
monkeypatch.setenv("LM_STUDIO_API_KEY", "env-key")
|
||||
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(["https://youtube.com/watch?v=demo"])
|
||||
|
||||
config = _build_translation_config(args)
|
||||
|
||||
assert config.base_url == "http://env-host:1234/v1"
|
||||
assert config.model == "env-model"
|
||||
assert config.api_key == "env-key"
|
||||
136
tests/test_translation.py
Normal file
136
tests/test_translation.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Tests for the LM Studio translation layer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from src.core_utils import TranslationError
|
||||
from src.translation import LMStudioTranslator, TranslationConfig
|
||||
|
||||
|
||||
def _mock_client(handler):
|
||||
return httpx.Client(transport=httpx.MockTransport(handler))
|
||||
|
||||
|
||||
def test_translation_config_normalizes_base_url():
|
||||
config = TranslationConfig.from_env(base_url="http://127.0.0.1:1234")
|
||||
|
||||
assert config.base_url == "http://127.0.0.1:1234/v1"
|
||||
assert config.chat_completions_url == "http://127.0.0.1:1234/v1/chat/completions"
|
||||
assert config.model == "gemma-3-4b-it"
|
||||
|
||||
|
||||
def test_build_payload_includes_model_and_prompt():
|
||||
translator = LMStudioTranslator(TranslationConfig(), client=_mock_client(lambda request: None))
|
||||
|
||||
payload = translator.build_payload("Hello world", "en", "es")
|
||||
|
||||
assert payload["model"] == "gemma-3-4b-it"
|
||||
assert payload["messages"][0]["role"] == "system"
|
||||
assert "Translate the user-provided text from en to es." in payload["messages"][0]["content"]
|
||||
assert payload["messages"][1]["content"] == "Hello world"
|
||||
|
||||
|
||||
def test_translate_segments_preserves_order_and_blank_segments():
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
text = request.read().decode("utf-8")
|
||||
if "first" in text:
|
||||
content = "primero"
|
||||
elif "third" in text:
|
||||
content = "tercero"
|
||||
else:
|
||||
content = "desconocido"
|
||||
return httpx.Response(200, json={"choices": [{"message": {"content": content}}]})
|
||||
|
||||
translator = LMStudioTranslator(TranslationConfig(), client=_mock_client(handler))
|
||||
|
||||
translated = translator.translate_segments(["first", "", "third"], target_language="es", source_language="en")
|
||||
|
||||
assert translated == ["primero", "", "tercero"]
|
||||
|
||||
|
||||
def test_retry_on_transient_http_error_then_succeeds():
|
||||
attempts = {"count": 0}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
attempts["count"] += 1
|
||||
if attempts["count"] == 1:
|
||||
return httpx.Response(503, json={"error": {"message": "busy"}})
|
||||
return httpx.Response(200, json={"choices": [{"message": {"content": "hola"}}]})
|
||||
|
||||
translator = LMStudioTranslator(
|
||||
TranslationConfig(max_retries=2),
|
||||
client=_mock_client(handler),
|
||||
sleeper=lambda _: None,
|
||||
)
|
||||
|
||||
translated = translator.translate_text("hello", target_language="es", source_language="en")
|
||||
|
||||
assert translated == "hola"
|
||||
assert attempts["count"] == 2
|
||||
|
||||
|
||||
def test_parse_response_content_rejects_empty_content():
|
||||
with pytest.raises(TranslationError, match="empty translation"):
|
||||
LMStudioTranslator.parse_response_content({"choices": [{"message": {"content": " "}}]})
|
||||
|
||||
|
||||
def test_translate_text_raises_on_malformed_response():
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, json={"choices": []})
|
||||
|
||||
translator = LMStudioTranslator(TranslationConfig(), client=_mock_client(handler))
|
||||
|
||||
with pytest.raises(TranslationError, match="did not contain a chat completion message"):
|
||||
translator.translate_text("hello", target_language="es", source_language="en")
|
||||
|
||||
|
||||
def test_translate_text_falls_back_to_user_only_prompt_for_template_error():
|
||||
attempts = {"count": 0}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
attempts["count"] += 1
|
||||
body = request.read().decode("utf-8")
|
||||
if attempts["count"] == 1:
|
||||
return httpx.Response(
|
||||
400,
|
||||
text='{"error":"Error rendering prompt with jinja template: \\"Conversations must start with a user prompt.\\""}',
|
||||
)
|
||||
assert '"role":"user"' in body
|
||||
return httpx.Response(200, json={"choices": [{"message": {"content": "hola"}}]})
|
||||
|
||||
translator = LMStudioTranslator(TranslationConfig(), client=_mock_client(handler))
|
||||
|
||||
translated = translator.translate_text("hello", target_language="es", source_language="en")
|
||||
|
||||
assert translated == "hola"
|
||||
assert attempts["count"] == 2
|
||||
|
||||
|
||||
def test_translate_text_falls_back_to_structured_prompt_for_custom_template():
|
||||
attempts = {"count": 0}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
attempts["count"] += 1
|
||||
body = request.read().decode("utf-8")
|
||||
if attempts["count"] == 1:
|
||||
return httpx.Response(
|
||||
400,
|
||||
text='{"error":"Error rendering prompt with jinja template: \\"Conversations must start with a user prompt.\\""}',
|
||||
)
|
||||
if attempts["count"] == 2:
|
||||
return httpx.Response(
|
||||
400,
|
||||
text='{"error":"Error rendering prompt with jinja template: \\"User role must provide `content` as an iterable with exactly one item. That item must be a mapping(type:\'text\' | \'image\', source_lang_code:string, target_lang_code:string, text:string | none, image:string | none).\\""}',
|
||||
)
|
||||
assert '"source_lang_code":"en"' in body
|
||||
assert '"target_lang_code":"es"' in body
|
||||
return httpx.Response(200, json={"choices": [{"message": {"content": "hola"}}]})
|
||||
|
||||
translator = LMStudioTranslator(TranslationConfig(), client=_mock_client(handler))
|
||||
|
||||
translated = translator.translate_text("hello", target_language="es", source_language="en")
|
||||
|
||||
assert translated == "hola"
|
||||
assert attempts["count"] == 3
|
||||
Reference in New Issue
Block a user