feat(translation): re-translate on model change — artist-scoped + global re-run (#146)
retranslate_posts resets the 5 translation columns to NULL for a scoped set of posts (all, or WHERE artist_id IN ids) then reuses the untranslated sweep to re-run them, chasing the tail until drained (run-until-done). Interpreter cache keys on engine_version so a changed model re-translates, an unchanged one is cache-fast. Reset only happens when the service is configured+healthy so translations are never wiped when they can't be rebuilt. New POST /settings/translation/retranslate (artist_id | all=true). UI: per-artist 'Re-translate posts' on the Artist Management tab + 'Re-translate all' in the Settings Translation card, both with confirm dialogs. No migration (reuses m143 columns). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,68 @@ async def test_translation_run_enqueues_when_configured(client, monkeypatch):
|
||||
assert (await resp.get_json())["celery_task_id"] == "t1"
|
||||
|
||||
|
||||
def _fake_retranslate(monkeypatch, sink):
|
||||
# Record the kwargs the endpoint enqueues with, return a fake AsyncResult.
|
||||
monkeypatch.setattr(
|
||||
"backend.app.tasks.translation.retranslate_posts.delay",
|
||||
lambda *a, **k: sink.update(k) or type("R", (), {"id": "r1"})(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_translation_retranslate_requires_config(client):
|
||||
# Disabled → 400 even with an explicit scope (no reset is enqueued).
|
||||
resp = await client.post(
|
||||
"/api/settings/translation/retranslate", json={"all": True})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_translation_retranslate_requires_target(client, monkeypatch):
|
||||
# 'all' must be explicit: an empty body is a 400, so a stray call can't
|
||||
# wipe every translation.
|
||||
_fake_retranslate(monkeypatch, {})
|
||||
await client.patch("/api/settings/import", json={
|
||||
"translation_enabled": True, "interpreter_base_url": "http://i.lan",
|
||||
})
|
||||
resp = await client.post("/api/settings/translation/retranslate")
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_translation_retranslate_artist_scope(client, monkeypatch):
|
||||
sink = {}
|
||||
_fake_retranslate(monkeypatch, sink)
|
||||
await client.patch("/api/settings/import", json={
|
||||
"translation_enabled": True, "interpreter_base_url": "http://i.lan",
|
||||
})
|
||||
resp = await client.post(
|
||||
"/api/settings/translation/retranslate", json={"artist_id": 7})
|
||||
assert resp.status_code == 202
|
||||
assert (await resp.get_json())["celery_task_id"] == "r1"
|
||||
assert sink["artist_ids"] == [7]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_translation_retranslate_all_scope(client, monkeypatch):
|
||||
sink = {}
|
||||
_fake_retranslate(monkeypatch, sink)
|
||||
await client.patch("/api/settings/import", json={
|
||||
"translation_enabled": True, "interpreter_base_url": "http://i.lan",
|
||||
})
|
||||
resp = await client.post(
|
||||
"/api/settings/translation/retranslate", json={"all": True})
|
||||
assert resp.status_code == 202
|
||||
assert sink["artist_ids"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_translation_retranslate_bad_artist_id(client):
|
||||
resp = await client.post(
|
||||
"/api/settings/translation/retranslate", json={"artist_id": "abc"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_rejects_non_object(client):
|
||||
resp = await client.patch("/api/settings/import", json=[1, 2, 3])
|
||||
|
||||
Reference in New Issue
Block a user