Files
UmbraAudio/tests/test_translation_delivery.py
Umbra Tools 301ff17f3e Replace a translation delivery instead of merging into it
A re-delivery could leave a stale row behind for an utterance the new
delivery does not cover: the upsert only touches rows the delivery mentions.
--replace clears the language's draft rows first, so what the reviewer sees
is what the delivery actually says.

Confirmed rows are never deleted. A confirmed translation is an approval, and
the import skips confirmed rows anyway, so the approved text survives a
re-delivery.

2 new tests.
2026-11-06 19:36:46 -08:00

211 lines
7.6 KiB
Python

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Tests for the translation delivery ingest (spec 31 section 8).
UmbraTrans delivers one row per utterance per target language. The two
behaviours pinned here are the ones that would make a reviewer distrust the
delivery:
* a delivery stored under a locale code (``zh-Hans``) must still be found
when the reviewer asks about the base code (``zh``);
* a source utterance the target does not voice must be *kept*, with its
reason, so the gap between the English and the translation stays visible
instead of silently disappearing.
"""
from __future__ import annotations
import importlib.util
import sqlite3
from pathlib import Path
import pytest
from umbraaudio import translations as tr
_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "import_translations.py"
_spec = importlib.util.spec_from_file_location("import_translations", _SCRIPT)
assert _spec is not None and _spec.loader is not None
import_translations = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(import_translations)
def _seed(conn: sqlite3.Connection) -> None:
conn.executescript(
"""
CREATE TABLE project (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE chapters (id INTEGER PRIMARY KEY, chapter_index INTEGER);
CREATE TABLE utterances (
id INTEGER PRIMARY KEY,
chapter_id INTEGER,
utterance_index INTEGER,
speaker_name TEXT,
text TEXT,
context_before TEXT,
context_after TEXT,
language_code TEXT,
language_origin TEXT
);
INSERT INTO project (id, name) VALUES (1, 'Test');
INSERT INTO chapters (id, chapter_index) VALUES (1, 1), (2, 2);
INSERT INTO utterances (id, chapter_id, utterance_index, speaker_name, text) VALUES
(1, 1, 0, 'narrator', 'The ship landed.'),
(2, 1, 1, 'Kainda', 'Welcome to the ship.'),
(3, 1, 2, 'Kainda', 'It is very large.'),
(4, 2, 0, 'narrator', 'Later that day.'),
(5, 2, 1, 'Nanny', 'Danger.');
"""
)
conn.commit()
@pytest.fixture
def conn() -> sqlite3.Connection:
c = sqlite3.connect(":memory:")
c.row_factory = sqlite3.Row
_seed(c)
tr.ensure_tables(c)
yield c
c.close()
def _delivery(**overrides: object) -> dict:
payload = {
"target_language": "zh",
"utterances": [
{"utterance_id": 1, "text": "飞船降落了。", "voiced": True},
{"utterance_id": 2, "text": "欢迎登船。", "voiced": True},
{"utterance_id": 3, "text": "", "voiced": False, "reason": "absorbed into utt-2"},
],
}
payload.update(overrides)
return payload
# --- ingest ---------------------------------------------------------------
def test_delivery_writes_one_row_per_utterance(conn: sqlite3.Connection) -> None:
report = tr.import_translations(conn, 1, _delivery())
assert report.accepted == 3
assert report.not_voiced == 1
rows = conn.execute(
"SELECT utterance_id, text, voiced, status FROM utterance_translations "
"ORDER BY utterance_id"
).fetchall()
assert len(rows) == 3
assert rows[0]["text"] == "飞船降落了。"
assert rows[0]["status"] == tr.STATUS_DRAFT
def test_a_collapsed_utterance_is_kept_with_its_reason(conn: sqlite3.Connection) -> None:
tr.import_translations(conn, 1, _delivery())
row = conn.execute(
"SELECT text, voiced, review_reason FROM utterance_translations WHERE utterance_id=3"
).fetchone()
assert row is not None
assert row["voiced"] == 0
assert "absorbed" in (row["review_reason"] or "")
# The row exists, so the gap is visible; it was not deleted.
assert conn.execute("SELECT COUNT(*) FROM utterances WHERE id=3").fetchone()[0] == 1
def test_confirmed_rows_are_not_overwritten(conn: sqlite3.Connection) -> None:
tr.import_translations(conn, 1, _delivery(), confirm=True)
report = tr.import_translations(conn, 1, _delivery())
assert report.skipped_confirmed == 3
row = conn.execute("SELECT text FROM utterance_translations WHERE utterance_id=1").fetchone()
assert row["text"] == "飞船降落了。"
# --- the locale-code bug --------------------------------------------------
def test_coverage_finds_a_delivery_stored_under_a_locale_code(
conn: sqlite3.Connection,
) -> None:
"""UmbraTrans writes ``zh-Hans``; the reviewer asks about ``zh``."""
tr.import_translations(conn, 1, _delivery(target_language="zh-Hans"))
assert (
conn.execute("SELECT DISTINCT language_code FROM utterance_translations").fetchone()[0]
== "zh"
)
report = tr.coverage(conn, "zh")
assert report["delivered"] == 3
assert report["voiced"] == 2
assert report["not_voiced"] == 1
def test_coverage_finds_a_row_written_with_a_script_subtag(
conn: sqlite3.Connection,
) -> None:
"""A row stored directly as ``zh-Hans`` must not read as "nothing"."""
conn.execute(
"INSERT INTO utterance_translations "
"(project_id, utterance_id, language_code, language_name, text, status, origin) "
"VALUES (1, 1, 'zh-Hans', 'Chinese', '飞船降落了。', 'draft', 'umbratrans')"
)
conn.commit()
report = tr.coverage(conn, "zh")
assert report["delivered"] == 1
assert report["missing"] == 4
def test_not_voiced_rows_are_found_under_a_locale_code(conn: sqlite3.Connection) -> None:
conn.execute(
"INSERT INTO utterance_translations "
"(project_id, utterance_id, language_code, language_name, text, status, origin, "
"voiced, review_reason) "
"VALUES (1, 3, 'zh-Hans', 'Chinese', '', 'draft', 'umbratrans', 0, 'absorbed into utt-2')"
)
conn.commit()
rows = tr.not_voiced_rows(conn, "zh")
assert len(rows) == 1
assert rows[0]["utterance_id"] == 3
assert rows[0]["chapter_id"] == 1
def test_coverage_of_an_undelivered_language_is_zero(conn: sqlite3.Connection) -> None:
report = tr.coverage(conn, "de")
assert report["delivered"] == 0
assert report["missing"] == 5
# --- replacing a delivery -------------------------------------------------
def test_replace_clears_drafts_and_keeps_confirmed(conn: sqlite3.Connection) -> None:
"""A re-delivery must not leave a stale row behind.
A confirmed translation is an approval, so it is never deleted - and the
import skips it, so the approved text survives a re-delivery.
"""
tr.import_translations(conn, 1, _delivery(), confirm=True)
conn.execute(
"INSERT INTO utterance_translations "
"(project_id, utterance_id, language_code, language_name, text, status, origin) "
"VALUES (1, 4, 'zh', 'Chinese', '陈旧的草稿。', 'draft', 'umbratrans')"
)
conn.commit()
removed = import_translations._clear_drafts(conn, "zh")
assert removed == 1
remaining = conn.execute(
"SELECT utterance_id, status FROM utterance_translations ORDER BY utterance_id"
).fetchall()
assert [row["utterance_id"] for row in remaining] == [1, 2, 3]
assert all(row["status"] == tr.STATUS_CONFIRMED for row in remaining)
def test_replace_matches_a_locale_code(conn: sqlite3.Connection) -> None:
conn.execute(
"INSERT INTO utterance_translations "
"(project_id, utterance_id, language_code, language_name, text, status, origin) "
"VALUES (1, 1, 'zh-Hans', 'Chinese', '飞船降落了。', 'draft', 'umbratrans')"
)
conn.commit()
assert import_translations._clear_drafts(conn, "zh") == 1
assert conn.execute("SELECT COUNT(*) FROM utterance_translations").fetchone()[0] == 0