Files
Jyotisha/tests/test_report_reader_main.py
T

319 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Reader-main export and the KP / Muntha defects behind BUG-1026/1027/1028."""
from __future__ import annotations
import copy
import re
import pytest
from scripts.annual_tajika_pack import _field_brief, _field_with_conflict_gate
from scripts.jyotish_engine import (
_copy_call_namespace,
_professional_report_reference_boundary_notice,
cmd_full_reading,
render_pl9_markdown,
)
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
from scripts.professional_report_reference import (
REPORT_VERSION_READER,
build_professional_report_reference,
)
from scripts.shared_full_report_authority import DEFAULT_REPORT_VERSION
from scripts.solar_return import calc_solar_return_chart, solar_return_full_report
# Clearly fictional. Not a user, not a published celebrity case.
FICTIONAL = {
"name": "虚构甲",
"year": 1991,
"month": 4,
"day": 7,
"hour": 9,
"minute": 15,
"second": 0,
"lat": 30.5728,
"lon": 104.0668,
"tz": 8.0,
"node_mode": "mean",
"ayanamsa": "lahiri",
"today": "2026-04-07",
"transit_date": "2026-04-07",
"target_year": 2026,
"age": 35,
"birth_time_accuracy": "confirmed",
}
LEAK_PATTERNS = (
re.compile(r"\bblocked\b", re.I),
re.compile(r"\bexecuted\b", re.I),
re.compile(r"\bpartial_verified\b", re.I),
re.compile(r"\bmissing_in_local\b", re.I),
re.compile(r"\bproducer\b", re.I),
re.compile(r"\bparity\b", re.I),
re.compile(r"PL9\s*第\s*\d+\s*页"),
re.compile(r"full-report://"),
re.compile(r"\bpl9_[A-Za-z0-9_]+\b", re.I),
re.compile(r"\b[A-Za-z][A-Za-z0-9]*_unavailable\b"),
re.compile(r"\b[A-Za-z][A-Za-z0-9]*_(?:pack|packet)\b", re.I),
re.compile(r"object has no attribute"),
re.compile(r"\bTraceback\b"),
re.compile(r"结论等级规则"),
re.compile(r"This reference export"),
re.compile(r"producer failed"),
re.compile(r"PyJHora"),
re.compile(r"\bparameter_sensitive\b"),
re.compile(r"\bschema\b", re.I),
re.compile(r"\bsource_path\b"),
re.compile(r"\{planets\}"),
)
CORE_CHAPTERS = (
"出生资料",
"Ashtakavarga",
"标准大运",
"KP",
"Year Lord",
"Tajika",
"Muntha",
)
def _class_args(**overrides):
fields = {**FICTIONAL, **overrides}
return type("Args", (), fields)()
def _leak_hits(markdown: str) -> list[str]:
return [pattern.pattern for pattern in LEAK_PATTERNS if pattern.search(markdown)]
@pytest.fixture(scope="module")
def full_reading():
return cmd_full_reading(_class_args())
def test_default_edition_stays_reference_and_reader_requests_v3() -> None:
assert DEFAULT_REPORT_VERSION == "pl9_personal_long_report.v3"
assert REPORT_VERSION_READER == "pl9_personal_long_report.v3"
class Handler:
def _high_rigor_birth_payload(self, body):
return dict(FICTIONAL)
def _compute_full_reading_for_thematic(self, body):
return {"modules": {"chart": {}}}
class Engine:
def build_professional_report_reference_packet(self, full_reading, args, packs):
return {"schema": "pl9_style_professional_export_v1", "selected_report_pack_ids": packs}
def render_pl9_markdown(self, packet):
return "reference-body"
handler = Handler()
engine = Engine()
default = build_professional_report_reference(handler, {"format": "markdown"}, engine=engine)
assert default["edition"] == "reference"
assert default["markdown"] == "reference-body"
reader = build_professional_report_reference(
handler,
{"format": "markdown", "edition": "reader_main"},
engine=engine,
)
assert reader["edition"] == "reader_main"
assert reader["report_version"] == "pl9_personal_long_report.v3"
def test_reference_body_drops_local_leak_phrases() -> None:
packet = {
"schema": "pl9_style_professional_export_v1",
"birth_info": {"name": "虚构甲"},
"reader_engine_boundary_notice": _professional_report_reference_boundary_notice(),
"worksheets": {},
}
markdown = render_pl9_markdown(packet)
assert "结论等级规则" not in markdown
assert "This reference export" not in markdown
assert "producer failed" not in markdown
assert "多引擎口径说明" in markdown
def test_class_args_copy_keeps_year_for_kp_and_transit(full_reading) -> None:
args = _class_args()
copied = _copy_call_namespace(args)
assert copied.year == 1991
assert vars(args) == {}
modules = full_reading["modules"]
for name in ("kp", "double_transit_pac", "transit_ll7l", "planetary_congregation"):
module = modules.get(name)
assert isinstance(module, dict), name
blob = repr(module)
assert "has no attribute" not in blob
assert module.get("status") != "blocked", (name, module.get("status"), module.get("reason"))
assert "error" not in module or not str(module.get("error", "")).startswith("KP producer failed")
def test_reader_main_hides_audit_vocabulary(full_reading) -> None:
from scripts.jyotish_engine import build_professional_report_reference_packet
packet = build_professional_report_reference_packet(full_reading, _class_args(), ["full"])
reference = _pl9_export_markdown_for_edition(packet, "reference")
reader = _pl9_export_markdown_for_edition(packet, "reader_main")
assert "结论等级规则" not in reference
assert "This reference export" not in reference
hits = _leak_hits(reader)
assert hits == [], hits
for chapter in CORE_CHAPTERS:
assert chapter in reader, chapter
assert reader.strip().startswith("#")
def test_solar_return_restores_birth_asc_and_year_lord_moves() -> None:
lords = set()
signs = set()
for year in (2024, 2026, 2028):
chart = calc_solar_return_chart(
FICTIONAL["year"], FICTIONAL["month"], FICTIONAL["day"],
FICTIONAL["hour"], FICTIONAL["minute"],
FICTIONAL["lat"], FICTIONAL["lon"], FICTIONAL["tz"],
year,
ayanamsa_name="lahiri",
)
assert "error" not in chart, chart.get("error")
assert isinstance(chart.get("birth_asc_sign_idx"), int)
report = solar_return_full_report(
FICTIONAL["year"], FICTIONAL["month"], FICTIONAL["day"],
FICTIONAL["hour"], FICTIONAL["minute"],
FICTIONAL["lat"], FICTIONAL["lon"], FICTIONAL["tz"],
year,
ayanamsa_name="lahiri",
)
muntha = report["muntha"]
assert muntha.get("status") != "blocked", muntha
assert isinstance(muntha.get("muntha_sign_idx"), int)
year_lord = report["year_lord"]
assert year_lord.get("status") != "blocked", year_lord
lords.add(year_lord["year_lord"])
signs.add(muntha["muntha_sign"])
assert len(signs) >= 2
assert len(lords) >= 2
def test_blocked_side_does_not_invent_a_conflict() -> None:
gated = _field_with_conflict_gate(
"muntha",
{"muntha": {"status": "blocked", "reason": "natal_ascendant_required_for_muntha"}},
{"muntha": {"muntha_sign": "Capricorn", "muntha_lord": "Saturn", "note": "extra"}},
)
assert gated["status"] == "partial_verified"
assert "conflict" not in _field_brief("Muntha", gated)
same_sign = _field_with_conflict_gate(
"year_lord",
{"year_lord": {"year_lord": "Mars", "year_lord_sign": "Aries", "trace": "left"}},
{"year_lord": {"year_lord": "Mars", "year_lord_sign": "Aries", "trace": "right"}},
)
assert same_sign["status"] == "partial_verified"
disagree = _field_with_conflict_gate(
"year_lord",
{"year_lord": {"year_lord": "Mars", "year_lord_sign": "Aries"}},
{"year_lord": {"year_lord": "Venus", "year_lord_sign": "Taurus"}},
)
assert disagree["status"] == "conflict"
@pytest.fixture(scope="module")
def reader_packet(full_reading):
from scripts.jyotish_engine import build_professional_report_reference_packet
return build_professional_report_reference_packet(full_reading, _class_args(), ["full"])
def test_real_annual_producers_agree_without_identical_metadata(reader_packet) -> None:
years = reader_packet["worksheets"]["timing_and_predictive_systems"]["annual_tajika_series"]["years"]
for annual in years.values():
field = annual["year_lord"]
assert field["status"] == "partial_verified"
left, right = [item["value"] for item in field["producers_checked"]]
assert "year_lord_sign" in left
assert "year_lord_sign" not in right
assert left["year_lord"] == right["year_lord"]
assert field["data"] == left
sections = annual["report_sections"]
assert "冲突" not in repr(sections["executive_summary"]["summary_lines"])
assert "分歧" not in repr(sections["thematic_narrative"]["highlights"])
assert "外部验证已经完成" in repr(sections["thematic_narrative"]["highlights"])
def test_reader_annual_cells_use_real_distinct_years_and_keep_return_dates(reader_packet) -> None:
reader = _pl9_export_markdown_for_edition(reader_packet, "reader_main")
lords = re.findall(r"^\| Varshesha \| ([^|]+) \|$", reader, re.M)
signs = re.findall(r"^\| Muntha sign \| ([^|]+) \|$", reader, re.M)
assert len(lords) == len(signs) == 3
assert "-" not in lords + signs
assert len(set(lords)) >= 2
assert len(set(signs)) >= 2
assert "| Selection basis | Muntha 所在星座的守护星(当前本地口径) |" in reader
assert "| Annual ascendant sign | - |" not in reader
timing = reader_packet["worksheets"]["timing_and_predictive_systems"]
for year in ("2027", "2028"):
annual = timing["annual_tajika_series"]["years"][year]
return_time = annual["solar_return"]["data"]["dt_local"]
section = reader.split(f"### {year} 年度 Varshaphala / Tajika 细项", 1)[1].split("### ", 1)[0]
assert f"| Varshapravesha | {return_time} |" in section
assert _leak_hits(reader) == []
def test_reader_annual_conflict_keeps_both_real_producer_values(reader_packet) -> None:
packet = copy.deepcopy(reader_packet)
timing = packet["worksheets"]["timing_and_predictive_systems"]
annual = timing["annual_tajika_pack"]
current = annual["year_lord"]["data"]
later = timing["annual_tajika_series"]["years"]["2027"]["year_lord"]["data"]
annual["year_lord"] = _field_with_conflict_gate(
"year_lord", {"year_lord": current}, {"year_lord": later},
)
assert annual["year_lord"]["status"] == "conflict"
reader = _pl9_export_markdown_for_edition(packet, "reader_main")
assert "| Varshesha | 太阳返照:火星;Tajika:金星(两者不同) |" in reader
assert "两种计算口径给出的年主不同,暂不合并" in reader
assert "| Muntha sign | 白羊座 |" in reader
assert _leak_hits(reader) == []
def test_reader_omits_missing_name_and_panchanga_machine_keys(reader_packet) -> None:
packet = copy.deepcopy(reader_packet)
packet["birth_info"].pop("name", None)
packet["birth_info"].pop("user_name", None)
reader = _pl9_export_markdown_for_edition(packet, "reader_main")
assert "- 姓名:" not in reader
assert "overall_score" not in reader
assert "吉性_count" not in reader
assert "total_elements" not in reader
assert "| 综合性质 |" in reader
assert "| 吉性要素数量 |" in reader
assert "| 要素总数 |" in reader
def test_reader_headings_drop_field_jargon_and_dignity_is_one_name(reader_packet) -> None:
"""BUG-1092 / BUG-1093: no 「字段」 in what a reader sees, readable planet-lord
headers, and the bilingual engine dignity label printed once, not 「入旺(入旺)」."""
from scripts.pl9_reader_export import _dignity_label
reader = _pl9_export_markdown_for_edition(reader_packet, "reader_main")
assert "字段" not in reader
assert "#### 行星主星与状态" in reader
assert "| 行星 | 星座主 | 星宿主 | 分主 | 分分主 | 尊贵状态 | 力量比 |" in reader
assert "| Planet | RL | NL | SL | SS |" not in reader
assert "力量比:六维力量与所需力量之比" in reader
table = reader.split("#### 行星主星与状态", 1)[1].split("\n\n", 2)[1]
for row in table.splitlines()[2:]:
dignity = row.split("|")[6].strip()
assert "(" not in dignity and "(" not in dignity, row
assert not re.search(r"([一-鿿]+)\(\1\)", reader)
assert _dignity_label("入旺(Exalted)") == "入旺"
assert _dignity_label("落陷取消(Neecha Bhanga)") == "落陷取消"
assert _dignity_label("极敌(Great Enemy)") == "极敌"
assert _dignity_label("中性") == "中性"
assert _dignity_label(None) == ""
assert _leak_hits(reader) == []