"""Birth-sky cover computation (TASK-birth-sky-cover-20260928 T1). Golden instant is the fictional sample from the brief: 1994-05-18 07:40 UTC+8, 30.27N 120.15E. Values are geometric altitudes (no refraction), azimuth from north increasing eastward. """ from __future__ import annotations import json from io import BytesIO import pytest import swisseph as swe from scripts.api_heavy_compute_gate import reset_heavy_compute_gate from scripts.birth_sky import ( STAR_FILE, BirthSkyError, compute_birth_sky, compute_birth_sky_from_request, precess_j2000, ) from scripts.jyotish_api_server import ( DEFAULT_ALLOWED_HOSTS, DEFAULT_ALLOWED_ORIGINS, JyotishAPIHandler, ) LAT = 30.27 LON = 120.15 def _sky(time: str = "07:40", date: str = "1994-05-18", lat: float = LAT, lon: float = LON, tz: float = 8): return compute_birth_sky(date, time, tz, lat, lon) def _planet(sky: dict, key: str) -> dict: return next(item for item in sky["planets"] if item["id"] == key) def test_golden_sample_sun_saturn_and_below_horizon_bodies() -> None: sky = _sky() sun = _planet(sky, "sun") saturn = _planet(sky, "saturn") assert sun["altitude"] == pytest.approx(31.7, abs=0.2) assert sun["azimuth"] == pytest.approx(84.7, abs=0.3) assert saturn["altitude"] == pytest.approx(50.3, abs=0.2) assert saturn["azimuth"] == pytest.approx(191.1, abs=0.3) assert _planet(sky, "moon")["altitude"] < 0 assert _planet(sky, "jupiter")["altitude"] < 0 assert sky["phase"] == "day" assert sky["sun_altitude"] == pytest.approx(31.7, abs=0.2) assert [item["id"] for item in sky["planets"]] == [ "sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", ] def test_response_does_not_echo_birth_inputs() -> None: sky = _sky() text = json.dumps(sky) for key in ("date", "time", "latitude", "longitude", "timezone", "lat", "lon"): assert f'"{key}"' not in text assert "1994" not in text assert "07:40" not in text @pytest.mark.parametrize( ("date", "lat"), [("1994-05-18", 30.27), ("2001-12-21", -33.87), ("2010-03-20", 51.5)], ) def test_local_apparent_noon_altitude_matches_90_minus_lat_minus_declination(date: str, lat: float) -> None: # Independent check: scan the local noon hour for the Sun's highest point # and compare with 90 - |lat - dec|, dec from swe's equatorial position. best = max( (_sky(f"{hour:02d}:{minute:02d}", date=date, lat=lat)["sun_altitude"], hour, minute) for hour in range(10, 15) for minute in range(60) ) altitude, hour, minute = best jd = swe.julday(*map(int, date.split("-")), hour + minute / 60 - 8) declination = swe.calc_ut(jd, swe.SUN, swe.FLG_EQUATORIAL)[0][1] assert altitude == pytest.approx(90 - abs(lat - declination), abs=0.5) def test_polaris_altitude_is_about_the_latitude() -> None: polaris = next(star for star in _sky()["stars"] if star["hr"] == 424) assert polaris["altitude"] == pytest.approx(30.0, abs=1.0) assert polaris["azimuth"] < 3 or polaris["azimuth"] > 357 @pytest.mark.parametrize( ("time", "phase"), [ ("07:40", "day"), ("04:50", "twilight_morning"), ("19:10", "twilight_evening"), ("00:00", "night"), ], ) def test_phase_has_four_states(time: str, phase: str) -> None: sky = _sky(time) assert sky["phase"] == phase if phase.startswith("twilight"): assert -18 <= sky["sun_altitude"] <= 0 def test_stars_are_above_horizon_and_lines_use_known_hr_numbers() -> None: sky = _sky("00:00") assert sky["stars"] assert all(star["altitude"] > 0 for star in sky["stars"]) assert all(star["magnitude"] <= 4.0 for star in sky["stars"]) assert [line["id"] for line in sky["lines"]] == ["big_dipper", "cassiopeia", "orion"] catalog = json.loads(STAR_FILE.read_text(encoding="utf-8")) known = {row[0] for row in catalog["stars"]} for line in sky["lines"]: for stroke in line["strokes"]: assert set(stroke) <= known def test_star_catalogue_records_public_domain_source() -> None: catalog = json.loads(STAR_FILE.read_text(encoding="utf-8")) assert "Yale Bright Star Catalogue" in catalog["source"] assert catalog["source_url"].startswith("https://cdsarc.cds.unistra.fr/ftp/V/50/") assert len(catalog["source_sha256"]) == 64 assert "Public domain" in catalog["license"] assert all(row[3] <= 4.0 for row in catalog["stars"]) assert len(catalog["stars"]) > 400 def test_iau_1976_precession_matches_meeus_example_21b() -> None: # Meeus, Astronomical Algorithms, example 21.b (theta Persei, J2000 -> 2028-11-13.19). ra, dec = precess_j2000(41.054063, 49.227750, 2462088.69) assert ra == pytest.approx(41.547214, abs=1e-5) assert dec == pytest.approx(49.348483, abs=1e-5) def test_request_validation_rejects_bad_input() -> None: good = {"date": "1994-05-18", "time": "07:40", "timezone_offset": 8, "latitude": LAT, "longitude": LON} assert compute_birth_sky_from_request(good)["phase"] == "day" for patch in ({"latitude": 91}, {"longitude": None}, {"date": "1994/05/18"}, {"time": "7h"}, {"timezone_offset": "x"}): with pytest.raises(BirthSkyError): compute_birth_sky_from_request({**good, **patch}) class _FakeHeaders(dict): def get(self, key, default=None): return super().get(key, default) class _FakeServer: allowed_origins = DEFAULT_ALLOWED_ORIGINS allowed_hosts = DEFAULT_ALLOWED_HOSTS class _PostCaptureHandler(JyotishAPIHandler): def __init__(self, path: str, payload: dict) -> None: raw = json.dumps(payload).encode("utf-8") self.headers = _FakeHeaders({"Content-Length": str(len(raw)), "Content-Type": "application/json"}) self.server = _FakeServer() self.path = path self.rfile = BytesIO(raw) self.wfile = BytesIO() self.status_code = None self.client_address = ("test-birth-sky", 0) def send_response(self, code, message=None): # noqa: ANN001 self.status_code = code def send_header(self, key, value): # noqa: ANN001 return None def end_headers(self): return None def payload(self) -> dict: return json.loads(self.wfile.getvalue().decode("utf-8")) @pytest.fixture def api_env(monkeypatch): monkeypatch.setenv("JYOTISH_API_RATE_LIMIT_PER_MINUTE", "0") reset_heavy_compute_gate() yield reset_heavy_compute_gate() def test_http_endpoint_is_thinly_registered(api_env) -> None: handler = _PostCaptureHandler( "/api/birth_sky", {"date": "1994-05-18", "time": "07:40", "timezone_offset": 8, "latitude": LAT, "longitude": LON}, ) handler.do_POST() assert handler.status_code == 200 payload = handler.payload() assert payload["phase"] == "day" assert _planet(payload, "saturn")["azimuth"] == pytest.approx(191.1, abs=0.3) bad = _PostCaptureHandler("/api/birth_sky", {"date": "1994-05-18", "time": "07:40"}) bad.do_POST() assert bad.status_code == 400