Files
Jyotisha/tests/test_double_transit_d9_targets.py
T
Jesse_ChenandClaude Opus 5.5 0176cb3008 test: three-column updates for the ported transit and Shadbala fixes (BUG-1185, BUG-1188)
- tests/fixtures/double_transit_d1_cl_golden.json recaptured with the test's
  own --capture (PYTHONHASHSEED=0, chart cache TTL 0): 413 of 578 leaf entries
  and all 36 chart-house stats rows move (double-transit overlaps: D1 0 -> 15, CL 0 -> 6) because aspects are now counted
  from 1 (42d1582f).
- V.P. Jain benchmark: within tolerance 34 -> 36, variants 8 -> 6; Chesta
  Mars/Jupiter/Saturn now match the published values, Venus no longer does
  (08376cf0). Original / new / reason recorded in the test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-02 14:09:39 +08:00

179 lines
7.4 KiB
Python

"""BUG-1060: Double Transit PAC D9 targets check the D9 signs their labels name.
Real engine runs on the three public AA charts of the evidence-card research
(`consult_evidence_card_lib.load_public_charts`, raman ayanamsa, mean nodes,
reference date fixed in the library). The D1 and Chandra Lagna layers are
compared with a golden captured from the real engine. First captured before
the BUG-1060 fix (base 5671039a; that fix only touched the D9 layer);
recaptured 2026-10-02 after BUG-1185 (TASK-upstream-sync4-20261002 T2: the
aspect check counts houses from 1, so D1 / CL aspect rows moved).
Regenerate the golden (only when the D1 / CL layer is meant to change):
PYTHONHASHSEED=0 python tests/test_double_transit_d9_targets.py --capture
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[1]
for path in (ROOT, ROOT / "scripts", ROOT / "scripts" / "research"):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
import jyotish_engine as engine # noqa: E402
from consult_evidence_card_lib import load_public_charts # noqa: E402
GOLDEN = ROOT / "tests" / "fixtures" / "double_transit_d1_cl_golden.json"
HOUSES = range(1, 13)
def _args(chart: dict, house: int) -> SimpleNamespace:
body = chart["body"]
return SimpleNamespace(
year=body["year"], month=body["month"], day=body["day"], hour=body["hour"], minute=body["minute"],
second=0, lat=body["lat"], lon=body["lon"], tz=float(body["tz"]), node_mode=body["node_mode"],
ayanamsa=body["ayanamsa"], date=body["today"], house=house,
)
def _natal(chart: dict):
natal_chart, asc_idx, _, _ = engine._compute_chart_from_args(_args(chart, 7))
ascendant = natal_chart["ascendant"]
asc_deg = ascendant.get("lon", ascendant.get("degree", 0))
return natal_chart["planets"], asc_idx, asc_deg
def _d9_targets(chart: dict, house: int):
natal, asc_idx, asc_deg = _natal(chart)
event_lord = engine.SIGN_LORDS[engine.SIGNS[(asc_idx + house - 1) % 12]]
ll_name = engine.SIGN_LORDS[engine.SIGNS[asc_idx]]
return engine._double_transit_d9_targets(asc_deg, natal, house, event_lord, ll_name), natal, asc_deg
def _d1_cl_layers() -> dict:
out = {}
for chart in load_public_charts(ROOT):
for house in HOUSES:
result = engine.cmd_double_transit_pac(_args(chart, house))
stats = result["stats"]
out[f"{chart['id']}|{house}"] = {
"d1": result["d1"],
"cl": result["cl"],
"d1_cl_double_transit": [row for row in result["double_transit"] if row["layer"] in ("D1", "CL")],
"stats": {key: value for key, value in stats.items() if key.startswith(("d1_", "cl_")) or key == "chandra_lagna"},
}
return out
@pytest.fixture(scope="module")
def charts() -> list[dict]:
return load_public_charts(ROOT)
def test_quick_gate_runs_this_file() -> None:
import run_quality_gate
assert "tests/test_double_transit_d9_targets.py" in run_quality_gate.CORE_PYTEST_TARGETS
def test_d9_house_target_is_the_d9_house_sign_not_the_d9_lagna(charts) -> None:
for chart in charts:
for house in range(2, 13):
(d9_asc_idx, targets), _, _ = _d9_targets(chart, house)
house_si = (d9_asc_idx + house - 1) % 12
label = f"D9_{house}宫({engine.SIGNS[house_si]})"
assert targets[label] == house_si * 30 + 15, (chart["id"], house)
assert targets[label] != d9_asc_idx * 30 + 15, (chart["id"], house)
def test_d9_house_1_target_is_still_the_d9_lagna(charts) -> None:
for chart in charts:
(d9_asc_idx, targets), _, asc_deg = _d9_targets(chart, 1)
assert d9_asc_idx == engine._navamsa_idx(asc_deg)
assert targets[f"D9_1宫({engine.SIGNS[d9_asc_idx]})"] == d9_asc_idx * 30 + 15
def test_d9_lord_targets_are_navamsa_signs(charts) -> None:
for chart in charts:
for house in HOUSES:
(d9_asc_idx, targets), natal, _ = _d9_targets(chart, house)
d9_lord = engine.SIGN_LORDS[engine.SIGNS[(d9_asc_idx + house - 1) % 12]]
lord_si = engine._navamsa_idx(natal[d9_lord]["degree"])
# Before BUG-1060 this target was the lord's D1 degree.
assert targets[f"D9_{d9_lord}(宫主)"] == lord_si * 30 + 15, (chart["id"], house)
def test_command_checks_the_helper_targets_in_the_d9_frame(charts, monkeypatch) -> None:
calls = []
original = engine._check_pac
def recording(planet_name, planet_lon, target_lon, asc_idx):
calls.append((planet_name, target_lon, asc_idx))
return original(planet_name, planet_lon, target_lon, asc_idx)
monkeypatch.setattr(engine, "_check_pac", recording)
for chart in charts:
for house in (1, 7, 10):
(d9_asc_idx, targets), _, _ = _d9_targets(chart, house)
calls.clear()
engine.cmd_double_transit_pac(_args(chart, house))
for planet in ("Jupiter", "Saturn"):
seen = [lon for name, lon, idx in calls if name == planet]
# d1 (4 targets) → d9 → cl (2), in insertion order, per transit planet; for house 1
# the D1 house lord is the lagna lord, so the D9 layer has 3 distinct targets.
assert seen[4:4 + len(targets)] == list(targets.values()), (chart["id"], house, planet)
assert {idx for name, _, idx in calls if name == planet} >= {d9_asc_idx}
def test_public_chart_seventh_house_d9_row(charts) -> None:
"""The BUG-1060 reproduction: Saturn's 10th aspect was on the D9 lagna, not on D9 house 7."""
chart = next(item for item in charts if item["id"] == "steve_jobs")
result = engine.cmd_double_transit_pac(_args(chart, 7))
assert result["stats"]["d9_ascendant"] == "Capricorn"
label = "D9_7宫(Cancer)"
assert label not in result["d9"]["saturn"]
assert result["d9"]["jupiter"][label] == [{"type": "Position", "desc": "同宫(7宫)"}]
assert "D9_Moon(宫主)" not in result["d9"]["saturn"]
def test_d1_and_chandra_lagna_layers_match_the_pre_fix_golden() -> None:
golden = json.loads(GOLDEN.read_text(encoding="utf-8"))
assert golden["provenance"]["charts"] == [chart["id"] for chart in load_public_charts(ROOT)]
assert json.dumps(_d1_cl_layers(), ensure_ascii=False, sort_keys=True) == json.dumps(
golden["layers"], ensure_ascii=False, sort_keys=True,
)
def _capture() -> None:
import os
if os.environ.get("PYTHONHASHSEED") != "0":
raise SystemExit("Set PYTHONHASHSEED=0 before starting this process (ERR-111).")
charts = load_public_charts(ROOT)
payload = {
"provenance": {
"generator": "tests/test_double_transit_d9_targets.py --capture (jyotish_engine.cmd_double_transit_pac)",
"charts": [chart["id"] for chart in charts],
"reference_date": charts[0]["body"]["today"],
"ayanamsa": charts[0]["body"]["ayanamsa"],
"node_mode": charts[0]["body"]["node_mode"],
"houses": list(HOUSES),
"note": "D1 / Chandra Lagna layers only; recaptured after BUG-1185 (aspect houses counted from 1).",
},
"layers": _d1_cl_layers(),
}
GOLDEN.parent.mkdir(parents=True, exist_ok=True)
GOLDEN.write_text(json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=1) + "\n", encoding="utf-8")
print(f"wrote {GOLDEN.relative_to(ROOT)}")
if __name__ == "__main__" and "--capture" in sys.argv:
_capture()