"""English edition of the reader report (TASK-report-english-edition-20260929, E1/E2). Three clearly fictional charts (day, night, southern hemisphere). For each, the Chinese reader is rendered from the same packet before and after the English render; the English reader must carry no Chinese, pass the reader leak patterns, and carry the same numbers in the same order outside the yoga tables (yoga names and combination notes are prose whose English forms number things the Chinese names do not, e.g. "Kalatra Dosha (7th Lord in 6/8/12)"). """ from __future__ import annotations import copy import hashlib import json import re from pathlib import Path import pytest from scripts.jyotish_engine import build_professional_report_reference_packet, cmd_full_reading from scripts.pl9_reader_english import HAN, han_leaks from scripts.pl9_reader_export import _dignity_label, _pl9_export_markdown_for_edition from scripts.professional_report_reference import ( ProfessionalReportReferenceInputError, _export_args, build_professional_report_reference, ) from scripts.yoga_engine import BiText, _bijoin, _combo, _planet from tests.test_report_reader_main import FICTIONAL, _leak_hits ROOT = Path(__file__).resolve().parents[1] NUMBER = re.compile(r"-?\d+(?:\.\d+)?") CASES = { "day": dict(FICTIONAL), # Clearly fictional night birth, northern China-like coordinates. "night": {**FICTIONAL, "name": "虚构乙", "year": 1984, "month": 11, "day": 23, "hour": 23, "minute": 40, "lat": 39.9042, "lon": 116.4074, "tz": 8.0, "age": 42}, # Clearly fictional southern-hemisphere birth. "south": {**FICTIONAL, "name": "虚构丙", "year": 1977, "month": 2, "day": 14, "hour": 6, "minute": 5, "lat": -33.8688, "lon": 151.2093, "tz": 11.0, "age": 49}, } def _packet(case: dict) -> dict: full_reading = cmd_full_reading(type("Args", (), dict(case))()) packet = dict(build_professional_report_reference_packet(full_reading, _export_args(dict(case)), [])) packet["report_version"] = "pl9_personal_long_report.v3" return packet @pytest.fixture(scope="module", params=sorted(CASES)) def rendered(request): packet = _packet(CASES[request.param]) zh_before = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main") english = copy.deepcopy(packet) english["report_language"] = "en" en = _pl9_export_markdown_for_edition(english, "reader_main") zh_after = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main") return request.param, zh_before, en, zh_after def _numbers_outside_yoga_tables(markdown: str) -> list[str]: numbers: list[str] = [] in_yoga_table = False for line in markdown.splitlines(): if line.startswith("| Yoga | Category |") or line.startswith("| 瑜伽 | 类别 |"): in_yoga_table = True continue if in_yoga_table and not line.startswith("|"): in_yoga_table = False if not in_yoga_table: numbers.extend(NUMBER.findall(line)) return numbers def test_chinese_edition_is_untouched_by_the_english_render(rendered) -> None: _, zh_before, _, zh_after = rendered assert zh_after == zh_before def test_english_edition_has_no_chinese(rendered) -> None: _, _, en, _ = rendered assert han_leaks(en) == [] assert not HAN.search(en) def test_english_edition_passes_reader_leak_patterns(rendered) -> None: _, _, en, _ = rendered assert _leak_hits(en) == [] assert en.startswith("# Vedic Astrology Chart Report\n") for heading in ("## Birth Data and Charts", "## Strength, Relationships and Ashtakavarga", "## Standard Dasha Tables", "## Saturn and KP", "## Annual Charts and Tajika", "## Bhavesh, Dosha and Yoga", "#### Planet Lords and Dignity"): assert heading in en assert "| Planet | Sign lord | Star lord | Sub lord | Sub-sub lord | Dignity | Strength ratio |" in en def test_both_editions_carry_the_same_numbers_in_order(rendered) -> None: _, zh, en, _ = rendered assert _numbers_outside_yoga_tables(en) == _numbers_outside_yoga_tables(zh) # Line for line, too: the English edition is the same volume, relabelled. assert len(en.splitlines()) == len(zh.splitlines()) def test_english_dignity_takes_the_english_half() -> None: assert _dignity_label("入旺(Exalted)", "en") == "Exalted" assert _dignity_label("落陷取消(Neecha Bhanga)", "en") == "Neecha Bhanga" assert _dignity_label("入旺(Exalted)") == "入旺" def test_yoga_combination_text_carries_an_english_twin() -> None: combo = _combo({}, "{a}与{b}同在第{h}宫", a=_planet("Sun"), b=_planet("Mercury"), h=10) assert combo == "太阳Sun与水星Mercury同在第10宫" assert isinstance(combo, BiText) and combo.en == "Sun and Mercury together in house 10" own = _combo({"combo_template": "{p}入庙", "combo_template_en": "{p} in own sign"}, "{p}在第{h}宫", p=_planet("Mars")) assert (str(own), own.en) == ("火星Mars入庙", "Mars in own sign") joined = _bijoin("; ", [combo, "", own]) assert str(joined) == "太阳Sun与水星Mercury同在第10宫; 火星Mars入庙" assert joined.en == "Sun and Mercury together in house 10; Mars in own sign" def _api_response(packet: dict, body: dict) -> dict: class Handler: def _high_rigor_birth_payload(self, request): return {**FICTIONAL} def _compute_full_reading_for_thematic(self, birth): return {"stub": True} class Engine: def build_professional_report_reference_packet(self, full_reading, args, packs): return copy.deepcopy(packet) return build_professional_report_reference(Handler(), body, engine=Engine()) def test_api_without_languages_is_unchanged_and_with_languages_adds_english() -> None: packet = _packet(CASES["day"]) base_body = {"format": "markdown", "edition": "reader_main", "include_fact_tables": True} plain = _api_response(packet, base_body) assert not {"markdown_en", "reader_dasha_applicability_en", "english_unavailable"} & set(plain) both = _api_response(packet, {**base_body, "languages": ["zh", "en"]}) assert both["markdown"] == plain["markdown"] assert both["fact_table_packet"] == plain["fact_table_packet"] assert both["reader_dasha_applicability"] == plain["reader_dasha_applicability"] assert not HAN.search(both["markdown_en"]) assert not HAN.search(both["reader_dasha_applicability_en"]) assert "english_unavailable" not in both zh_only = _api_response(packet, {**base_body, "languages": ["zh"]}) assert json.dumps(zh_only, sort_keys=True, ensure_ascii=False) == json.dumps(plain, sort_keys=True, ensure_ascii=False) def test_api_drops_english_when_chinese_would_leak(monkeypatch) -> None: from scripts import pl9_reader_english packet = _packet(CASES["day"]) monkeypatch.setattr(pl9_reader_english, "PHRASES_EN", []) reply = _api_response(packet, {"format": "markdown", "edition": "reader_main", "languages": ["zh", "en"]}) assert reply["english_unavailable"] == "han_leak" assert "markdown_en" not in reply assert reply["markdown"] def test_api_rejects_languages_outside_reader_main() -> None: packet = {"report_version": "x"} with pytest.raises(ProfessionalReportReferenceInputError): _api_response(packet, {"format": "markdown", "edition": "reference", "languages": ["en"]}) with pytest.raises(ProfessionalReportReferenceInputError): _api_response(packet, {"format": "markdown", "edition": "reader_main", "languages": ["fr"]}) def test_english_golden_pair_matches_a_live_render() -> None: """The frontend fixtures are a real zh/en pair from one packet (AGENTS §7-4).""" en_fixture = json.loads((ROOT / "frontend/tests/fixtures/report-reader-main-fictional-en.json").read_text(encoding="utf-8")) zh_fixture = json.loads((ROOT / "frontend/tests/fixtures/report-reader-main-fictional-zh-pair.json").read_text(encoding="utf-8")) assert en_fixture["fixtureProvenance"]["fictional"] is True assert en_fixture["fixtureProvenance"]["language"] == "en" assert zh_fixture["fixtureProvenance"]["language"] == "zh" assert en_fixture["fixtureProvenance"]["pairSha256"] == hashlib.sha256(zh_fixture["markdown"].encode("utf-8")).hexdigest() assert not HAN.search(en_fixture["markdown"]) assert _numbers_outside_yoga_tables(en_fixture["markdown"]) == _numbers_outside_yoga_tables(zh_fixture["markdown"])