feat: legibility — conditions, hints, kinder errors (Workstream B)
GET /api/conditions surfaces what the backend already computes: moon phase, veil thinness, geomagnetic state — junk lon degrades to moon-only instead of a 422, missing NOAA data means fewer lines, never an error. VeilConditions renders it in the séance side column, polling every 10 min. ModeHint: one in-fiction line per mode after 15s of an unused sensor, dismissed forever via localStorage. Error copy in evp/radio now detect-and-redirects (mic denied -> 'the board needs no ear'; no WebUSB -> try EVP) instead of dead-ending. No geolocation prompt from the conditions strip — asking for location from a passive readout would be hostile; ?lon= stays supported for callers that have it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> # Conflicts: # frontend/src/pages/SeancePage.tsx
This commit is contained in:
73
backend/tests/test_conditions.py
Normal file
73
backend/tests/test_conditions.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""GET /api/conditions — sky always present, geomagnetic nullable, lon lenient.
|
||||
|
||||
The geomagnetic cache is stubbed in every test so nothing here ever
|
||||
touches NOAA; a cold real cache would otherwise fire a live HTTP request.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import app.routes.conditions as conditions_module
|
||||
|
||||
|
||||
class _StubCache:
|
||||
def __init__(self, reading):
|
||||
self._reading = reading
|
||||
|
||||
async def reading(self):
|
||||
return self._reading
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def geo_null(monkeypatch):
|
||||
monkeypatch.setattr(conditions_module, "geomagnetic_cache", _StubCache(None))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def geo_storm(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
conditions_module,
|
||||
"geomagnetic_cache",
|
||||
_StubCache({"kp": 6.33, "label": "moderate geomagnetic storm",
|
||||
"disturbance": 6.33 / 9.0, "storm": True}),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sky_always_present_geomagnetic_null_tolerated(client, geo_null):
|
||||
resp = await client.get("/api/conditions")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["geomagnetic"] is None
|
||||
sky = body["sky"]
|
||||
assert sky["moon_name"]
|
||||
assert 0.0 <= sky["moon_illumination"] <= 1.0
|
||||
assert 0.0 <= sky["thinness"] <= 1.0
|
||||
# No longitude offered — witching proximity is unknowable.
|
||||
assert sky["witching_proximity"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_lon_yields_witching_proximity(client, geo_null):
|
||||
resp = await client.get("/api/conditions", params={"lon": "-3.7"})
|
||||
assert resp.status_code == 200
|
||||
witching = resp.json()["sky"]["witching_proximity"]
|
||||
assert witching is not None
|
||||
assert 0.0 <= witching <= 1.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("bad", ["ghost", "", "nan", "inf", "999", "-181"])
|
||||
async def test_invalid_lon_ignored_not_422(client, geo_null, bad):
|
||||
resp = await client.get("/api/conditions", params={"lon": bad})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["sky"]["witching_proximity"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_geomagnetic_reading_passed_through(client, geo_storm):
|
||||
resp = await client.get("/api/conditions")
|
||||
assert resp.status_code == 200
|
||||
geo = resp.json()["geomagnetic"]
|
||||
assert geo["kp"] == pytest.approx(6.33)
|
||||
assert geo["storm"] is True
|
||||
assert geo["label"] == "moderate geomagnetic storm"
|
||||
Reference in New Issue
Block a user