prop/test/microwaveprop/workers/gefs_fetch_worker_test.exs
Graham McIntire 0537a1831b
feat(gefs): ingest worker and grid-profile extraction
- GefsClient.build_profile/1: converts raw wgrib2 output into the
  scorer-shaped profile, deriving Td from RH via Magnus at both the
  surface and each pressure level
- GefsClient.fetch_grid_profiles/3: downloads the idx sidecar from
  NOMADS, byte-ranges only the surface messages, hands the slim
  GRIB2 binary to wgrib2 -lola for bilinear regridding from 0.5°
  onto the CONUS 0.125° grid
- GefsFetchWorker: one Oban job per (run_time, forecast_hour),
  upserts into gefs_profiles and runs SoundingParams.derive so the
  same refractivity/ducting metrics flow through

Oban gets a new :gefs queue (2 slots dev/config, 1 slot per prod
pod). Test env gets a gefs_req_options Req.Test stub slot so future
integration tests can drive the full fetch path. No cron wiring or
scorer integration yet — that's Step 3.
2026-04-18 14:36:03 -05:00

114 lines
3.3 KiB
Elixir

defmodule Microwaveprop.Workers.GefsFetchWorkerTest do
use Microwaveprop.DataCase, async: false
alias Microwaveprop.Weather
alias Microwaveprop.Weather.GefsProfile
alias Microwaveprop.Workers.GefsFetchWorker
describe "perform/1" do
test "rejects args missing run_time or forecast_hour" do
job = %Oban.Job{args: %{}}
assert {:cancel, _} = GefsFetchWorker.perform(job)
end
test "cancels when run_hour is not a valid GEFS cycle" do
args = %{
"run_time" => "2026-04-18T05:00:00Z",
"forecast_hour" => 24
}
assert {:cancel, _} = GefsFetchWorker.perform(%Oban.Job{args: args})
end
end
describe "build_profile_attrs/3" do
test "merges lat/lon/valid_time onto a GefsClient profile map" do
run_time = ~U[2026-04-18 12:00:00Z]
profile = %{
lat: 32.9,
lon: -97.0,
surface_temp_c: 20.0,
surface_dewpoint_c: 10.0,
surface_pressure_mb: 1013.0,
pwat_mm: 20.0,
wind_u_mps: 2.0,
wind_v_mps: -1.0,
cloud_cover_pct: 50.0,
precip_mm: 0.0,
profile: [%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 10.0, "hght" => 100.0}]
}
attrs = GefsFetchWorker.build_profile_attrs(run_time, 24, profile)
assert attrs.run_time == run_time
assert attrs.forecast_hour == 24
assert attrs.valid_time == ~U[2026-04-19 12:00:00Z]
assert attrs.lat == 32.9
assert attrs.surface_temp_c == 20.0
end
test "derives SoundingParams-style refractivity metrics when profile is populated" do
run_time = ~U[2026-04-18 12:00:00Z]
profile = %{
lat: 32.9,
lon: -97.0,
surface_temp_c: 20.0,
surface_dewpoint_c: 15.0,
surface_pressure_mb: 1013.0,
pwat_mm: 25.0,
profile: [
%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 15.0, "hght" => 100.0},
%{"pres" => 925.0, "tmpc" => 18.0, "dwpc" => 12.0, "hght" => 770.0},
%{"pres" => 850.0, "tmpc" => 14.0, "dwpc" => 8.0, "hght" => 1500.0}
]
}
attrs = GefsFetchWorker.build_profile_attrs(run_time, 24, profile)
assert is_float(attrs.surface_refractivity)
assert is_float(attrs.min_refractivity_gradient)
assert is_boolean(attrs.ducting_detected)
end
end
describe "storing profiles" do
test "round-trips a full batch through the context" do
run_time = ~U[2026-04-18 12:00:00Z]
fh = 24
valid_time = DateTime.add(run_time, fh * 3600, :second)
batch = [
%{
run_time: run_time,
forecast_hour: fh,
valid_time: valid_time,
lat: 32.9,
lon: -97.0,
surface_temp_c: 20.0,
surface_dewpoint_c: 10.0,
surface_pressure_mb: 1013.0,
pwat_mm: 20.0,
profile: []
},
%{
run_time: run_time,
forecast_hour: fh,
valid_time: valid_time,
lat: 33.0,
lon: -97.0,
surface_temp_c: 19.5,
surface_dewpoint_c: 9.5,
surface_pressure_mb: 1012.5,
pwat_mm: 19.0,
profile: []
}
]
{count, _} = Weather.upsert_gefs_profiles_batch(batch)
assert count == 2
persisted = Microwaveprop.Repo.all(GefsProfile)
assert length(persisted) == 2
end
end
end