- /api/professional_report_reference accepts languages [zh,en]; one packet, zh byte-identical, markdown_en + reader_dasha_applicability_en, or english_unavailable when a Chinese character would leak. - pl9_reader_english renders the parity volume with report_language=en and a fixed term table; yoga_engine carries combination_en / effects_en / strength_en. - 75 fictional charts: zero Han, same line count, same numbers outside yoga tables. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
184 lines
8.4 KiB
Python
184 lines
8.4 KiB
Python
"""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"])
|