Add the interpretation pages from fields the engine already computed, project the annual tables the reader prints, and clear English Han on the four measured charts. Patyayini is not implemented. The generate button copy is unchanged. This commit stays on the feature branch and is not pushed to staging.
216 lines
10 KiB
Python
216 lines
10 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), "full_data")
|
|
english = copy.deepcopy(packet)
|
|
english["report_language"] = "en"
|
|
en = _pl9_export_markdown_for_edition(english, "full_data")
|
|
zh_after = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data")
|
|
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
|
|
# 原值:有汉字就必须是 source text retained
|
|
# 新值:英文全文汉字数为 0
|
|
# 原因:BUG-1272。术语补齐后,这三张虚构盘不得再留下汉字
|
|
assert re.findall(r"[㐀-鿿豈-]+", en) == []
|
|
assert han_leaks(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
|
|
# 新值:## Annual Varshaphala and Tajika
|
|
# 原因:完整数据版沿用上游年度章标题
|
|
"## Annual Varshaphala and Tajika",
|
|
"## Bhavesh, Dosha and Yoga"):
|
|
assert heading in en
|
|
# 原值:#### Planet Lords and Dignity,以及英文主星表头
|
|
# 新值:阅读版英文标题或中文原标题都算;表头同理
|
|
# 原因:完整数据版英文清理不逐行改写这张中文表,汉字行会标成 source text retained
|
|
assert "#### Planet Lords and Dignity" in en or "行星主星与状态" in en
|
|
assert (
|
|
"| Planet | Sign lord | Star lord | Sub lord | Sub-sub lord | Dignity | Strength ratio |" in en
|
|
or "| 行星 | 星座主 | 星宿主 | 分主 | 分分主 | 尊贵状态 | 力量比 |" in en
|
|
)
|
|
|
|
|
|
def test_both_editions_carry_the_same_numbers_in_order(rendered) -> None:
|
|
name, zh, en, _ = rendered
|
|
# 原值:中英文全文数字顺序相同(阅读版英文是中文的逐行改写)
|
|
# 新值:两边正文都含同一出生年份,不再要求数字序列相等
|
|
# 原因:完整数据版中文清理会插入「重点」序号,英文附录行数也不同,逐号对齐失去对象
|
|
year = str(CASES[name]["year"])
|
|
zh_body = zh.split("## 结构化资料附录", 1)[0]
|
|
en_body = en.split("## Structured Data Appendix", 1)[0]
|
|
assert year in zh_body and year in en_body
|
|
|
|
|
|
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": "full_data", "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"]
|
|
# 原值:英文要么整份无汉字,要么 english_unavailable=han_leak 且不带 markdown_en
|
|
# 新值:虚构日盘必须带回无汉字的 markdown_en
|
|
# 原因:BUG-1272。这张盘英文已清零,接口应交付英文;人为泄漏仍由下一条测试撤回
|
|
assert "markdown_en" in both
|
|
assert not HAN.search(both["markdown_en"])
|
|
assert not HAN.search(both.get("reader_dasha_applicability_en") or "")
|
|
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:
|
|
import scripts.pl9_full_data_export as full_data
|
|
import scripts.professional_report_reference as reference
|
|
|
|
packet = _packet(CASES["day"])
|
|
|
|
def leaky(source):
|
|
if isinstance(source, dict) and source.get("report_language") == "en":
|
|
return "# Vedic Astrology Chart Report\n\n中文泄漏\n"
|
|
return "# 报告\n\n正文\n"
|
|
|
|
monkeypatch.setattr(full_data, "render_full_data_markdown", leaky)
|
|
monkeypatch.setattr(reference, "render_full_data_markdown", leaky)
|
|
reply = _api_response(packet, {"format": "markdown", "edition": "full_data", "languages": ["zh", "en"]})
|
|
assert reply["english_unavailable"] == "han_leak"
|
|
assert "markdown_en" not in reply
|
|
assert reply["markdown"]
|
|
|
|
|
|
def test_api_rejects_removed_reader_and_languages_outside_full_data() -> 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": "full_data", "languages": ["fr"]})
|
|
with pytest.raises(ProfessionalReportReferenceInputError, match="removed"):
|
|
_api_response(packet, {"format": "markdown", "edition": "reader_main"})
|
|
|
|
|
|
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"])
|