Add backend test suite; gate direct pushes; serialize LAN deploys (#41)
- backend/tests: 74 hermetic tests (no network, no repo data//logs/ writes)
covering grid snapping/round-trips, grading percentiles/bands/windows/
dry streaks, the places index (norm, one-edit matchers, search,
corrections), the derived store (token validity, cache=False, degraded
mode), and route-level API tests over a faked climate layer — routing,
validation, ETag/304 revalidation, store replay, the /cell bundle, and
the v1/v2 aliases. The API tests would have caught the /place
AttributeError regression.
- requirements-dev.txt + make test (venv prefers uv-pinned 3.12, matching
deploy-dev.sh — pyarrow wheels stop at 3.12 and some pyenv builds lack
sqlite).
- CI: extract the build job into a reusable build.yml, add the test run
and an API health probe (page-only curl can't catch route wiring
faults); deploy-dev.yml now runs the same build gate before deploying
direct pushes, which previously deployed with no CI at all.
- Deploys serialize under one dev-lan-deploy concurrency group across
both workflows (previously per-PR groups could interleave two deploys
to the same checkout), and are never cancelled mid-restart.
- deploy-dev.sh health check also probes /api/v2/place — best-effort
externals mean a failure there is a genuine server bug.
2026-07-11 19:37:49 +00:00
|
|
|
"""Route-level tests over the FastAPI app with the weather/geocode layer faked —
|
|
|
|
|
they exercise the real routing, validation, derived-store and ETag plumbing, and
|
|
|
|
|
would catch wiring regressions (e.g. a handler calling a deleted helper)."""
|
|
|
|
|
import pandas as pd
|
|
|
|
|
import pytest
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
|
|
|
|
import app as appmod
|
|
|
|
|
import climate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def client(monkeypatch, history, recent):
|
|
|
|
|
monkeypatch.setattr(climate, "get_history",
|
|
|
|
|
lambda cell: (history.copy(), {"cached": True, "cache_age_days": 3}))
|
|
|
|
|
monkeypatch.setattr(climate, "get_recent_forecast", lambda cell: recent.copy())
|
|
|
|
|
monkeypatch.setattr(climate, "load_cached_history", lambda cell: history.copy())
|
|
|
|
|
monkeypatch.setattr(climate, "recent_stamp", lambda cell_id: "rs-test")
|
|
|
|
|
monkeypatch.setattr(climate, "reverse_geocode", lambda lat, lon: "Testville, Washington")
|
|
|
|
|
return TestClient(appmod.app)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Q = {"lat": 47.6062, "lon": -122.3321}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_place_serves_any_point_worldwide(client):
|
|
|
|
|
for q in (Q, {"lat": 48.8566, "lon": 2.3522}, {"lat": -33.8688, "lon": 151.2093}):
|
|
|
|
|
r = client.get("/thermograph/api/v2/place", params=q)
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
assert r.json()["place"] == "Testville, Washington"
|
|
|
|
|
assert set(r.json()["cell"]) == {"center_lat", "center_lon"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_grade_shape_and_conditional_revalidation(client, history):
|
|
|
|
|
r = client.get("/thermograph/api/v2/grade", params=Q)
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["place"] == "Testville, Washington"
|
|
|
|
|
assert body["climatology"]["tmax"] is not None
|
|
|
|
|
days = [d["date"] for d in body["recent"]]
|
|
|
|
|
assert days == sorted(days, reverse=True) # newest first
|
|
|
|
|
assert body["recent"][0]["tmax"]["grade"]
|
|
|
|
|
|
|
|
|
|
etag = r.headers["etag"]
|
|
|
|
|
r304 = client.get("/thermograph/api/v2/grade", params=Q,
|
|
|
|
|
headers={"If-None-Match": etag})
|
|
|
|
|
assert r304.status_code == 304 and r304.headers["etag"] == etag
|
|
|
|
|
|
|
|
|
|
# Without the validator the derived store replays the exact same bytes.
|
|
|
|
|
r2 = client.get("/thermograph/api/v2/grade", params=Q)
|
|
|
|
|
assert r2.status_code == 200 and r2.content == r.content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_grade_is_aliased_across_api_versions(client):
|
|
|
|
|
for prefix in ("api", "api/v1", "api/v2"):
|
|
|
|
|
assert client.get(f"/thermograph/{prefix}/grade", params=Q).status_code == 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_query_validation_rejects_out_of_range(client):
|
|
|
|
|
assert client.get("/thermograph/api/v2/grade",
|
|
|
|
|
params={"lat": 999, "lon": 0}).status_code == 422
|
|
|
|
|
assert client.get("/thermograph/api/v2/grade",
|
|
|
|
|
params={**Q, "days": 0}).status_code == 422
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_day_detail_and_ladders(client, history):
|
|
|
|
|
r = client.get("/thermograph/api/v2/day", params=Q)
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["latest"] == pd.Timestamp(history["date"].max()).date().isoformat()
|
|
|
|
|
tmax = body["detail"]["metrics"]["tmax"]
|
|
|
|
|
assert tmax["ladder"]["tiers"][0]["c"] == "rec-hot"
|
|
|
|
|
assert tmax["obs"]["grade"]
|
|
|
|
|
|
|
|
|
|
|
Single-source cache identity; shared fetch preamble and cache flow (#43)
The derived store's key/token formats existed in three places — each
endpoint, api_cell's slice assembly, and migrate.py — where any drift
would silently split the cache (endpoints missing rows the bundle wrote,
migrate materializing rows nobody reads). They are now defined once in
views.py (grade_key/calendar_key/day_key/forecast_key, history_token/
recent_token/day_token) and consumed everywhere, with a pinning test so
a format change is always deliberate.
The four data endpoints shared two copy-pasted sequences, now helpers:
- _fetch_history: history (+ optional recent bundle) fetch with upstream
failures mapped to clean HTTP errors and an empty record to 404.
- _cached_response: If-None-Match 304 / token-valid store replay /
build + persist + serve, with calendar's dont-persist-placeless rule
as an explicit flag.
Each endpoint is now its audit run + identity + a build callback (~10
lines); api_day's hourly-token special case moved into day_token. New
tests: identity format pins, rate-limit 503 parametrized across all five
data routes, prefetch=1 never touching upstream (cold 204, warm
history-only slices), and calendar's placeless-payload retry behavior.
2026-07-11 19:49:15 +00:00
|
|
|
@pytest.mark.parametrize("path", ["grade", "calendar", "day", "forecast", "cell"])
|
|
|
|
|
def test_every_data_route_maps_rate_limit_to_503(client, monkeypatch, path):
|
Add backend test suite; gate direct pushes; serialize LAN deploys (#41)
- backend/tests: 74 hermetic tests (no network, no repo data//logs/ writes)
covering grid snapping/round-trips, grading percentiles/bands/windows/
dry streaks, the places index (norm, one-edit matchers, search,
corrections), the derived store (token validity, cache=False, degraded
mode), and route-level API tests over a faked climate layer — routing,
validation, ETag/304 revalidation, store replay, the /cell bundle, and
the v1/v2 aliases. The API tests would have caught the /place
AttributeError regression.
- requirements-dev.txt + make test (venv prefers uv-pinned 3.12, matching
deploy-dev.sh — pyarrow wheels stop at 3.12 and some pyenv builds lack
sqlite).
- CI: extract the build job into a reusable build.yml, add the test run
and an API health probe (page-only curl can't catch route wiring
faults); deploy-dev.yml now runs the same build gate before deploying
direct pushes, which previously deployed with no CI at all.
- Deploys serialize under one dev-lan-deploy concurrency group across
both workflows (previously per-PR groups could interleave two deploys
to the same checkout), and are never cancelled mid-restart.
- deploy-dev.sh health check also probes /api/v2/place — best-effort
externals mean a failure there is a genuine server bug.
2026-07-11 19:37:49 +00:00
|
|
|
def rate_limited(cell):
|
2026-07-11 19:53:46 +00:00
|
|
|
raise climate.WeatherUnavailable(climate.limit_message(False))
|
Add backend test suite; gate direct pushes; serialize LAN deploys (#41)
- backend/tests: 74 hermetic tests (no network, no repo data//logs/ writes)
covering grid snapping/round-trips, grading percentiles/bands/windows/
dry streaks, the places index (norm, one-edit matchers, search,
corrections), the derived store (token validity, cache=False, degraded
mode), and route-level API tests over a faked climate layer — routing,
validation, ETag/304 revalidation, store replay, the /cell bundle, and
the v1/v2 aliases. The API tests would have caught the /place
AttributeError regression.
- requirements-dev.txt + make test (venv prefers uv-pinned 3.12, matching
deploy-dev.sh — pyarrow wheels stop at 3.12 and some pyenv builds lack
sqlite).
- CI: extract the build job into a reusable build.yml, add the test run
and an API health probe (page-only curl can't catch route wiring
faults); deploy-dev.yml now runs the same build gate before deploying
direct pushes, which previously deployed with no CI at all.
- Deploys serialize under one dev-lan-deploy concurrency group across
both workflows (previously per-PR groups could interleave two deploys
to the same checkout), and are never cancelled mid-restart.
- deploy-dev.sh health check also probes /api/v2/place — best-effort
externals mean a failure there is a genuine server bug.
2026-07-11 19:37:49 +00:00
|
|
|
monkeypatch.setattr(climate, "get_history", rate_limited)
|
Single-source cache identity; shared fetch preamble and cache flow (#43)
The derived store's key/token formats existed in three places — each
endpoint, api_cell's slice assembly, and migrate.py — where any drift
would silently split the cache (endpoints missing rows the bundle wrote,
migrate materializing rows nobody reads). They are now defined once in
views.py (grade_key/calendar_key/day_key/forecast_key, history_token/
recent_token/day_token) and consumed everywhere, with a pinning test so
a format change is always deliberate.
The four data endpoints shared two copy-pasted sequences, now helpers:
- _fetch_history: history (+ optional recent bundle) fetch with upstream
failures mapped to clean HTTP errors and an empty record to 404.
- _cached_response: If-None-Match 304 / token-valid store replay /
build + persist + serve, with calendar's dont-persist-placeless rule
as an explicit flag.
Each endpoint is now its audit run + identity + a build callback (~10
lines); api_day's hourly-token special case moved into day_token. New
tests: identity format pins, rate-limit 503 parametrized across all five
data routes, prefetch=1 never touching upstream (cold 204, warm
history-only slices), and calendar's placeless-payload retry behavior.
2026-07-11 19:49:15 +00:00
|
|
|
# A distinct spot per route: a store row cached by an earlier test would
|
|
|
|
|
# otherwise be checked only after the (failing) history fetch anyway.
|
|
|
|
|
r = client.get(f"/thermograph/api/v2/{path}", params={"lat": 51.5, "lon": -0.1})
|
Add backend test suite; gate direct pushes; serialize LAN deploys (#41)
- backend/tests: 74 hermetic tests (no network, no repo data//logs/ writes)
covering grid snapping/round-trips, grading percentiles/bands/windows/
dry streaks, the places index (norm, one-edit matchers, search,
corrections), the derived store (token validity, cache=False, degraded
mode), and route-level API tests over a faked climate layer — routing,
validation, ETag/304 revalidation, store replay, the /cell bundle, and
the v1/v2 aliases. The API tests would have caught the /place
AttributeError regression.
- requirements-dev.txt + make test (venv prefers uv-pinned 3.12, matching
deploy-dev.sh — pyarrow wheels stop at 3.12 and some pyenv builds lack
sqlite).
- CI: extract the build job into a reusable build.yml, add the test run
and an API health probe (page-only curl can't catch route wiring
faults); deploy-dev.yml now runs the same build gate before deploying
direct pushes, which previously deployed with no CI at all.
- Deploys serialize under one dev-lan-deploy concurrency group across
both workflows (previously per-PR groups could interleave two deploys
to the same checkout), and are never cancelled mid-restart.
- deploy-dev.sh health check also probes /api/v2/place — best-effort
externals mean a failure there is a genuine server bug.
2026-07-11 19:37:49 +00:00
|
|
|
assert r.status_code == 503
|
|
|
|
|
assert "rate-limited" in r.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 19:53:46 +00:00
|
|
|
def test_upstream_failure_classification(client, monkeypatch):
|
|
|
|
|
q = {"lat": 52.52, "lon": 13.4}
|
|
|
|
|
|
|
|
|
|
def daily_quota(cell):
|
|
|
|
|
raise climate.WeatherUnavailable(climate.limit_message(True), daily=True)
|
|
|
|
|
monkeypatch.setattr(climate, "get_history", daily_quota)
|
|
|
|
|
r = client.get("/thermograph/api/v2/grade", params=q)
|
|
|
|
|
assert r.status_code == 503 and "tomorrow" in r.json()["detail"]
|
|
|
|
|
|
|
|
|
|
# A raw upstream 429 that no fetcher classified still maps to a clean 503.
|
|
|
|
|
def raw_429(cell):
|
|
|
|
|
e = RuntimeError("upstream said no")
|
|
|
|
|
e.response = type("R", (), {"status_code": 429})()
|
|
|
|
|
raise e
|
|
|
|
|
monkeypatch.setattr(climate, "get_history", raw_429)
|
|
|
|
|
r = client.get("/thermograph/api/v2/grade", params=q)
|
|
|
|
|
assert r.status_code == 503 and "rate-limited" in r.json()["detail"]
|
|
|
|
|
|
|
|
|
|
# Anything else is a genuine upstream fault: 502 with the raw error.
|
|
|
|
|
def boom(cell):
|
|
|
|
|
raise RuntimeError("parquet cache corrupted")
|
|
|
|
|
monkeypatch.setattr(climate, "get_history", boom)
|
|
|
|
|
r = client.get("/thermograph/api/v2/grade", params=q)
|
|
|
|
|
assert r.status_code == 502 and "parquet cache corrupted" in r.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
Single-source cache identity; shared fetch preamble and cache flow (#43)
The derived store's key/token formats existed in three places — each
endpoint, api_cell's slice assembly, and migrate.py — where any drift
would silently split the cache (endpoints missing rows the bundle wrote,
migrate materializing rows nobody reads). They are now defined once in
views.py (grade_key/calendar_key/day_key/forecast_key, history_token/
recent_token/day_token) and consumed everywhere, with a pinning test so
a format change is always deliberate.
The four data endpoints shared two copy-pasted sequences, now helpers:
- _fetch_history: history (+ optional recent bundle) fetch with upstream
failures mapped to clean HTTP errors and an empty record to 404.
- _cached_response: If-None-Match 304 / token-valid store replay /
build + persist + serve, with calendar's dont-persist-placeless rule
as an explicit flag.
Each endpoint is now its audit run + identity + a build callback (~10
lines); api_day's hourly-token special case moved into day_token. New
tests: identity format pins, rate-limit 503 parametrized across all five
data routes, prefetch=1 never touching upstream (cold 204, warm
history-only slices), and calendar's placeless-payload retry behavior.
2026-07-11 19:49:15 +00:00
|
|
|
def test_cell_prefetch_never_fetches_upstream(client, monkeypatch):
|
|
|
|
|
def boom(cell):
|
|
|
|
|
raise AssertionError("prefetch=1 must never fetch weather upstream")
|
|
|
|
|
monkeypatch.setattr(climate, "get_history", boom)
|
|
|
|
|
monkeypatch.setattr(climate, "get_recent_forecast", boom)
|
|
|
|
|
monkeypatch.setattr(climate, "load_cached_history", lambda cell: None)
|
|
|
|
|
r = client.get("/thermograph/api/v2/cell", params={**Q, "prefetch": 1})
|
|
|
|
|
assert r.status_code == 204 # cold cell: no body, no quota spent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cell_prefetch_builds_history_slices_only(client):
|
|
|
|
|
r = client.get("/thermograph/api/v2/cell", params={"lat": 10.0, "lon": 10.0, "prefetch": 1})
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
assert set(r.json()["slices"]) == {"calendar", "day"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_calendar_placeless_payload_is_not_persisted(client, monkeypatch):
|
|
|
|
|
q = {"lat": -10.0, "lon": 20.0, "months": 2}
|
|
|
|
|
monkeypatch.setattr(climate, "reverse_geocode", lambda lat, lon: None)
|
|
|
|
|
r = client.get("/thermograph/api/v2/calendar", params=q)
|
|
|
|
|
assert r.status_code == 200 and r.json()["place"] is None
|
|
|
|
|
# The placeless payload wasn't cached, so the next request retries the
|
|
|
|
|
# label instead of replaying bare coordinates for the life of the token.
|
|
|
|
|
monkeypatch.setattr(climate, "reverse_geocode", lambda lat, lon: "Resolved, Now")
|
|
|
|
|
assert client.get("/thermograph/api/v2/calendar", params=q).json()["place"] == "Resolved, Now"
|
|
|
|
|
|
|
|
|
|
|
Add backend test suite; gate direct pushes; serialize LAN deploys (#41)
- backend/tests: 74 hermetic tests (no network, no repo data//logs/ writes)
covering grid snapping/round-trips, grading percentiles/bands/windows/
dry streaks, the places index (norm, one-edit matchers, search,
corrections), the derived store (token validity, cache=False, degraded
mode), and route-level API tests over a faked climate layer — routing,
validation, ETag/304 revalidation, store replay, the /cell bundle, and
the v1/v2 aliases. The API tests would have caught the /place
AttributeError regression.
- requirements-dev.txt + make test (venv prefers uv-pinned 3.12, matching
deploy-dev.sh — pyarrow wheels stop at 3.12 and some pyenv builds lack
sqlite).
- CI: extract the build job into a reusable build.yml, add the test run
and an API health probe (page-only curl can't catch route wiring
faults); deploy-dev.yml now runs the same build gate before deploying
direct pushes, which previously deployed with no CI at all.
- Deploys serialize under one dev-lan-deploy concurrency group across
both workflows (previously per-PR groups could interleave two deploys
to the same checkout), and are never cancelled mid-restart.
- deploy-dev.sh health check also probes /api/v2/place — best-effort
externals mean a failure there is a genuine server bug.
2026-07-11 19:37:49 +00:00
|
|
|
def test_calendar_compact_range(client, history):
|
|
|
|
|
r = client.get("/thermograph/api/v2/calendar", params={**Q, "months": 2})
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["range"]["end"] == pd.Timestamp(history["date"].max()).date().isoformat()
|
|
|
|
|
day = body["days"][0]
|
|
|
|
|
assert {"date", "dsr", "tmax", "tmin", "precip"} <= set(day)
|
|
|
|
|
assert set(day["tmax"]) == {"v", "pct", "c", "g"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_forecast_grades_future_days(client):
|
|
|
|
|
r = client.get("/thermograph/api/v2/forecast", params=Q)
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["forecast"] is True
|
|
|
|
|
days = [d["date"] for d in body["recent"]]
|
|
|
|
|
assert days and days == sorted(days, reverse=True) # furthest-out first
|
|
|
|
|
assert min(days) > body["target_date"] # strictly future
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cell_bundle_matches_per_view_payloads(client):
|
|
|
|
|
r = client.get("/thermograph/api/v2/cell", params=Q)
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
slices = r.json()["slices"]
|
|
|
|
|
assert set(slices) == {"calendar", "grade", "forecast", "day"}
|
|
|
|
|
for s in slices.values():
|
|
|
|
|
assert s["etag"] and s["data"]
|
|
|
|
|
# The grade slice must be byte-for-byte what /grade serves (same store row).
|
|
|
|
|
grade = client.get("/thermograph/api/v2/grade", params=Q)
|
|
|
|
|
assert grade.json() == slices["grade"]["data"]
|
|
|
|
|
assert grade.headers["etag"] == slices["grade"]["etag"]
|
|
|
|
|
|
|
|
|
|
r304 = client.get("/thermograph/api/v2/cell", params=Q,
|
|
|
|
|
headers={"If-None-Match": r.headers["etag"]})
|
|
|
|
|
assert r304.status_code == 304
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_suggest_falls_back_to_upstream_geocoder(client, monkeypatch):
|
|
|
|
|
upstream = [{"name": "Seattle", "admin1": "Washington", "country": "United States",
|
|
|
|
|
"country_code": "US", "lat": 47.6, "lon": -122.33, "population": 737015}]
|
|
|
|
|
monkeypatch.setattr(climate, "geocode", lambda q, count=5: list(upstream))
|
|
|
|
|
r = client.get("/thermograph/api/v2/suggest", params={"q": "seattle-fallback-probe"})
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["results"][0]["name"] == "Seattle"
|
|
|
|
|
assert body["corrected"] is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_pages_serve_with_origin_filled_in(client):
|
|
|
|
|
r = client.get("/thermograph/")
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
assert "text/html" in r.headers["content-type"]
|
|
|
|
|
assert "__ORIGIN__" not in r.text
|
|
|
|
|
assert client.head("/thermograph/calendar").status_code == 200
|
2026-07-11 20:47:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cell_neighbors_flag_enqueues_warming(client, monkeypatch):
|
|
|
|
|
import app as appmod
|
|
|
|
|
monkeypatch.setattr(appmod, "_WARM_SEEN", {})
|
|
|
|
|
# No lifespan in TestClient without a context manager, so no worker drains
|
|
|
|
|
# the queue — enqueued cells just accumulate for inspection.
|
|
|
|
|
monkeypatch.setattr(appmod, "_warm_queue", __import__("queue").Queue())
|
|
|
|
|
r = client.get("/thermograph/api/v2/cell", params={"lat": 35.0, "lon": 25.0, "neighbors": 1})
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
assert appmod._warm_queue.qsize() == 8
|
|
|
|
|
assert len(appmod._WARM_SEEN) == 8
|
|
|
|
|
# Same spot again within the TTL: nothing new enqueued.
|
|
|
|
|
client.get("/thermograph/api/v2/cell", params={"lat": 35.0, "lon": 25.0, "neighbors": 1},
|
|
|
|
|
headers={"If-None-Match": r.headers["etag"]})
|
|
|
|
|
assert appmod._warm_queue.qsize() == 8
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_warm_cell_materializes_history_slices(client, tmp_store, monkeypatch, history):
|
|
|
|
|
import app as appmod
|
|
|
|
|
import grid
|
|
|
|
|
import views
|
|
|
|
|
cell = grid.snap(-33.87, 151.21)
|
|
|
|
|
appmod._warm_cell(cell)
|
|
|
|
|
token = views.history_token(history)
|
|
|
|
|
start_ts, end_ts = views.cal_span(history, None, None, 24)
|
|
|
|
|
assert tmp_store.get_payload("calendar", cell["id"],
|
|
|
|
|
views.calendar_key(start_ts, end_ts, 24), token) is not None
|
|
|
|
|
import pandas as pd
|
|
|
|
|
last = pd.Timestamp(history["date"].max()).normalize()
|
|
|
|
|
assert tmp_store.get_payload("day", cell["id"], views.day_key(last), token) is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_warm_cell_never_fetches_upstream(client, monkeypatch):
|
|
|
|
|
import app as appmod
|
|
|
|
|
def boom(cell):
|
|
|
|
|
raise AssertionError("warming must never fetch weather upstream")
|
|
|
|
|
monkeypatch.setattr(climate, "get_history", boom)
|
|
|
|
|
monkeypatch.setattr(climate, "get_recent_forecast", boom)
|
|
|
|
|
monkeypatch.setattr(climate, "load_cached_history", lambda cell: None)
|
|
|
|
|
appmod._warm_cell({"id": "1_1", "center_lat": 0.03, "center_lon": 0.03}) # cold: no-op
|