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'} @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 '