116 lines
3.7 KiB
Python
116 lines
3.7 KiB
Python
"""Phase 1.1 / 2.7 — translator service.
|
|
|
|
Validates the glossary prompt-prefixing and the graceful "no LLM" path.
|
|
Does NOT hit a real LLM — the client is mocked.
|
|
"""
|
|
import os
|
|
os.environ.setdefault("OMNIVOICE_DISABLE_FILE_LOG", "1")
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
import pytest
|
|
|
|
from services import translator as tr
|
|
|
|
|
|
# ── Glossary preamble ──────────────────────────────────────────────────────
|
|
|
|
|
|
def test_glossary_text_empty_inputs():
|
|
assert tr._glossary_text(None) == ""
|
|
assert tr._glossary_text([]) == ""
|
|
assert tr._glossary_text([{"source": "", "target": "y"}]) == ""
|
|
|
|
|
|
def test_glossary_text_includes_each_term():
|
|
out = tr._glossary_text([
|
|
{"source": "Marcus", "target": "Marcus", "note": "character name"},
|
|
{"source": "breakthrough", "target": "Durchbruch"},
|
|
])
|
|
assert "Marcus → Marcus" in out
|
|
assert "breakthrough → Durchbruch" in out
|
|
assert "character name" in out
|
|
|
|
|
|
# ── No-LLM graceful path ───────────────────────────────────────────────────
|
|
|
|
|
|
def test_cinematic_no_llm_returns_literal_with_marker(monkeypatch):
|
|
monkeypatch.setattr(tr, "_llm_client", lambda: None)
|
|
res = tr.cinematic_refine_sync(
|
|
"Hello.",
|
|
"Hola.",
|
|
source_lang="en",
|
|
target_lang="es",
|
|
)
|
|
# With no LLM, "text" falls back to literal and an error marker is present.
|
|
assert res["text"] == "Hola."
|
|
assert res["literal"] == "Hola."
|
|
assert res["critique"] == ""
|
|
assert res.get("error") == "no-llm"
|
|
|
|
|
|
def test_cinematic_empty_literal_is_passthrough():
|
|
res = tr.cinematic_refine_sync(
|
|
"Source",
|
|
"",
|
|
source_lang="en",
|
|
target_lang="es",
|
|
)
|
|
assert res["text"] == ""
|
|
assert res["literal"] == ""
|
|
# No error when there's literally nothing to refine.
|
|
assert "error" not in res
|
|
|
|
|
|
# ── Happy-path 3-step chain (mocked client) ────────────────────────────────
|
|
|
|
|
|
def test_cinematic_full_chain_with_mocked_llm(monkeypatch):
|
|
calls = []
|
|
responses = iter([
|
|
"reads stiff; prefer idiomatic phrasing", # reflect
|
|
"Hola, mundo.", # adapt
|
|
])
|
|
|
|
def fake_chat(client, *, system, user):
|
|
calls.append({"system": system, "user": user})
|
|
return next(responses)
|
|
|
|
mock_client = MagicMock()
|
|
monkeypatch.setattr(tr, "_llm_client", lambda: mock_client)
|
|
monkeypatch.setattr(tr, "_chat", fake_chat)
|
|
|
|
res = tr.cinematic_refine_sync(
|
|
"Hello, world.",
|
|
"Hola mundo.",
|
|
source_lang="en",
|
|
target_lang="es",
|
|
glossary=[{"source": "world", "target": "mundo"}],
|
|
)
|
|
assert res["literal"] == "Hola mundo."
|
|
assert res["text"] == "Hola, mundo."
|
|
assert res["critique"] == "reads stiff; prefer idiomatic phrasing"
|
|
assert len(calls) == 2
|
|
# Glossary prepended to both system prompts (reflect + adapt).
|
|
assert "world → mundo" in calls[0]["system"]
|
|
assert "world → mundo" in calls[1]["system"]
|
|
|
|
|
|
def test_cinematic_reflect_failure_returns_literal(monkeypatch):
|
|
mock_client = MagicMock()
|
|
monkeypatch.setattr(tr, "_llm_client", lambda: mock_client)
|
|
|
|
def boom(*a, **kw):
|
|
raise RuntimeError("LLM down")
|
|
monkeypatch.setattr(tr, "_chat", boom)
|
|
|
|
res = tr.cinematic_refine_sync(
|
|
"Hello",
|
|
"Hola",
|
|
source_lang="en",
|
|
target_lang="es",
|
|
)
|
|
assert res["text"] == "Hola"
|
|
assert res["literal"] == "Hola"
|
|
assert "reflect" in res.get("error", "")
|