thermograph/tests/test_grid.py
Emi Griffith 46c90914b3 Warm neighbor cells server-side (/cell?neighbors=1) (#51)
cache.js contained a JavaScript clone of backend/grid.py's snapping math
(its own comment said so) to compute the 8 surrounding cells and fire 8
staggered prefetch requests — grid geometry had two homes, one per
language, plus a client-side guess at Nominatim pacing.

The server now owns it: grid.neighbors(cell) steps one cell width from
the center and re-snaps (adjacent rows have different longitude steps;
poles and the antimeridian handled by snap), and /api/v2/cell grew a
neighbors=1 flag that enqueues those cells for a single background
worker. The warm-only guarantee matches prefetch=1 — a cell with no
cached archive is skipped, so no weather-API quota is ever spent — and
reverse_geocode's own lock paces the at-most-one Nominatim call per
never-labeled cell. Re-enqueues are TTL-deduped; the worker starts from
the lifespan hook, so tests and offline importers never spawn it.

The client now sends its one conditional bundle request with
neighbors=1 (a warm spot costs an empty 304) instead of skipping the
bundle and firing 8 extra requests; the grid-math clone and the
now-unused hasFreshCache are deleted.

Tests (114): grid.neighbors mid-latitude/pole/antimeridian, the
neighbors=1 enqueue + TTL dedupe, _warm_cell materializing the
history-derived store rows, and the never-fetch-upstream guarantee.
Verified with the headless-Chromium smoke across all five pages.
2026-07-11 20:47:31 +00:00

86 lines
2.7 KiB
Python

import math
import pytest
import grid
def test_snap_is_deterministic_and_contains_point():
a = grid.snap(47.6062, -122.3321)
b = grid.snap(47.6062, -122.3321)
assert a == b
assert a["bounds"]["south"] <= 47.6062 <= a["bounds"]["north"]
def test_snap_center_lands_in_same_cell():
cell = grid.snap(40.7128, -74.0060)
again = grid.snap(cell["center_lat"], cell["center_lon"])
assert again["id"] == cell["id"]
def test_from_id_round_trip():
cell = grid.snap(35.6762, 139.6503) # Tokyo — worldwide coverage
rebuilt = grid.from_id(cell["id"])
assert rebuilt == cell
def test_from_id_rejects_malformed():
with pytest.raises(ValueError):
grid.from_id("not-a-cell")
def test_longitude_wraps_across_antimeridian():
assert grid.snap(10.0, 200.0)["id"] == grid.snap(10.0, -160.0)["id"]
assert grid.snap(10.0, 540.0)["id"] == grid.snap(10.0, 180.0)["id"]
def test_latitude_clamped_at_poles():
cell = grid.snap(95.0, 0.0)
assert cell == grid.snap(90.0, 0.0)
assert -90.0 <= cell["center_lat"] <= 90.0
assert -180.0 <= cell["center_lon"] <= 180.0
def test_reported_center_is_a_valid_coordinate_everywhere():
for lat, lon in [(89.99, 179.99), (-89.99, -179.99), (0.0, 0.0), (66.5, -179.5)]:
cell = grid.snap(lat, lon)
assert -90.0 <= cell["center_lat"] <= 90.0
assert -180.0 <= cell["center_lon"] <= 180.0
def test_cells_stay_roughly_square():
# The cos(lat) scaling should keep the area near ~4 sq mi away from the poles.
for lat in (0.0, 30.0, 45.0, 60.0):
cell = grid.snap(lat, 10.0)
assert cell["area_sq_mi"] == pytest.approx(4.0, rel=0.15)
def test_lon_step_clamps_near_poles():
# cos(89°) ~ 0.017 would blow the step up; the 0.05 clamp caps it.
assert grid._lon_step(89.0) == pytest.approx(grid.LAT_STEP / 0.05)
assert not math.isinf(grid._lon_step(90.0))
def test_neighbors_mid_latitude():
cell = grid.snap(47.6062, -122.3321)
ns = grid.neighbors(cell)
assert len(ns) == 8
ids = {n["id"] for n in ns}
assert cell["id"] not in ids and len(ids) == 8
# Every neighbor is at most one row away and shares a border region.
i = int(cell["id"].split("_")[0])
assert all(abs(int(n["id"].split("_")[0]) - i) <= 1 for n in ns)
def test_neighbors_skip_rows_past_the_pole():
cell = grid.snap(89.99, 10.0) # topmost row
ns = grid.neighbors(cell)
assert 0 < len(ns) < 8 # no row above the pole
assert all(-90 <= n["center_lat"] <= 90 for n in ns)
def test_neighbors_wrap_across_antimeridian():
cell = grid.snap(10.0, 179.999)
ns = grid.neighbors(cell)
assert len(ns) == 8
assert any(n["center_lon"] < 0 for n in ns) # some sit past the date line