New reports request reader_main from upstream origin/main 23b9609e. KP and transit no longer copy an empty vars() dict, solar returns keep birth_asc_sign_idx, and ordinary projection deletes internal lines whole.
221 lines
7.7 KiB
Python
221 lines
7.7 KiB
Python
"""Reader-main export and the KP / Muntha defects behind BUG-1026/1027/1028."""
|
|
|
|
from __future__ import annotations
|
|
|
|
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"
|