Files
Jyotisha/tests/test_pyjhora_annual_method_reference.py
T
jesse-uxandGrok (agent loop) 19379fbb41 合并 origin/staging,并补上两处测试替身。
研究章仍在六章之后、口径替换之前。独立年度方法参考仍在口径替换之后,来源说明不会被删掉。两条打回测试的替身补上额外参数,不改报告计算,不改原有断言。指纹记录改称 BUG-1430,因为 BUG-1429 已被研究章占用。

跑过两条打回测试、六章与指纹相关测试,以及 200 张英文扫描,退出码都是 0。

Co-Authored-By: Grok (agent loop)
2026-10-24 11:13:26 +08:00

386 lines
14 KiB
Python

"""Issue #93: PyJHora annual method reference stays in one child process."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
import pytest
from scripts.diagnose_pyjhora_adapter import build_product_report
from scripts.pl9_full_data_export import read_full_data_domain, render_full_data_markdown
from scripts.rectification_three_engine_packet import _pyjhora_d1
from scripts.report_render.annual_method_reference import (
CHAPTER_EN,
CHAPTER_ZH,
build_pyjhora_annual_method_reference,
normalize_worker_payload,
mask_report_fingerprint_lines,
render_annual_method_reference,
reset_reference_cache,
set_reference_runner,
strip_annual_method_chapter,
)
from tests.report_english_fictional import fictional_case
ROOT = Path(__file__).resolve().parents[1]
def setup_function() -> None:
reset_reference_cache()
set_reference_runner(None)
def test_result_hash_includes_the_annual_method_chapter() -> None:
# 原值: 两个标记的 result_hash 相同(本章被摘出指纹)。
# 新值: 两个标记的 result_hash 不同,且本章仍留在资料包里。
# 原因: 交付包里有这一章时,重算指纹必须对得上(BUG-1430;合并 staging 后 1429 已是研究章日出记录)。
from scripts.calculation_profile_contract import bind_result_to_profile
profile = {"input_hash": "ab" * 32}
def packet(marker: str) -> dict:
return {
"value": 1,
"worksheets": {
"timing_and_predictive_systems": {
"kept": "same",
"pyjhora_annual_method_reference": {"marker": marker},
}
},
}
installed = bind_result_to_profile(packet("installed"), profile)
blocked = bind_result_to_profile(packet("blocked"), profile)
assert installed["result_hash"] != blocked["result_hash"]
assert installed["worksheets"]["timing_and_predictive_systems"]["pyjhora_annual_method_reference"]["marker"] == "installed"
assert installed["worksheets"]["timing_and_predictive_systems"]["kept"] == "same"
def test_fingerprint_lines_match_after_masking_for_the_chapter_compare() -> None:
left = "\n".join([
"## 报告身份与来源",
"- 报告 ID: `full-report://one/aaaaaaaaaaaaaaaaaaaaaaaa`",
"- 计算结果摘要: `0123456789abcdef...`",
"## 前面的章",
"same",
f"## {CHAPTER_ZH}",
"only-in-left",
]) + "\n"
right = "\n".join([
"## 报告身份与来源",
"- 报告 ID: `full-report://two/bbbbbbbbbbbbbbbbbbbbbbbb`",
"- 计算结果摘要: `fedcba9876543210...`",
"## 前面的章",
"same",
f"## {CHAPTER_ZH}",
"only-in-right",
]) + "\n"
assert mask_report_fingerprint_lines(strip_annual_method_chapter(left)) == mask_report_fingerprint_lines(
strip_annual_method_chapter(right)
)
assert "aaaaaaaaaaaaaaaaaaaaaaaa" not in mask_report_fingerprint_lines(left)
assert "0123456789abcdef" not in mask_report_fingerprint_lines(left)
def test_reference_edition_does_not_request_the_annual_method_worker(monkeypatch) -> None:
from types import SimpleNamespace
from scripts.professional_report_reference import (
FULL_DATA_EDITION,
_assemble_professional_report_reference,
)
seen: list[bool] = []
def build(_reading, _args, _packs, include_annual_method_reference=True):
seen.append(include_annual_method_reference)
return {"report_version": "v", "worksheets": {}}
engine = SimpleNamespace(
build_professional_report_reference_packet=build,
render_pl9_markdown=lambda _packet: "standard",
)
handler = SimpleNamespace(_compute_full_reading_for_thematic=lambda _birth: {})
birth = fictional_case(0)
reference = _assemble_professional_report_reference(
handler, birth, "markdown", "reference", [], [], False, engine,
)
assert reference["markdown"] == "standard"
assert seen == [False]
monkeypatch.setattr(
"scripts.professional_report_reference.render_full_data_markdown",
lambda _packet: "full-data",
)
full = _assemble_professional_report_reference(
handler, birth, "markdown", FULL_DATA_EDITION, [], [], False, engine,
)
assert full["markdown"] == "full-data"
assert seen == [False, True]
def test_product_report_does_not_probe(monkeypatch) -> None:
def _boom(*_args, **_kwargs):
raise AssertionError("product diagnostics must not probe jhora")
monkeypatch.setattr("scripts.diagnose_pyjhora_adapter.importlib.util.find_spec", _boom)
report = build_product_report()
assert report["status"] == "missing_dependency"
assert report["dependency_available"] is False
assert report["missing_dependency"] == "jhora"
def test_pyjhora_d1_stays_on_the_missing_install_row() -> None:
source = (ROOT / "scripts" / "rectification_three_engine_packet.py").read_text(encoding="utf-8")
assert "import_module" not in source
assert "import jhora" not in source
with pytest.raises(ModuleNotFoundError):
_pyjhora_d1({"year": 1990})
def test_deploy_does_not_set_year_lord_pyjhora_reference() -> None:
hits = []
for path in (ROOT / "deploy").rglob("*"):
if not path.is_file():
continue
text = path.read_text(encoding="utf-8", errors="ignore")
if "JYOTISH_YEAR_LORD_PYJHORA_REFERENCE" in text:
hits.append(path.relative_to(ROOT).as_posix())
assert hits == []
def test_timeout_and_nonzero_exit_leave_every_table_unavailable() -> None:
def _timeout(_payload, timeout):
raise subprocess.TimeoutExpired(cmd="worker", timeout=timeout)
set_reference_runner(_timeout)
reference = build_pyjhora_annual_method_reference(_args(0), {"birth_info": {}})
assert _all_unavailable(reference)
def _exit(_payload, _timeout):
return {"ok": False, "reason": "exit_3"}
set_reference_runner(_exit)
reference = build_pyjhora_annual_method_reference(_args(0), {"birth_info": {}})
assert _all_unavailable(reference)
def test_normalize_keeps_nine_yogas_and_two_mudda_tables() -> None:
raw = {
"ok": True,
"ayanamsa_ok": True,
"ayanamsa_readback": "RAMAN",
"node_header": "mean",
"years": [{
"requested_year": 2026,
"anchor": {"date": [2026, 6, 15], "time": "16:35:04"},
"mudda_birth": [[[0, 1], [2026, 1, 2, 3.5], 30]],
"mudda_annual": [[[2, "L"], [2026, 6, 15, 1], 10]],
"patyayini": [
[[3, 4], [2026, 6, 15, 0], 0.4],
[[5, 6], [2026, 11, 1, 0], 0.6],
],
"tajika": {
"yogas": {name: [] for name in (
"ithasala", "eesarpha", "manahoo", "kamboola", "nakta", "yamaya",
)},
"failures": {"radda": "NameError"},
},
}],
}
raw["years"][0]["tajika"]["yogas"]["gairi_kamboola"] = None
raw["years"][0]["tajika"]["yogas"]["khallasara"] = None
second = json.loads(json.dumps(raw["years"][0]))
third = json.loads(json.dumps(raw["years"][0]))
second["requested_year"] = 2027
third["requested_year"] = 2028
raw["years"].extend([second, third])
reference = normalize_worker_payload(raw, 2026)
tables = reference["years"][0]["tables"]
assert tables["mudda_birth"]["rows"][0]["main_lord"] == "Sun"
assert tables["mudda_annual"]["rows"][0]["sub_lord"] == "Lagna"
assert tables["patyayini"]["rows"][0]["end"] == "2026-11-01 00:00:00"
assert tables["patyayini"]["rows"][1]["end"] is None
states = {row["yoga"]: row["state"] for row in tables["tajika"]["rows"]}
assert len(states) == 9
assert states["Radda"] == "unavailable"
assert states["Gairi-Kamboola"] == "unknown"
assert states["Ithasala"] == "empty"
def test_chapter_is_last_and_domain_view_omits_the_key() -> None:
packet = {
"report_language": "zh",
"worksheets": {"timing_and_predictive_systems": {"pyjhora_annual_method_reference": {
"years": [_shell(2026), _shell(2027), _shell(2028)],
}}},
}
chinese = render_full_data_markdown(packet)
english_packet = dict(packet)
english_packet["report_language"] = "en"
english = render_full_data_markdown(english_packet)
assert chinese.rstrip().splitlines()[-1] != ""
assert _h2(chinese)[-1] == CHAPTER_ZH
assert _h2(english)[-1] == CHAPTER_EN
assert chinese.count("来源:PyJHora") == 3
assert english.count("Source: PyJHora") == 3
assert "未获得结果" in chinese and "Result unavailable" in english
assert strip_annual_method_chapter(chinese) == strip_annual_method_chapter(strip_annual_method_chapter(chinese))
assert CHAPTER_ZH not in strip_annual_method_chapter(chinese)
domain = read_full_data_domain(packet, "career")
timing = domain["worksheets"]["timing_and_predictive_systems"]
assert "pyjhora_annual_method_reference" not in timing
def test_three_fictional_charts_match_a_second_annual_chart_call() -> None:
"""Real PyJHora, three fictional charts. Birth fields stay out of the assertion text."""
script = r"""
import json, sys
sys.path[:0] = [sys.argv[1], sys.argv[1] + "\\scripts"]
from tests.report_english_fictional import fictional_case
from scripts.report_render.annual_method_reference import _invoke_worker, _payload_from
class Args:
pass
out = []
for index in (0, 1, 9):
birth = fictional_case(index)
args = Args()
for key, value in birth.items():
setattr(args, key, value)
payload = _payload_from(args, {})
if index == 0:
payload["prove_reuse"] = True
payload["include_upstream_diff"] = True
raw = _invoke_worker(payload, 120)
years = raw.get("years") or []
diff = (years[0].get("upstream_diff") if years else None) or {}
out.append({
"i": index,
"ok": bool(raw.get("ok")),
"ayanamsa_ok": bool(raw.get("ayanamsa_ok")),
"readback": raw.get("ayanamsa_readback"),
"node": raw.get("node_header"),
"reuse": [bool(year.get("reuse_matches")) for year in years],
"documented_anchor": (diff.get("documented_anchor") or {}).get("date"),
"upstream_anchor": (diff.get("upstream_anchor") or {}).get("date"),
"documented_mudda": diff.get("documented_mudda_start"),
"upstream_mudda": diff.get("upstream_mudda_start"),
"yoga_keys": sorted(((years[0].get("tajika") or {}).get("yogas") or {}).keys()) if years else [],
"radda": ((years[0].get("tajika") or {}).get("failures") or {}).get("radda"),
"jhora_in_parent": any(name == "jhora" or name.startswith("jhora.") for name in sys.modules),
})
print(json.dumps(out, ensure_ascii=False))
"""
completed = subprocess.run(
[sys.executable, "-c", script, str(ROOT)],
cwd=ROOT,
text=True,
capture_output=True,
timeout=400,
check=False,
)
assert completed.returncode == 0, completed.stderr[-1000:]
rows = json.loads(completed.stdout)
assert [row["i"] for row in rows] == [0, 1, 9]
for row in rows:
assert row["ok"] and row["ayanamsa_ok"], row
assert row["readback"]
if row["i"] == 0:
assert row["reuse"] == [True, True, True]
assert row["documented_anchor"] and row["upstream_anchor"]
assert row["jhora_in_parent"] is False
assert "radda" not in row["yoga_keys"]
assert row["radda"]
def test_parent_moon_and_ayanamsa_stay_put() -> None:
script = r"""
import json, sys
sys.path[:0] = [sys.argv[1], sys.argv[1] + "\\scripts"]
import swisseph as swe
from tests.report_english_fictional import fictional_case
from scripts.report_render.annual_method_reference import build_pyjhora_annual_method_reference
birth = fictional_case(0)
class Args:
pass
args = Args()
for key, value in birth.items():
setattr(args, key, value)
jd = swe.julday(2000, 1, 1, 0)
before_ay = swe.get_ayanamsa_ut(jd)
before_moon = swe.calc_ut(jd, swe.MOON)[0][0]
reference = build_pyjhora_annual_method_reference(args, {})
after_ay = swe.get_ayanamsa_ut(jd)
after_moon = swe.calc_ut(jd, swe.MOON)[0][0]
print(json.dumps({
"same_ayanamsa": before_ay == after_ay,
"same_moon": before_moon == after_moon,
"jhora": any(name == "jhora" or name.startswith("jhora.") for name in sys.modules),
"years": len(reference.get("years") or []),
"tables": sorted((reference.get("years") or [{}])[0].get("tables", {})),
}))
"""
completed = subprocess.run(
[sys.executable, "-c", script, str(ROOT)],
cwd=ROOT,
text=True,
capture_output=True,
timeout=90,
check=False,
)
assert completed.returncode == 0, completed.stderr[-1000:]
payload = json.loads(completed.stdout.strip().splitlines()[-1])
assert payload["same_ayanamsa"] and payload["same_moon"]
assert payload["jhora"] is False
assert payload["years"] == 3
assert payload["tables"] == ["mudda_annual", "mudda_birth", "patyayini", "tajika"]
def _args(index: int):
birth = fictional_case(index)
class Args:
pass
args = Args()
for key, value in birth.items():
setattr(args, key, value)
return args
def _shell(year: int) -> dict:
return {
"requested_year": year,
"ayanamsa": "RAMAN",
"ayanamsa_ok": False,
"node_header": "mean",
"patyayini_anchor": None,
"tajika_anchor": None,
"tables": {name: {"ok": False, "rows": []} for name in ("mudda_birth", "mudda_annual", "patyayini", "tajika")},
}
def _all_unavailable(reference: dict) -> bool:
years = reference.get("years") or []
return len(years) == 3 and all(
all(not year["tables"][name]["ok"] for name in year["tables"])
for year in years
)
def _h2(markdown: str) -> list[str]:
return [line[3:].strip() for line in markdown.splitlines() if line.startswith("## ")]
def test_rendered_english_chapter_has_no_han() -> None:
text = render_annual_method_reference({
"worksheets": {"timing_and_predictive_systems": {"pyjhora_annual_method_reference": {
"years": [_shell(2026)],
}}},
}, "en")
assert "Source: PyJHora" in text
assert not any("\u4e00" <= char <= "\u9fff" for char in text)