"""Book-shaped rashi dasha rows. These tests do not import PyJHora.""" from __future__ import annotations import ast import json from datetime import date from pathlib import Path from scripts.goel_short_strength import dignity_units, graha_strengths, rashi_strengths from scripts.rashi_dashas_native import ( calculate_rashi_dashas, drig_aspect_sequence, navamsha_sign, navamsha_years, ) from scripts.rashi_dasha_report import render_rashi_dasha_chapter ROOT = Path(__file__).resolve().parents[1] FIXTURE = ROOT / "tests" / "fixtures" / "rashi_dasha_pyjhora_20261008.json" GOEL_LONS = { "Sun": 0 * 30 + 20.18, "Moon": 7 * 30 + 20.15, "Mars": 7 * 30 + 23.02, "Mercury": 1 * 30 + 11.12, "Jupiter": 5 * 30 + 3.13, "Venus": 11 * 30 + 17.28, "Saturn": 0 * 30 + 7.07, } CAPRICORN_STRONGER = { "Sun": 9 * 30 + 1, "Moon": 9 * 30 + 2, "Mars": 9 * 30 + 3, "Mercury": 9 * 30 + 4, "Jupiter": 9 * 30 + 5, "Venus": 9 * 30 + 6, "Saturn": 6 * 30 + 15, "Rahu": 15.0, "Ketu": 195.0, } def test_goel_short_method_matches_the_printed_totals_and_direct_sthira() -> None: graha = graha_strengths(GOEL_LONS) assert [graha[name]["total"] for name in ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")] == [ 150, 120, 180, 130, 70, 150, 50, ] rashi = rashi_strengths(GOEL_LONS, graha) assert [rashi[index]["total"] for index in range(12)] == [ 280, 260, 250, 260, 250, 260, 230, 300, 250, 130, 150, 260, ] packet = calculate_rashi_dashas(date(2001, 1, 1), 5 * 30 + 1.55, GOEL_LONS) sthira = packet["sthira"] assert sthira["profile_id"] == "goel_rao_short_brahma_method_v1" assert sthira["brahma_planet"] == "Mars" assert [row["sign_index"] for row in sthira["periods"]] == list(range(7, 12)) + list(range(0, 7)) assert [row["years"] for row in sthira["periods"]] == [8, 9, 7, 8, 9, 7, 8, 9, 7, 8, 9, 7] aquarius = sthira["periods"][3] assert aquarius["sign"] == "Aquarius" assert aquarius["start"] == "2025-01-01" assert [row["sign"] for row in aquarius["antardasha"][:2]] == ["Aquarius", "Pisces"] assert aquarius["antardasha"][0]["end"] == "2025-09-01" assert "pratyantar" not in aquarius def test_same_goel_chart_niryaana_uses_short_bala_not_the_printed_jaimini_start() -> None: # Printed Jaimini rashi bala on this chart starts Pisces. Short-method totals tie, # and the karaka rank of the lords selects Virgo. The report keeps the short method. packet = calculate_rashi_dashas(date(2001, 1, 1), 5 * 30 + 1.55, GOEL_LONS) niryaana = packet["niryaana_shoola"] assert niryaana["periods"][0]["sign"] == "Virgo" assert [row["years"] for row in niryaana["periods"]] == [9] * 12 assert niryaana["periods"][0]["antardasha"][0]["sign"] == "Virgo" assert niryaana["periods"][0]["antardasha"][0]["end"] == "2001-10-01" def test_drig_aspect_order_for_a_cancer_lagna_and_narayana_style_years() -> None: assert drig_aspect_sequence(3) == [11, 8, 5, 2, 0, 4, 7, 10, 1, 9, 6, 3] packet = calculate_rashi_dashas(date(2001, 1, 1), 3 * 30 + 1.0, GOEL_LONS) drig = packet["drig"] assert drig["profile_id"] == "drig_rath_aspect_sequence_v1" assert [row["sign_index"] for row in drig["periods"]] == drig_aspect_sequence(3) assert all(isinstance(row["years"], int) and 0 <= row["years"] <= 12 for row in drig["periods"]) def test_shoola_from_the_stronger_seventh_puts_aries_at_year_27() -> None: packet = calculate_rashi_dashas(date(2001, 1, 1), 3 * 30 + 1.0, CAPRICORN_STRONGER) shoola = packet["shoola"] assert shoola["profile_id"] == "shoola_forward_md_direct_ad_v1" assert [row["sign"] for row in shoola["periods"][:4]] == ["Capricorn", "Aquarius", "Pisces", "Aries"] assert shoola["periods"][3]["start"] == "2028-01-01" assert shoola["periods"][3]["years"] == 9 assert shoola["periods"][0]["antardasha"][0]["sign"] == "Capricorn" def test_navamsha_method_1_starts_from_the_seventh_when_d9_lagna_is_even() -> None: longitude = 3 * 30 + 26.7 assert navamsha_sign(longitude) == 11 assert navamsha_years(0, 0) == 12 assert navamsha_years(0, 1) == 2 assert navamsha_years(1, 0) == 2 packet = calculate_rashi_dashas(date(2001, 1, 1), longitude, GOEL_LONS) navamsha = packet["navamsha"] assert navamsha["profile_id"] == "goel_method_1_v1" assert navamsha["d9_lagna_index"] == 11 assert navamsha["periods"][0]["sign"] == "Virgo" assert navamsha["periods"][0]["antardasha"][0]["sign"] == "Virgo" assert "pratyantar" not in navamsha["periods"][0] def test_chara_keeps_the_existing_kn_rao_order_and_a_zero_year_row() -> None: longitudes = {name: 10.0 for name in ("Sun", "Moon", "Mars", "Mercury", "Venus", "Saturn", "Rahu")} longitudes["Jupiter"] = 9 * 30 + 5.0 longitudes["Ketu"] = 190.0 packet = calculate_rashi_dashas(date(2001, 1, 1), 10.0, longitudes) chara = packet["chara"] assert chara["profile_id"] == "kn_rao" assert [row["sign_index"] for row in chara["periods"]] == list(range(12)) sagittarius = chara["periods"][8] assert sagittarius["sign"] == "Sagittarius" assert sagittarius["years"] == 0 assert sagittarius["start"] == sagittarius["end"] assert chara["periods"][0]["antardasha"][0]["sign"] == "Taurus" assert chara["periods"][0]["antardasha"][-1]["sign"] == "Aries" def test_lagna_kendradi_is_omitted() -> None: packet = calculate_rashi_dashas(date(2001, 1, 1), 10.0, GOEL_LONS) section = packet["lagna_kendradi"] assert section["status"] == "omitted" assert section["periods"] == [] def test_mercury_in_virgo_uses_longitude_bands() -> None: assert dignity_units("Mercury", 5, 10.0) == 70 assert dignity_units("Mercury", 5, 16.0) == 60 assert dignity_units("Mercury", 5, 22.0) == 50 def test_report_modules_do_not_call_the_old_shoola_or_niryaana_helpers() -> None: for name in ("rashi_dashas_native.py", "rashi_dasha_report.py", "goel_short_strength.py"): source = (ROOT / "scripts" / name).read_text(encoding="utf-8") tree = ast.parse(source) imported = [] for node in ast.walk(tree): if isinstance(node, ast.ImportFrom) and node.module: imported.append(node.module) elif isinstance(node, ast.Import): imported.extend(alias.name for alias in node.names) assert "niryaana_shoola_dasha" not in imported assert "extended_dashas" not in imported assert all(not item.split(".")[0] == "jhora" for item in imported) def test_pyjhora_fixture_diffs_stay_on_the_recorded_steps() -> None: payload = json.loads(FIXTURE.read_text(encoding="utf-8")) assert payload["pyjhora_version"] == "4.8.7" assert payload["generator_python_has_pyjhora"] is True from scripts.jyotish_engine import compute_chart_data for case_id, case in payload["cases"].items(): birth = case["birth_source"] chart, _asc, _jd, _ayanamsa = compute_chart_data( birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], birth["lat"], birth["lon"], birth["tz"], second=birth.get("second") or 0, ayanamsa_name="lahiri", # the frame PyJHora was frozen in ) assert chart is not None longitudes = { name: float(row["degree_raw"]) for name, row in chart["planets"].items() if isinstance(row, dict) and "degree_raw" in row } native = calculate_rashi_dashas(chart["birth_info"]["date"], float(chart["ascendant"]["degree_raw"]), longitudes) for key, expected in case["diff_steps"].items(): section = native[key] recorded = case["native"][key] if key == "lagna_kendradi": assert section["status"] == "omitted" assert expected == ["start_sign", "direction", "years", "subperiod"] continue assert [row["sign_index"] for row in section["periods"]] == recorded["sign_indexes"] assert [row["years"] for row in section["periods"]] == recorded["years"] assert _diff_steps(section, case["pyjhora"][key]) == expected def _succession(signs: list[int]) -> tuple[int, ...]: return tuple((signs[index] - signs[index - 1]) % 12 for index in range(1, len(signs))) def _diff_steps(section: dict, pyjhora: dict) -> list[str]: our_signs = [row["sign_index"] for row in section["periods"]] their_signs = pyjhora["sign_indexes"] our_years = [row["years"] for row in section["periods"]] their_years = pyjhora["years"] steps = [] if not our_signs or not their_signs or our_signs[0] != their_signs[0]: steps.append("start_sign") if _succession(our_signs) != _succession(their_signs): steps.append("direction") if sorted(zip(our_signs, our_years)) != sorted(zip(their_signs, their_years)): steps.append("years") our_sub = [row["sign_index"] for row in (section["periods"][0].get("antardasha") or [])] if section["periods"] else [] their_sub = pyjhora.get("first_sub_sign_indexes") or [] if our_sub != their_sub: steps.append("subperiod") if our_signs == their_signs and our_years == their_years and section["periods"][1:2]: if section["periods"][1]["start"] != pyjhora.get("second_start"): steps.append("date_basis") return steps def test_jobs_chapter_names_the_method_and_leaves_kendradi_out() -> None: payload = json.loads(FIXTURE.read_text(encoding="utf-8")) birth = payload["cases"]["steve_jobs_1955_aa"]["birth_source"] from scripts.jyotish_engine import compute_chart_data chart, _asc, _jd, _ayanamsa = compute_chart_data( birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], birth["lat"], birth["lon"], birth["tz"], second=birth.get("second") or 0, ) packet = { "report_language": "zh", "birth_info": {"date": chart["birth_info"]["date"], "name": "steve_jobs_1955_aa"}, "core_chart": {"ascendant": chart["ascendant"], "planets": chart["planets"]}, } text = render_rashi_dasha_chapter(packet) for title in ("Sthira", "Niryaana Shoola", "Drig", "Navamsha", "Shoola", "Chara"): assert f"### {title}" in text assert "本站选用的方法:" in text assert "未与外部软件逐一核对" in text assert "K.N. Rao" in text assert "不是 Narayana 表" in text kendradi = text.split("### Lagna Kendradi", 1)[1] assert "本轮不上表" in kendradi assert "| 序号 | 星座 |" not in kendradi english = render_rashi_dasha_chapter({**packet, "report_language": "en"}) assert "Method used on this site:" in english assert "This report does not include Lagna Kendradi" in english assert "not checked one-by-one against external software." in english leaked = [char for char in english if "\u4e00" <= char <= "\u9fff"] assert leaked == [] def test_full_data_report_appends_the_chapter_without_touching_a_chartless_packet() -> None: from scripts.pl9_full_data_export import render_full_data_markdown from tests.test_report_full_data_fix import _packet chartless = render_full_data_markdown(_packet("zh")) assert "Sthira Dasha" not in chartless payload = json.loads(FIXTURE.read_text(encoding="utf-8")) birth = payload["cases"]["steve_jobs_1955_aa"]["birth_source"] from scripts.jyotish_engine import compute_chart_data chart, _asc, _jd, _ayanamsa = compute_chart_data( birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], birth["lat"], birth["lon"], birth["tz"], second=birth.get("second") or 0, ) packet = _packet("zh") packet["birth_info"]["date"] = chart["birth_info"]["date"] packet["core_chart"] = {"ascendant": chart["ascendant"], "planets": chart["planets"]} text = render_full_data_markdown(packet) assert "星座大运" in text assert "Sthira" in text assert "本站选用的方法" in text assert "Kendradi" in text assert "本轮不上表" in text kendradi = text.split("Kendradi", 1)[1] assert "| 序号 | 星座 |" not in kendradi english_packet = _packet("en") english_packet["birth_info"]["date"] = chart["birth_info"]["date"] english_packet["core_chart"] = packet["core_chart"] english = render_full_data_markdown(english_packet) assert "Sthira" in english assert "Method used on this site:" in english assert "This report does not include Lagna Kendradi" in english assert not any("\u4e00" <= char <= "\u9fff" for char in english)