Set the Game Clock — ephemeris narrowing (hard CSP): placements shrink the REAL date window; narrowed range prints below the prompt; unreachable signs dim + reject — TDD

- PySwiss gains its FIRST reverse lookup: GET /api/windows/?placements=
  Uranus:Aquarius,…&next=Saturn — sign_windows (per-planet stride scan +
  bisection edge refine to the hour) folds thru narrowed_windows
  (slowest planet first, each scan restricted to the prior intersection
  so the fast bodies only ever scan slivers) over the game window
  (settings GAME_WINDOW_START/END, default 1781-03-13 — Uranus's
  discovery — → 2100-12-31, the snapshot span); present_signs reports
  the next planet's reachable signs. Self-validating UTs (every window
  forward-checked at midpoint + edges) + 8 API ITs
- epic clock_windows endpoint (lazy, table_sky-shaped — room views stay
  HTTP-free) proxies the lookup, cached per room+placements (six felts
  polling one ritual state = ONE upstream call; failures cached 60s);
  fails OPEN {available:false} when PySwiss is unreachable
- place_clock_planet enforces the HARD constraint: a sign outside the
  narrowed windows' reach → 409 sign_unreachable; fail-open w.o PySwiss
  (the ritual never bricks on microservice downtime); PlaceClockPlanet
  ITs sever PySwiss in setUp so the turn walk stays deterministic
  against a live local service
- felt: #id_clock_windows readout below the prompt for ALL viewers —
  "1995-04-01 → 1998-04-17 · 2 windows" — fetched at parse + after own
  placement + on every clock_placement broadcast; drawRim opts gain
  allowedSigns → unreachable wedges .nw-sign--blocked (dimmed, inert,
  no handlers); SkyWheelSpec R10/R11
- SeedMapClockNarrowingTest FT stubs PySwiss in-process (real proxy,
  real gating): readout renders, blocked Aries won't place, allowed
  Pisces lands Saturn, readout re-narrows

[[project-voronoi-spec]]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-10 12:30:16 -04:00
parent b2ddd98956
commit 32db203543
15 changed files with 888 additions and 7 deletions

View File

@@ -0,0 +1,81 @@
"""
Integration tests for GET /api/windows/ — the reverse ephemeris lookup
(Set the Game Clock narrowing, EarthmanRPG roadmap step 21).
The endpoint folds `placements` (Planet:Sign pairs) into the intersected date
windows where every placement holds simultaneously, bounded by the game window
(settings GAME_WINDOW_START/END — defaults span the precomputed snapshot
range, 1781-03-13 → 2100-12-31), and reports which signs the `next` planet can
reach within them. Tests override the game window to a tight decade so the
real ephemeris scan stays fast + deterministic.
Run:
pyswiss/.venv/Scripts/python pyswiss/manage.py test apps.charts
"""
from django.test import SimpleTestCase, override_settings
@override_settings(GAME_WINDOW_START='1990-01-01', GAME_WINDOW_END='2000-01-01')
class WindowsApiTest(SimpleTestCase):
def test_empty_placements_returns_the_full_game_window(self):
response = self.client.get('/api/windows/')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(len(data['windows']), 1)
self.assertTrue(data['windows'][0]['start'].startswith('1990-01-01'))
self.assertTrue(data['windows'][0]['end'].startswith('2000-01-01'))
self.assertAlmostEqual(data['days'], 3652, delta=2)
def test_placements_narrow_the_windows(self):
response = self.client.get(
'/api/windows/',
{'placements': 'Uranus:Aquarius,Saturn:Pisces'},
)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertGreaterEqual(len(data['windows']), 1)
self.assertLess(data['days'], 500)
for w in data['windows']:
self.assertLess(w['start'], w['end'])
def test_next_reports_reachable_signs(self):
response = self.client.get(
'/api/windows/',
{'placements': 'Saturn:Pisces', 'next': 'Jupiter'},
)
self.assertEqual(response.status_code, 200)
nxt = response.json()['next']
self.assertEqual(nxt['planet'], 'Jupiter')
self.assertEqual(len(nxt['signs']), 12)
reachable = [s for s, ok in nxt['signs'].items() if ok]
# Jupiter (1-year residence) reaches SOME but not ALL signs inside
# Saturn's ~3-year Pisces windows.
self.assertGreater(len(reachable), 0)
self.assertLess(len(reachable), 12)
def test_unreachable_placement_yields_empty_windows(self):
response = self.client.get(
'/api/windows/', {'placements': 'Saturn:Leo', 'next': 'Jupiter'})
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data['windows'], [])
self.assertEqual(data['days'], 0)
self.assertEqual(
[s for s, ok in data['next']['signs'].items() if ok], [])
def test_invalid_planet_400(self):
self.assertEqual(self.client.get(
'/api/windows/', {'placements': 'Vulcan:Aries'}).status_code, 400)
def test_invalid_sign_400(self):
self.assertEqual(self.client.get(
'/api/windows/', {'placements': 'Saturn:Notasign'}).status_code, 400)
def test_invalid_next_400(self):
self.assertEqual(self.client.get(
'/api/windows/', {'next': 'Vulcan'}).status_code, 400)
def test_malformed_placements_400(self):
self.assertEqual(self.client.get(
'/api/windows/', {'placements': 'SaturnPisces'}).status_code, 400)