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.
211 lines
7.6 KiB
Python
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
|