Files
Jyotisha/tests/test_chart_page_vedic_tables_contract.py
T
Jesse_ChenandClaude Opus 5.5 ae18952e51
Independent Staging Quality Gate / validate (push) Successful in 15m27s
Independent Staging Quality Gate / publish (push) Successful in 3m42s
fix(report,chart): blank columns — drop Kranti, split Ashtakavarga house SAV, ascendant nakshatra, engine varga dignity, no duplicate charts (BUG-1199..1203)
- Report p33 table: no Kranti column (no producer; materials.kranti stays blocked).
- Report p53 table: the "SAV" column was the with-Lagna total (386). Split in the
  reader into SAV / Lagna BAV / SAV + Lagna with a totals row; ashtakavarga.py is
  untouched (frozen rectification identity, ERR-115).
- /api/chart ascendant carries nakshatra / pada / lord (_nakshatra_fields shared with
  the nine bodies); chart cache schema 4 -> 5.
- /api/varga_full bodies carry dignity_level from varga_dignity_level, the function
  the Vimsopaka path now shares (A/B identical); the chart page shows the D1 status
  wording and no longer derives a three-level dignity itself.
- Report page draws each chart once: the trailing block skips charts the body already
  fenced (same rule as the export); the birth-time caveat moves under the body's
  chart heading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-10-03 09:11:15 +08:00

126 lines
5.6 KiB
Python

"""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: varga
meanings (`divisional_charts_extended.VargaType`) and the dignity wording
(`jyotish_engine.DIGNITY_LABELS`). The dignity level itself comes from
`/api/varga_full` (`varga_dignity_level`, BUG-1202); the page never derives it.
"""
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 test_dignity_labels_equal_engine_status_wording() -> None:
# BUG-1202: divisional rows carry the engine's level and the page labels it
# with the D1 status wording; the three-level page table is retired.
assert TABLES["dignityLabels"] == jyotish_engine.DIGNITY_LABELS
assert {"signLords", "exaltation", "debilitation", "nodeOwnSign"}.isdisjoint(TABLES)
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"]
def test_d9_projected_dignity_is_the_engine_level(d9_projection: tuple[dict, dict]) -> None:
golden, projected = d9_projection
d9_planets = golden["varga_full"]["result"]["D9_Navamsa"]["planets"]
levels = set()
for row in projected["rows"]:
expected = jyotish_engine.varga_dignity_level(row["name"], d9_planets)
assert expected is not None
assert row["dignity"] == expected == d9_planets[row["name"]]["dignity_level"]
levels.add(expected)
# The golden must show that friendship levels reach the page, not only
# exalted / own / debilitated.
assert {"OWN_SIGN", "DEBILITATED", "NEUTRAL", "ENEMY", "FRIEND"} <= levels