7a4de7278d
tasks/translation.py — translate_posts: picks untranslated posts (title OR description non-empty), per-post [title, description] batch via the Interpreter client, stores translations + detected lang + engine_version; passthrough / already-target posts are marked handled with no stored translation. 503 or a connection error interrupts (retry next cycle), 400 stops (fix config), per-post commit keeps progress; wall-clock bounded. Wired into celery (maintenance_long lane) + a daily beat. No-op unless enabled + base URL set + healthy. GET /settings/translation/status + POST .../run for the Settings card. Task tests (stubbed client, monkeypatched session). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
128 lines
3.7 KiB
Python
128 lines
3.7 KiB
Python
"""translate_posts sweep (#143) — integration, with a stubbed Interpreter client
|
|
and the task's session factory pointed at the test's db_sync."""
|
|
import pytest
|
|
from sqlalchemy import select
|
|
|
|
from backend.app.models import Artist, ImportSettings, Post
|
|
from backend.app.tasks.translation import translate_posts
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
class _Ctx:
|
|
def __init__(self, s):
|
|
self.s = s
|
|
|
|
def __enter__(self):
|
|
return self.s
|
|
|
|
def __exit__(self, *a):
|
|
return False
|
|
|
|
|
|
def _sf(db_sync):
|
|
class _SM:
|
|
def __call__(self):
|
|
return _Ctx(db_sync)
|
|
|
|
return _SM()
|
|
|
|
|
|
def _artist(db):
|
|
a = Artist(name="A", slug="a")
|
|
db.add(a)
|
|
db.flush()
|
|
return a
|
|
|
|
|
|
def _post(db, artist_id, *, title=None, desc=None, ext="p1"):
|
|
p = Post(
|
|
artist_id=artist_id, external_post_id=ext,
|
|
post_title=title, description=desc,
|
|
)
|
|
db.add(p)
|
|
db.flush()
|
|
return p
|
|
|
|
|
|
def _enable(db, url="http://i.lan"):
|
|
cfg = db.execute(
|
|
select(ImportSettings).where(ImportSettings.id == 1)
|
|
).scalar_one()
|
|
cfg.translation_enabled = True
|
|
cfg.interpreter_base_url = url
|
|
db.flush()
|
|
|
|
|
|
def _patch(monkeypatch, db_sync):
|
|
monkeypatch.setattr(
|
|
"backend.app.tasks.translation._sync_session_factory",
|
|
lambda: _sf(db_sync),
|
|
)
|
|
|
|
|
|
def test_translate_posts_stores_translation(db_sync, monkeypatch):
|
|
_patch(monkeypatch, db_sync)
|
|
monkeypatch.setattr("backend.app.tasks.translation.ic.health", lambda *a, **k: True)
|
|
monkeypatch.setattr(
|
|
"backend.app.tasks.translation.ic.translate",
|
|
lambda texts, **k: {
|
|
"translations": [f"EN:{t}" for t in texts],
|
|
"detected_lang": "ja", "engine": "llm", "engine_version": "v1",
|
|
},
|
|
)
|
|
a = _artist(db_sync)
|
|
p = _post(db_sync, a.id, title="ねこ", desc="かわいい")
|
|
_enable(db_sync)
|
|
db_sync.commit()
|
|
|
|
assert "translated=1" in translate_posts()
|
|
db_sync.refresh(p)
|
|
assert p.post_title_translated == "EN:ねこ"
|
|
assert p.description_translated == "EN:かわいい"
|
|
assert p.translated_source_lang == "ja"
|
|
assert p.translation_engine_version == "v1"
|
|
|
|
|
|
def test_translate_posts_passthrough_english_marks_handled(db_sync, monkeypatch):
|
|
_patch(monkeypatch, db_sync)
|
|
monkeypatch.setattr("backend.app.tasks.translation.ic.health", lambda *a, **k: True)
|
|
monkeypatch.setattr(
|
|
"backend.app.tasks.translation.ic.translate",
|
|
lambda texts, **k: {
|
|
"translations": list(texts), "detected_lang": "en",
|
|
"engine": "none", "engine_version": None,
|
|
},
|
|
)
|
|
a = _artist(db_sync)
|
|
p = _post(db_sync, a.id, title="hello world", ext="p2")
|
|
_enable(db_sync)
|
|
db_sync.commit()
|
|
|
|
translate_posts()
|
|
db_sync.refresh(p)
|
|
assert p.translated_source_lang == "en" # marked handled...
|
|
assert p.post_title_translated is None # ...but nothing to show
|
|
|
|
|
|
def test_translate_posts_disabled_is_noop(db_sync, monkeypatch):
|
|
_patch(monkeypatch, db_sync)
|
|
a = _artist(db_sync)
|
|
p = _post(db_sync, a.id, title="ねこ", ext="p3")
|
|
db_sync.commit() # translation_enabled defaults False
|
|
assert translate_posts() == "disabled"
|
|
db_sync.refresh(p)
|
|
assert p.translated_source_lang is None
|
|
|
|
|
|
def test_translate_posts_service_down_leaves_untranslated(db_sync, monkeypatch):
|
|
_patch(monkeypatch, db_sync)
|
|
monkeypatch.setattr("backend.app.tasks.translation.ic.health", lambda *a, **k: False)
|
|
a = _artist(db_sync)
|
|
p = _post(db_sync, a.id, title="ねこ", ext="p4")
|
|
_enable(db_sync)
|
|
db_sync.commit()
|
|
assert translate_posts() == "interpreter unavailable"
|
|
db_sync.refresh(p)
|
|
assert p.translated_source_lang is None # will retry next run
|