Files
Jyotisha/tests/test_chart_view_points_api.py
T
ClaudeandClaude Opus 5.5 5157f96df3
Independent Staging Quality Gate / validate (push) Successful in 29m39s
Independent Staging Quality Gate / publish (push) Successful in 4m3s
feat(chart): 盘面数据 card beside the board, linked both ways
The chart page's tables move into one fixed-height card beside the board
with nine tabs: 行星, 虚星, 特殊点, 映射, 八分点, 星力, 大运, 敌友, 瑜伽.
The point tabs and 八分点 are the board's layers, switched together from
either side. 星力, 大运, 敌友 and 瑜伽 mark, tint, link and fade bodies on
the natal board (lib/chart-board-focus.ts).

The planet table gains the ascendant row, nakshatra lord, pada and the
seven Chara karakas, now produced by /api/chart_points from
jaimini.calc_chara_karaka_7 (blocked on a tie). Every card keeps a fixed
height and scrolls inside; tables never wrap and can be dragged sideways.
Hint captions are gone, 八宫点 reads 八分点, and the deep reading keeps
time, energy and birth data only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyeZrYn6ryNcSe5Y2Y
2026-10-30 16:27:04 +08:00

123 lines
5.2 KiB
Python

"""`/api/chart_points` (issue #114): the chart page's data tables come from existing producers."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
from chart_view_points.api import ChartPointsInputError, SIGNS, compute_chart_points # noqa: E402
# Real engine /api/chart output (1990-06-15 12:00 Beijing, raman), shared with the frontend tests.
GOLDEN = json.loads((ROOT / "frontend" / "tests" / "fixtures" / "chart-insight-golden.json").read_text(encoding="utf-8"))
BODY = {
"year": 1990, "month": 6, "day": 15, "hour": 12, "minute": 0, "second": 0,
"lat": 39.9042, "lon": 116.4074, "tz": 8, "ayanamsa": "raman",
"planets": GOLDEN["chart"]["planets"], "ascendant": GOLDEN["chart"]["ascendant"],
}
def _sign_index(longitude: float) -> int:
return int((longitude % 360) // 30)
def test_every_block_is_computed_from_the_engine_producers():
result = compute_chart_points(BODY)
assert result["success"] is True
assert result["upagrahas"]["status"] == "computed"
assert [row["key"] for row in result["upagrahas"]["rows"]][:5] == ["Dhuma", "Vyatipata", "Parivesha", "Indrachapa", "Upaketu"]
assert len(result["upagrahas"]["rows"]) == 11
assert result["special_lagnas"]["status"] == "computed"
assert len(result["special_lagnas"]["rows"]) == 8
assert [row["key"] for row in result["yogi_points"]["rows"]] == ["Yogi_Point", "Avayogi_Point"]
assert [row["key"] for row in result["arudha_padas"]["rows"]] == ["A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "A11", "UL"]
assert len(result["friendship"]["rows"]) == 9
def test_points_carry_sign_and_house_counted_from_the_ascendant():
result = compute_chart_points(BODY)
asc = _sign_index(float(BODY["ascendant"]["lon"]))
for block in ("upagrahas", "special_lagnas", "yogi_points"):
for row in result[block]["rows"]:
sign = _sign_index(row["longitude"])
assert row["sign"] == SIGNS[sign]
assert row["house"] == (sign - asc) % 12 + 1
assert 0 <= row["degree_in_sign"] < 30
def test_solar_upagrahas_follow_the_sun_formula():
sun = float(BODY["planets"]["Sun"]["lon"])
rows = {row["key"]: row["longitude"] for row in compute_chart_points(BODY)["upagrahas"]["rows"]}
dhuma = (sun + 133 + 20 / 60) % 360
assert rows["Dhuma"] == pytest.approx(dhuma, abs=1e-3)
assert rows["Vyatipata"] == pytest.approx((360 - dhuma) % 360, abs=1e-3)
assert rows["Parivesha"] == pytest.approx((rows["Vyatipata"] + 180) % 360, abs=1e-3)
def test_avayogi_is_yogi_plus_186_40():
rows = {row["key"]: row["longitude"] for row in compute_chart_points(BODY)["yogi_points"]["rows"]}
assert (rows["Avayogi_Point"] - rows["Yogi_Point"]) % 360 == pytest.approx(186 + 40 / 60, abs=1e-3)
def test_friendship_compound_is_natural_plus_temporary():
rows = compute_chart_points(BODY)["friendship"]["rows"]
sun = next(row for row in rows if row["planet"] == "Sun")
for target in sun["compound"]["great_friends"]:
assert target in sun["natural"]["friends"] and target in sun["temporary"]["friends"]
for target in sun["compound"]["great_enemies"]:
assert target in sun["natural"]["enemies"] and target in sun["temporary"]["enemies"]
def test_chara_karakas_rank_the_seven_by_in_sign_degree():
result = compute_chart_points(BODY)["chara_karakas"]
assert result["status"] == "computed"
assert result["scheme"] == "chara_7"
rows = result["rows"]
assert [row["key"] for row in rows] == ["AK", "AmK", "BK", "MK", "PK", "GK", "DK"]
planets = BODY["planets"]
expected = sorted(
("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"),
key=lambda name: planets[name]["lon"] % 30,
reverse=True,
)
assert [row["planet"] for row in rows] == expected
def test_chara_karakas_block_on_a_tie():
planets = {name: dict(row) for name, row in BODY["planets"].items()}
planets["Moon"]["lon"] = planets["Sun"]["lon"] % 30 + 30 * int(planets["Moon"]["lon"] // 30)
result = compute_chart_points({**BODY, "planets": planets})
assert result["chara_karakas"] == {"status": "blocked", "reason": "tied_degrees"}
def test_one_failing_block_does_not_fail_the_response(monkeypatch):
import gulika
def broken(*_args, **_kwargs):
raise RuntimeError("boom")
monkeypatch.setattr(gulika, "calculate_upagrahas", broken)
result = compute_chart_points(BODY)
assert result["upagrahas"]["status"] == "blocked"
assert result["arudha_padas"]["status"] == "computed"
def test_missing_birth_or_chart_is_a_bad_request():
with pytest.raises(ChartPointsInputError):
compute_chart_points({**BODY, "planets": None})
with pytest.raises(ChartPointsInputError):
compute_chart_points({key: value for key, value in BODY.items() if key != "lat"})
def test_route_is_registered_thinly():
source = (SCRIPTS / "jyotish_api_server.py").read_text(encoding="utf-8")
assert "elif path == '/api/chart_points':" in source
assert "_load_local_module('chart_view_points.api')" in source