Revision note (2026-07-15). Rewritten from the original proposal into a status tracker: the framework is roughly half implemented (commits
f4e84ae,0c703a1) and the suite passes — 62 tests, offline, ~7 s. Scope clarification: these tests are a safety net for the migration in DESIGN.md, written against the current API (okay,save_data,.df). They will be revised alongside the migration and do not need to anticipate the futureResult/OpAPI; a more complete test suite gets built out after the migration. NOMADS tests are deferred (see below).
Two constraints from the user:
- No network on every test run. Fixtures replay cached query results so
the default
pytestis offline and fast. - Detect schema changes infrequently. A separate, opt-in run hits the real services and validates response structure.
Standing decisions:
- Lean two-layer fixtures. VCR cassette for
open_meteo(the only query with non-trivial custom parsing of a binary FlatBuffers wire format); parquet/netcdf/rowset snapshots for the rest. Formonitor, snapshot the raw rowset soparse_rows/rowset_to_dataframeget exercised too. - pandera schemas as the single source of truth for query DataFrame contracts — reused by live schema tests and as documentation.
- pytest markers (
schema,slow) gate live/long tests; defaultpytestis offline. - Thin assertion layer on query tests. The fixtures and pandera schemas
are the durable assets — they validate the data, which survives the
DESIGN.md refactor unchanged. The assertions on the wrapper (
okay,save_data) are deliberately thin and against the current API; they get rewritten toResultstatus-gate assertions during DESIGN migration steps 2–6. Do not pre-build tests against the future API.
- Test deps in
pyproject.tomltestextra:pytest-recording,pytest-cov,pandera[pandas] -
[tool.pytest.ini_options]:testpaths,pythonpath = ["tests"],schema/slowmarkers,addopts = "-m 'not schema and not slow'" -
tests/conftest.py— path fixtures,vcr_config(match_on,decode_compressed_response, header filtering), andbypass_openmeteo_cache(monkeypatchesCACHE_SESSION/RETRY_SESSIONto a plainrequests.Sessionso VCR can intercept below requests-cache) -
tests/schemas/queries.py— schemas forOpenMeteoVla,OpenMeteoMultiSite,OpenMeteoVlaPressure,OpenMeteoVlaEnsemble,Ndfd,ApiQuery,WeatherStation;assert_herbie_datasethelper - Pure-function unit tests:
test_station.py,test_readdata.py,test_sensitivity_pure.py,test_plot_helpers.py,test_query_helpers.py -
unit/test_open_meteo_session.py(unplanned; regression tests for the 503/429 retry config added in5fb3dc6) -
query/test_ndfd.py+fixtures/dataframes/ndfd.parquet -
query/test_monitor.py+monitor_api_rows.pkl.gz/monitor_weather_rows.pkl.gz(mockedpsycopg2.connect, rowsets replayed byside_effectcall order) - Fixture refresh via
scripts/refresh_fixtures.py <source>(see the unified-script item below; per-source logic inscripts/refresh/) -
query/test_open_meteo.py+ four cassettes underfixtures/cassettes/. Conftest adds adefault_cassette_nameoverride keyed off a@pytest.mark.cassette("name")marker so the VLA tests share one recording. Recording revealed the date index isdatetime64[s, UTC], not the[ns]the schemas assumed — the four open-meteo schemas were corrected to match. -
schema_live/lane (test_live_{open_meteo,ndfd,herbie_maps,monitor}.py, all@pytest.mark.schema) +schema_live/conftest.pysocket-reachability skip guards. Herbie parametrized totcolw/mcc; monitor gates ondf is not None. The live run caught thatndfd.parquetwas stale ([ns]vs the current parser's[us, UTC]); regenerated it and setNdfdSchemato[us](now refreshed viarefresh_fixtures.py ndfd). - CI:
.github/workflows/test.yml— remote is GitHub (autocorr/site_forecast), so this applies as planned: PR/push trigger,uv sync --all-extras,pytest --cov=site_forecast, offline lane only. Independent of the migration. -
query/test_herbie_maps.py+fixtures/dataframes/herbie_tcolw_tiny.nc(524 KB) +refresh_fixtures.py herbie. Two layers: known-answer unit tests for the pure reductions (get_var_names,geodetic_to_number,subset_rectangular_region,add_coverage) and a fixture-replay integration test that mocksget_tcolwand runs the realsubset/extract_*/add_coveragepipeline offline (pick_pointsconfirmed to work on a round-tripped netcdf). Integration assertions are structural (assert_herbie_dataset) + invariants, not golden constants. -
unit/test_sensitivity.py+fixtures/sensitivity/om_{surface,pressure}.parquet+refresh_fixtures.py sensitivity. CoversAmModelPredictorprofile prep, real-amsmoke runs (AmModelPredictor.run,VlaSeasonalPredictor.run, and a truncated clear-skycompute()— all in the default lane, ~3 s),_add_derived_columnsknown-answer,_get_tcolw_df, the cloud/ensemble_run_seriesMultiIndex assembly (with_run_am_modelmocked), and the realmultiprocessing.Poolpath atn_workers=2(exercises_worker_initin forked workers + arg pickling + aggregation). Reuses the open-meteo schemas and the Herbie fixture. Caught two production bugs from the amwrapmainbump:sensitivity.py:129(.m→.value) and:418(str→Path(...)), both of which had silently zeroed out the sensitivity forecast; fixed and now guarded. - Unified
scripts/refresh_fixtures.py— single manual CLI over a sharedscripts/refresh/library. OneFixtureSourceper source (refresh()fetches live + writes atomically;load()signatures the current on-disk fixture). The CLI captures the signature before/after and prints a per-source diff — column/data_var adds/drops, dtype and index-resolution changes, and shape delta — the recurringdatetime64[s]/[ns]/[us]drift being the real target.open_meteohas noload(): itsrefresh()shells out topytest --record-mode=rewritethen replays the cassettes as the check.--all/ named subset /--list; no-arg is a safe no-op (never silently hits the network + DB).
None — the offline-suite milestone is complete. The next build-out (fuller
plotter/orchestrator tests, nomads, the slow end-to-end lane) happens
alongside/after the DESIGN.md migration.
nomads_cutout— the module was written some time ago but is not yet run in operations or wired to any consumer. It will eventually be fully implemented (see DESIGN.md Section 10); defer its offline test, fixture, pandera schema, and live schema test until it is brought into service after the migration. Do not build fixtures for it speculatively.slowend-to-end lane (instantiating the orchestrator with mocked queries and asserting outputs) — defer to the post-migration build-out, where it targetsForecast(profile, ctx)directly.
- Purpose. This suite exists so that DESIGN migration steps 2–10 can ship incrementally with a regression net under them. Coverage of the current behavior is the goal; elegance of the assertions is not.
- What survives the migration unchanged: replay fixtures (cassettes,
parquet/netcdf snapshots, pickled rowsets), the pandera schemas, the
pure-function unit tests, and the live
schemalane. - What gets rewritten during the migration: the thin wrapper assertions
in
tests/query/(assert q.okay→ status-gate assertions onq.result, then on operationResults;save_data()checks → generic serializer checks). This happens naturally as each DESIGN step lands; it is expected churn, not rework to avoid. - After the migration, build the fuller suite: plotters unit-tested
with hand-built
dict[str, Result],validate_profiletests, theslowend-to-end lane, and nomads once implemented.
open_meteo (cassette, done): pytest-recording records via
@pytest.mark.vcr. The module-level
CACHE_SESSION = requests_cache.CachedSession(".cache", expire_after=3600)
short-circuits HTTP at the requests-cache layer, so VCR (at urllib3) never
sees the call — the bypass_openmeteo_cache conftest fixture swaps in a
plain requests.Session for the duration of each test. Note this also
bypasses the retry adapter; the retry configuration is covered separately
by unit/test_open_meteo_session.py. Cassettes store the FlatBuffers binary
as base64 in YAML — opaque but functional.
ndfd (parquet, done): stores the parsed DataFrame. The text parser is
dead in offline tests — its schema-change detection is the responsibility
of the live schema test.
herbie_maps (netcdf, done): the refresh script produces a tiny
subset — ~50×50 grid centered on the VLA (all sub-hourly steps kept),
524 KB. get_tcolw is mocked to return the cached Dataset; the real
reduction pipeline runs offline. The quantile/mean-at-radius reductions are
checked as known-answer unit tests on synthetic grids (exact values) and as
invariants on the fixture-driven HerbieQuery. (The
exploratory full-size netcdfs — test_tcolw.netcdf ~403 MB and
test_tcolw_subset.netcdf — now live under data/old_data/, still
untracked; the committed fixture is produced fresh by the refresh script,
never copied from working data.)
monitor (pickled rowset, done): psycopg2.connect monkeypatched to a
mock whose cursor fetchall() returns pre-pickled raw rows by call order,
so parse_rows and rowset_to_dataframe — the real schema-bearing
parsers — execute against representative data.
tests/schemas/queries.py declares one DataFrameSchema per in-service
query class, plus an assert_herbie_dataset structural check for the
xarray-based Herbie query (pandera targets DataFrames). Two consumers:
- Offline test loads the fixture, asserts it satisfies the schema — catches accidental fixture mutation and serves as a documented contract.
- Live test (
-m schema) hits the real service, asserts the result satisfies the schema — primary mechanism for detecting upstream structural change.
Schemas declare columns, dtypes, nullability, and index/MultiIndex names.
Value ranges are deliberately not locked (those drift), only structural
shape. These schemas are also the foothold for the schema-validated
Result evolution sketched in DESIGN.md Section 10.
pytest— offline, fast, default (and the CI lane)pytest -m schema— live, run before releases or on a weekly cronpytest -m slow— end-to-end (post-migration)pytest -m ""— everything
The suite surfaces ~21 warnings in two families, both worth fixing before upstream deprecations become errors:
Pandas4Warning: 'd' is deprecatedfromStation.sun_rise_and_setsand from the tests themselves (pd.Timedelta("1d")→"1D"), plus oneconcatdefault-sorting deprecation.DeprecationWarning: get_cmap ... removed in 3.11from the Matplotlib colormap helpers.
uv sync --all-extrasinstalls cleanlyruff check . && ruff format --check .passespytest— all offline tests pass in well under a minute with no network trafficpytest -m schema— run manually once after adding a live testpytest --cov=site_forecast— coverage report generated- CI green once
.github/workflows/test.ymllands