import asyncio import json import zipfile from pathlib import Path import pytest from bs4 import BeautifulSoup from ebooklib import epub from src.common.data_model import ManifestEntry, BookProfile, BookStructure from src.preprocessing.format_extractor import FormatExtractor from src.preprocessing.epub_cleaner import EpubCleaner from src.preprocessing.text_extractor import FineGrainedExtractor from src.assembly.format_restorer import FormatRestorer from src.assembly.restoration_engine import RestorationEngine from src.assembly.backfiller import BackfillEngine from src.assembly.builder import BilingualBuilder from src.translation.translator_engine import Translator from src.translation.glossary import BookGlossary, matches from src.translation.glossary import prepare_glossary from src.translation.manifest_manager import ManifestManager from src.translation.llm_client import LLMClient from src.qa.epub_audit import audit_epub, compare_audits def entry(html='
Masayoshi Son built a company.
'): _, text, mapping, _, _ = FormatExtractor().extract(html) return ManifestEntry(entry_id='Text/ch.xhtml#p1', element_id='p1', file_path='Text/ch.xhtml', original_text=text, placeholders=mapping) def test_glossary_model_conflicts_are_recorded_and_scan_resumes(tmp_path): class Scanner: model = 'test-model' def __init__(self, fail=False): self.calls = 0; self.fail = fail async def raw_chat_completion(self, *args): self.calls += 1 if self.fail and self.calls == 2: raise RuntimeError('network interrupted') return json.dumps({'terms': [ {'source': 'Masayoshi Son', 'target': '孙正义', 'aliases': ['Masa']}, {'source': 'Masa', 'target': '马萨', 'aliases': []}]}) entries = [entry(), entry('Masa continued.
')] path = tmp_path / 'glossary.json' with pytest.raises(RuntimeError): asyncio.run(prepare_glossary(entries, Scanner(True), path, chunk_chars=1)) saved = json.loads(path.read_text()) assert saved['next_chunk'] == 1 and not saved['scan_complete'] assert saved['terms'][0]['target'] == '孙正义' assert saved['conflicts'] resumed = Scanner() glossary = asyncio.run(prepare_glossary(entries, resumed, path, chunk_chars=1)) assert resumed.calls == 1 assert len(glossary.terms) == 1 assert json.loads(path.read_text())['scan_complete'] asyncio.run(prepare_glossary(entries, resumed, path, chunk_chars=1)) assert resumed.calls == 1 def test_zenmux_credentials_override_unrelated_openai_key(tmp_path, monkeypatch): from src.common.config import load_global_config config = tmp_path / 'zenmux.yaml' config.write_text('llm:\n base_url: https://zenmux.ai/api/v1\n') monkeypatch.setenv('ZENMUX_API_KEY', 'test-zenmux-key') monkeypatch.setenv('OPENAI_API_KEY', 'test-other-key') monkeypatch.delenv('OPENAI_BASE_URL', raising=False) assert load_global_config(config)['llm']['api_key'] == 'test-zenmux-key' def test_shared_surname_is_removed_from_both_distinct_entities(): from src.translation.glossary import merge_proposal terms = [{'source': 'Sun Microsystems', 'target': '太阳微系统公司', 'aliases': ['Sun']}] conflicts, ambiguous = [], set() terms = merge_proposal(terms, {'source': 'David Sun', 'target': '孙大卫', 'aliases': ['Sun']}, conflicts, ambiguous) assert len(terms) == 2 and ambiguous == {'sun'} assert all(not t['aliases'] for t in terms) terms = merge_proposal(terms, {'source': 'Sun Microsystems', 'target': '太阳微系统公司', 'aliases': ['Sun']}, conflicts, ambiguous) glossary = BookGlossary(terms) assert not glossary.validate('David Sun founded Kingston.', '孙大卫创办了金士顿。') assert not glossary.validate('Sun made an investment.', '孙先生作了投资。') assert glossary.validate('Sun Microsystems supplied computers.', '孙先生提供了电脑。') def test_acronyms_do_not_match_ordinary_pronouns(): glossary = BookGlossary([ {'source': 'World Health Organization', 'target': '世界卫生组织', 'aliases': ['WHO']}, {'source': 'United States', 'target': '美国', 'aliases': ['US']}, {'source': 'information technology', 'target': '信息技术', 'aliases': ['IT']}]) assert not glossary.validate('The people who helped us did it.', '帮助我们的人做了这件事。') assert not glossary.relevant([entry('Who helped us do it?
')]) assert glossary.validate('WHO met the US IT team.', '他们会见了团队。') assert matches('WHO', 'The WHO report') and not matches('WHO', 'people who helped') def test_name_aliases_do_not_match_common_lowercase_words(): glossary = BookGlossary([{'source': 'Steve Jobs', 'target': '乔布斯', 'aliases': ['Jobs']}, {'source': 'Arm Holdings', 'target': '安谋', 'aliases': ['Arm', 'ARM']}]) assert not glossary.validate('More jobs in its US arm.', '其美国分部增加就业。') assert glossary.validate('Jobs bought Arm.', '他收购了这家公司。') assert set(glossary.relevant([entry('Jobs invested in ARM.
')])) == {'Jobs', 'ARM'} def test_automatic_glossary_does_not_force_generic_senses_or_implicit_surnames(): from src.translation.glossary import sanitize_automatic_terms terms, excluded = sanitize_automatic_terms([ {'source': 'Otosan', 'target': '爸爸', 'aliases': ['Father']}, {'source': 'face', 'target': '面子'}, {'source': 'carried interest', 'target': '绩效分成', 'aliases': ['carry']}, {'source': 'World Health Organization', 'target': '世界卫生组织', 'aliases': ['WHO']}, {'source': 'Sun Microsystems', 'target': '太阳微系统公司', 'aliases': ['Sun']}, {'source': 'Sun Yafang', 'target': '孙亚芳'}], set()) assert excluded glossary = BookGlossary(terms) assert not glossary.validate('Father had a smile on his face and could carry a bag.', '父亲面带微笑,可以提包。') assert not glossary.validate('WHO AM I?', '我是谁?') assert not glossary.validate('Sun Yafang visited. Sun gave a talk.', '孙亚芳到访并发表演讲。') assert glossary.validate('carried interest', '费用') assert glossary.validate('Otosan', '广告') assert not BookGlossary([{'source': 'Fortune', 'target': '《财富》'}]).validate('Fortune', '据《财富》报道。') def test_auto_aliases_require_evidence_but_explicit_nicknames_remain(): from src.translation.glossary import sanitize_automatic_terms terms, _ = sanitize_automatic_terms([ {'source': 'The Wall Street Journal', 'target': '《华尔街日报》', 'aliases': ['the Journal']}, {'source': 'Theresa May', 'target': '特蕾莎·梅', 'aliases': ['May']}, {'source': 'University of California, Berkeley', 'target': '加州大学伯克利分校', 'aliases': ['Berkeley', 'UC Berkeley']}, {'source': 'Masayoshi Son', 'target': '孙正义', 'aliases': ['Masa'], 'notes': 'Masa is his nickname.'}], set()) glossary = BookGlossary(terms) assert not glossary.validate('In May he stayed at the Berkeley hotel.', '五月他住在伯克利酒店。') assert not glossary.validate('A domestic journal reported. The journal said.', '一家国内期刊报道。该期刊说。') assert glossary.validate('Masa studied at UC Berkeley.', '他去上学。') assert glossary.validate('Theresa May visited.', '五月来访。') @pytest.mark.parametrize('html', [ 'Hello world.
', 'The angle φ = 10, and φ1φ is literal mathematical text.
', 'A & B example.
', 'A[1] note.
', '日本語と中文 
text.
A formula.
', 'The first word.
', ]) def test_dom_roundtrip(html): e = entry(html) restored, ok = FormatRestorer().restore(e.original_text, e.placeholders, original_text=e.original_text) assert ok before, after = BeautifulSoup(html, 'html.parser').p, BeautifulSoup(''+restored+'
', 'html.parser').p assert before.get_text() == after.get_text() assert [(t.name, t.attrs) for t in before.find_all(True)] == [(t.name, t.attrs) for t in after.find_all(True)] @pytest.mark.parametrize('text', [ 'φ1φφ2φ中文φ/1φφ/2φ', # crossed 'φ1φφ2φ中文φ/2φφ/1φφ1φ', # duplicate 'φ1φ中文φ/1φ', # missing pair 'φ1φφ2φ中文φ/2φφ/1φφ99φ', # unknown 'φ1φφ2φ中文φ/2φφ/1φφbrokenφ', ]) def test_invalid_placeholders_never_restore(text): e = entry('Hello world
') assert FormatRestorer().restore(text, e.placeholders, original_text=e.original_text) == ('', False) def test_model_text_is_escaped(): result, ok = FormatRestorer().restore(' A & B', {}) assert ok and '