Validate on Linux Node 22 and PostgreSQL 17: 3894 frontend tests and 64 database tests pass, with no removed test names or new failures. Preserve static Home, bounded gzip, assertion-change records and manual acceptance gaps. Co-Authored-By: Claude Code <noreply@anthropic.com>
295 lines
11 KiB
Python
295 lines
11 KiB
Python
"""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
|