Files
Jyotisha/tests/test_report_english_edition.py
T
Jesse_ChenandClaude Opus 5.5 419e1f4d37 feat(report): English reader edition from the same packet; yoga combination English twins (E1/E2/E3 wiring)
- /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
2026-09-29 19:26:00 +08:00

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"])