fix(prediction): correct dataset mapping

This commit is contained in:
gili8420 2026-08-04 13:42:10 +09:00
parent 7f70ebdd96
commit a53adc4585
3 changed files with 107 additions and 3 deletions

View file

@ -0,0 +1,74 @@
from django.test import SimpleTestCase
from .serializers import PredictionRequestSerializer
class LaunchLongitudeTests(SimpleTestCase):
"""
This API does not bound longitude. The predictor does.
Longitude lives on a circle: every real number names a real meridian, and
5170 is a legitimate way to write 50 E. So any interval is a policy about
typos, not about places, and there is exactly one place that policy belongs
the service that owns the wind grid. Two layers each guessing at it is how the
original defect happened: this serializer declared min_value=0, narrower than
the endpoint it calls, and refused every launch west of Greenwich. Canada,
Greenland and Alaska, i.e. most of the Arctic sites this product targets.
The endpoint actually called is GET /api/v1/prediction, which applies no
longitude bound at all it normalises and lets the grid refuse what it cannot
use. Mirroring that exactly is the point: this layer must never be the narrower
one.
Nuuk is -51.7. The same meridian written unsigned is 308.3, and that always
worked, which is what showed the constraint was about notation, not place.
Non-numbers are still refused here, by FloatField. NaN and infinity are not:
FloatField parses them, and the predictor's request decoder rejects them with a
400 (measured). Deliberately not re-checked here, so this layer keeps a single
responsibility.
"""
def payload(self, lng):
return {
"launch_latitude": 64.1,
"launch_longitude": lng,
"launch_datetime": "2026-08-05T12:00:00Z",
"launch_altitude": 0,
"ascent_rate": 5,
"burst_altitude": 30000,
"descent_rate": 5,
"profile": "standard_profile",
}
def assert_accepted(self, lng):
s = PredictionRequestSerializer(data=self.payload(lng))
self.assertTrue(s.is_valid(), f"lng={lng} rejected: {s.errors}")
def test_accepts_a_western_launch(self):
self.assert_accepted(-51.7)
def test_accepts_the_same_meridian_written_unsigned(self):
self.assert_accepted(308.3)
def test_accepts_every_notation_the_predictor_accepts(self):
# Measured against GET /api/v1/prediction: each of these returns 200, and
# the pairs below name the same meridian, so each returns the same
# trajectory as its twin. -180/180 are one meridian; 0/360 are one
# meridian; -90/270 are one meridian.
for lng in (-180, 180, 0, 360, -90, 270, -200, -180.0001, 359.999):
with self.subTest(lng=lng):
self.assert_accepted(lng)
def test_does_not_bound_the_range(self):
# 1e30 is nonsense, and it is the predictor that says so — it answers 400
# "lng=1e+30 out of range". Asserting acceptance here is the explicit
# statement that range is not this layer's job; a bound added here would
# again risk being narrower than the service behind it.
self.assert_accepted(1e30)
def test_still_refuses_things_that_are_not_numbers(self):
for lng in ("abc", None, "", []):
with self.subTest(lng=lng):
s = PredictionRequestSerializer(data=self.payload(lng))
self.assertFalse(s.is_valid(), f"lng={lng!r} accepted but is not a number")