"""The chart page's dignity and varga-meaning tables must be the engine's. `frontend/src/lib/vedic-chart-tables.json` is read by the chart-page mapper so the divisional tables can show 入旺 / 自宫 / 落陷 without a second hand-written rule set. This test pins it to the constants `jyotish_engine._get_dignity_level` uses and to `divisional_charts_extended.VargaType`. """ from __future__ import annotations import json import shutil import subprocess import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) import divisional_charts_extended # noqa: E402 import jyotish_engine # noqa: E402 from consultation_native_layers import build_d9_summary # noqa: E402 TABLES = json.loads((ROOT / "frontend/src/lib/vedic-chart-tables.json").read_text(encoding="utf-8")) PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"] def _table_dignity(planet: str, sign: str) -> str | None: """Same precedence as `vargaDignity` in frontend/src/lib/chart-view-varga.ts.""" if TABLES["exaltation"].get(planet) == sign: return "EXALTED" if TABLES["signLords"].get(sign) == planet or TABLES["nodeOwnSign"].get(planet) == sign: return "OWN_SIGN" if TABLES["debilitation"].get(planet) == sign: return "DEBILITATED" return None def test_tables_equal_engine_constants() -> None: assert TABLES["signLords"] == jyotish_engine.SIGN_LORDS assert TABLES["exaltation"] == jyotish_engine.EXALTATION assert TABLES["debilitation"] == jyotish_engine.DEBILITATION def test_every_planet_sign_pair_matches_engine_dignity_level() -> None: for planet in PLANETS: for sign in jyotish_engine.SIGN_LORDS: # No degree and no chart context: the varga table shows the sign # dignity only (no moolatrikona band, no neecha bhanga, no friendship). engine = jyotish_engine._get_dignity_level(planet, sign) expected = engine if engine in {"EXALTED", "OWN_SIGN", "DEBILITATED"} else None if engine == "NEECHA_BHANGA": expected = "DEBILITATED" assert _table_dignity(planet, sign) == expected, (planet, sign, engine) def test_varga_meanings_equal_engine_varga_types() -> None: engine = {str(v.division): v.meaning for v in divisional_charts_extended.VargaType if v.division <= 60} assert TABLES["vargaMeaning"] == engine for division in ("40", "45", "60"): assert division in TABLES["vargaMeaning"] @pytest.fixture(scope="module") def d9_projection() -> tuple[dict, dict]: """Run the real server mapper, not a Python imitation of its comparison. The checked-in golden is a fictional chart, Raman ayanamsa / mean node. No ephemeris is recomputed under a different process-global ayanamsa. """ frontend = ROOT / "frontend" node = shutil.which("node") tsx = frontend / "node_modules/tsx/dist/cli.mjs" if not node or not tsx.is_file(): pytest.skip("D9 mapper/native comparison requires Node and frontend tsx dependencies") golden = json.loads((frontend / "tests/fixtures/chart-view-golden.json").read_text(encoding="utf-8")) assert golden["chart"]["birth"]["ayanamsa_name"] == "raman" assert golden["chart"]["birth"]["node_mode"] == "mean" script = """ import { readFileSync } from 'node:fs'; import { buildChartView } from './src/lib/chart-view-mapper.ts'; const golden = JSON.parse(readFileSync('./tests/fixtures/chart-view-golden.json', 'utf8')); const birth = golden.chart.birth; const view = buildChartView({ chart: golden.chart, varga: golden.varga_full, chara: null, western: null, profile: { name: 'Synthetic', date: birth.date, time: birth.time, latitude: birth.lat, longitude: birth.lon, timezoneOffset: 8, ayanamsa: birth.ayanamsa_name, birthTimeStatus: 'confirmed', }, asOf: '2026-09-15', }); process.stdout.write(JSON.stringify(view.vedic.vargas.find(varga => varga.id === 'D9'))); """ completed = subprocess.run( [node, str(tsx), "--eval", script], cwd=frontend, text=True, encoding="utf-8", capture_output=True, timeout=60, check=False, ) assert completed.returncode == 0, completed.stderr return golden, json.loads(completed.stdout) def test_d9_projected_planets_match_native_vargottama(d9_projection: tuple[dict, dict]) -> None: golden, projected = d9_projection natal = golden["chart"]["planets"] d9 = golden["varga_full"]["result"]["D9_Navamsa"] native = jyotish_engine._calc_vargottama(natal, {"D9_Navamsa": d9["planets"]}) assert projected["supportsDignity"] is True assert projected["supportsVargottama"] is True assert len(projected["rows"]) == len(PLANETS) results = set() for row in projected["rows"]: expected = native[row["name"]] assert row["sign"] == expected["d9_sign"] assert row["natalSign"] == expected["d1_sign"] assert row["vargottama"] is expected["is_vargottama"] results.add(row["vargottama"]) assert results == {True, False}, "golden must exercise both valid outcomes" def test_d9_projected_ascendant_matches_native_summary(d9_projection: tuple[dict, dict]) -> None: # _calc_vargottama handles planets, NOT the ascendant; use the native lagna # summary path for this separate contract instead of attributing it there. golden, projected = d9_projection native = build_d9_summary( jyotish_engine, golden["chart"], {"varga_full": golden["varga_full"]["result"]}, ) assert native["status"] == "executed" assert projected["ascendant"]["sign"] == native["lagna"] assert projected["ascendant"]["natalSign"] == golden["chart"]["ascendant"]["sign"] assert projected["ascendant"]["vargottama"] is native["lagna_vargottama"]