""" Unit tests for the reverse ephemeris lookup — sign_windows / narrowed_windows / present_signs (Set the Game Clock narrowing, EarthmanRPG roadmap step 21). Unlike test_calc.py these DO drive the Swiss Ephemeris (Moshier fallback when data files are absent — 0.1° precision, far inside the ±1-day tolerances here). The assertions are largely SELF-VALIDATING: rather than hardcoding astrological history, each returned window is forward-checked — its midpoint must actually carry the planet in the claimed sign, and its edges must be true sign boundaries. Run: pyswiss/.venv/Scripts/python pyswiss/manage.py test apps.charts """ from datetime import datetime, timezone from django.test import SimpleTestCase import swisseph as swe from apps.charts.calc import ( PLANET_CODES, get_julian_day, get_sign, narrowed_windows, present_signs, set_ephe_path, sign_windows, ) def _jd(iso): return get_julian_day(datetime.fromisoformat(iso).replace(tzinfo=timezone.utc)) def _sign_at(jd, planet): pos, _ = swe.calc_ut(jd, PLANET_CODES[planet], swe.FLG_SWIEPH) return get_sign(pos[0]) class SignWindowsTest(SimpleTestCase): @classmethod def setUpClass(cls): super().setUpClass() set_ephe_path() def test_sun_in_aries_2020(self): wins = sign_windows('Sun', 'Aries', [(_jd('2020-01-01'), _jd('2020-12-31'))]) self.assertEqual(len(wins), 1) a, b = wins[0] # Ingress at the March equinox, egress ~30 days later. self.assertAlmostEqual(a, _jd('2020-03-20'), delta=1.0) self.assertAlmostEqual(b, _jd('2020-04-19'), delta=1.0) def test_window_edges_are_true_sign_boundaries(self): lo, hi = _jd('1990-01-01'), _jd('2000-01-01') wins = sign_windows('Saturn', 'Pisces', [(lo, hi)]) # Saturn's mid-90s Pisces residence, split by the retrograde dip back # into Aquarius — at least one window, every one self-consistent. self.assertGreaterEqual(len(wins), 1) for a, b in wins: self.assertLess(a, b) self.assertEqual(_sign_at((a + b) / 2, 'Saturn'), 'Pisces') if a > lo + 1: # true boundary (not clipped by the scan bound) self.assertNotEqual(_sign_at(a - 0.2, 'Saturn'), 'Pisces') if b < hi - 1: self.assertNotEqual(_sign_at(b + 0.2, 'Saturn'), 'Pisces') def test_edges_refined_to_the_hour(self): wins = sign_windows('Sun', 'Aries', [(_jd('2020-01-01'), _jd('2020-12-31'))]) a, b = wins[0] # Within 1/24 day of the crossing on each side. self.assertEqual(_sign_at(a + 1 / 24, 'Sun'), 'Aries') self.assertNotEqual(_sign_at(a - 1 / 24, 'Sun'), 'Aries') self.assertEqual(_sign_at(b - 1 / 24, 'Sun'), 'Aries') self.assertNotEqual(_sign_at(b + 1 / 24, 'Sun'), 'Aries') def test_narrowed_windows_intersect_all_placements(self): base = [(_jd('1990-01-01'), _jd('2000-01-01'))] wins = narrowed_windows({'Uranus': 'Aquarius', 'Saturn': 'Pisces'}, base) self.assertGreaterEqual(len(wins), 1) for a, b in wins: mid = (a + b) / 2 self.assertEqual(_sign_at(mid, 'Uranus'), 'Aquarius') self.assertEqual(_sign_at(mid, 'Saturn'), 'Pisces') total = sum(b - a for a, b in wins) # Vastly narrower than the 10-year base, but a real window. self.assertGreater(total, 30) self.assertLess(total, 500) def test_narrowed_windows_empty_when_unreachable(self): # Saturn resided in Leo in the late 70s and mid 00s — never in the 90s. wins = narrowed_windows( {'Saturn': 'Leo'}, [(_jd('1990-01-01'), _jd('2000-01-01'))]) self.assertEqual(wins, []) def test_no_placements_keeps_the_base_window(self): base = [(_jd('1990-01-01'), _jd('2000-01-01'))] self.assertEqual(narrowed_windows({}, base), base) def test_present_signs_lists_reachable_signs_only(self): signs = present_signs( 'Saturn', [(_jd('1994-06-01'), _jd('1996-06-01'))]) # Pisces until the 1996 Aries ingress — exactly two reachable signs. self.assertEqual(signs, {'Pisces', 'Aries'}) def test_moon_windows_inside_a_narrow_window(self): # The end-game shape: by the Moon's turn the windows are days wide. wins = sign_windows('Moon', 'Cancer', [(_jd('2020-06-01'), _jd('2020-07-01'))]) self.assertEqual(len(wins), 1) a, b = wins[0] self.assertAlmostEqual(b - a, 2.4, delta=0.5) # ~2.4-day residence self.assertEqual(_sign_at((a + b) / 2, 'Moon'), 'Cancer')