Both RTMA and METAR5 schemas + clients + workers were defined but never had a consumer. `find_nearest_rtma/3` and `recent_surface_obs/3` existed in `Microwaveprop.Weather` with zero callers cluster-wide; RtmaFetchWorker had test coverage but was never enqueued anywhere; no IEM 5-min ASOS fetcher was ever written. The tables sat empty in prod and the recalibrate audit was permanently flagging them BROKEN even though the empty state was the steady state. Drop the lot — schemas, clients, workers, queue config, Req.Test stubs, audit checks, and the per-table unique tests in the observation-changesets suite. New migration drops the now-unreferenced `rtma_observations` and `metar_5min_observations` tables (both 0 rows in prod). Trim the `:rtma` slot out of `backfill_only_queues` in runtime.exs so Oban Pro's Smart engine stops trying to manage a queue with no producer. Audit thresholds reset on the surviving NARR check: drop the "WARN < 5000 absolute rows" rule that didn't account for NarrFetchWorker's 0.13° spatial dedup, and point remediation at the existing BackfillEnqueueWorker cron instead of the dev-only `mix narr.backfill` task. scripts/recalibrate_algo.py: ROW_COUNT_SOURCES + DATA_GAP_SQL + DATA_GAP_RULES all stop referencing the dropped tables.
187 lines
6 KiB
Elixir
187 lines
6 KiB
Elixir
defmodule Microwaveprop.SpaceWeather.ObservationChangesetsTest do
|
|
@moduledoc """
|
|
Cast/validate coverage for the three space-weather observation
|
|
schemas. These were historically at ~50 % line coverage because
|
|
only the happy path of `swpc_client` exercised `changeset/2` — the
|
|
required/optional splits and the unique constraints had no direct
|
|
assertions.
|
|
"""
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Microwaveprop.Ionosphere.Observation, as: IonoObs
|
|
alias Microwaveprop.SpaceWeather.GeomagneticObservation
|
|
alias Microwaveprop.SpaceWeather.SolarFluxObservation
|
|
alias Microwaveprop.SpaceWeather.SolarXrayObservation
|
|
alias Microwaveprop.Weather.HrrrClimatology
|
|
|
|
describe "GeomagneticObservation.changeset/2" do
|
|
test "valid with only valid_time" do
|
|
cs = GeomagneticObservation.changeset(%GeomagneticObservation{}, %{valid_time: ~U[2026-04-21 00:00:00Z]})
|
|
assert cs.valid?
|
|
end
|
|
|
|
test "accepts kp_index and estimated_kp" do
|
|
cs =
|
|
GeomagneticObservation.changeset(%GeomagneticObservation{}, %{
|
|
valid_time: ~U[2026-04-21 00:00:00Z],
|
|
kp_index: 4,
|
|
estimated_kp: 4.33
|
|
})
|
|
|
|
assert cs.valid?
|
|
assert Ecto.Changeset.get_change(cs, :kp_index) == 4
|
|
assert Ecto.Changeset.get_change(cs, :estimated_kp) == 4.33
|
|
end
|
|
|
|
test "invalid when valid_time is missing" do
|
|
cs = GeomagneticObservation.changeset(%GeomagneticObservation{}, %{kp_index: 3})
|
|
refute cs.valid?
|
|
assert %{valid_time: ["can't be blank"]} = errors_on(cs)
|
|
end
|
|
end
|
|
|
|
describe "SolarFluxObservation.changeset/2" do
|
|
test "valid with required valid_time" do
|
|
cs = SolarFluxObservation.changeset(%SolarFluxObservation{}, %{valid_time: ~U[2026-04-21 00:00:00Z]})
|
|
assert cs.valid?
|
|
end
|
|
|
|
test "accepts frequency, flux, schedule, and 90-day mean" do
|
|
cs =
|
|
SolarFluxObservation.changeset(%SolarFluxObservation{}, %{
|
|
valid_time: ~U[2026-04-21 00:00:00Z],
|
|
frequency_mhz: 2800,
|
|
flux_sfu: 142.4,
|
|
reporting_schedule: "daily",
|
|
ninety_day_mean: 150.0
|
|
})
|
|
|
|
assert cs.valid?
|
|
end
|
|
|
|
test "invalid when valid_time is missing" do
|
|
cs = SolarFluxObservation.changeset(%SolarFluxObservation{}, %{flux_sfu: 142.4})
|
|
refute cs.valid?
|
|
assert %{valid_time: ["can't be blank"]} = errors_on(cs)
|
|
end
|
|
end
|
|
|
|
describe "SolarXrayObservation.changeset/2" do
|
|
test "valid with required valid_time" do
|
|
cs = SolarXrayObservation.changeset(%SolarXrayObservation{}, %{valid_time: ~U[2026-04-21 00:00:00Z]})
|
|
assert cs.valid?
|
|
end
|
|
|
|
test "casts satellite, energy_band, flux, and electron_contaminated" do
|
|
cs =
|
|
SolarXrayObservation.changeset(%SolarXrayObservation{}, %{
|
|
valid_time: ~U[2026-04-21 00:00:00Z],
|
|
satellite: 16,
|
|
flux_wm2: 1.23e-6,
|
|
energy_band: "0.1-0.8nm",
|
|
electron_contaminated: true
|
|
})
|
|
|
|
assert cs.valid?
|
|
assert Ecto.Changeset.get_change(cs, :electron_contaminated) == true
|
|
end
|
|
|
|
test "invalid when valid_time is missing" do
|
|
cs = SolarXrayObservation.changeset(%SolarXrayObservation{}, %{flux_wm2: 1.0e-7})
|
|
refute cs.valid?
|
|
assert %{valid_time: ["can't be blank"]} = errors_on(cs)
|
|
end
|
|
end
|
|
|
|
describe "Ionosphere.Observation.changeset/2" do
|
|
test "valid with required station_code + valid_time" do
|
|
cs =
|
|
IonoObs.changeset(%IonoObs{}, %{
|
|
station_code: "BC840",
|
|
valid_time: ~U[2026-04-21 00:00:00Z]
|
|
})
|
|
|
|
assert cs.valid?
|
|
end
|
|
|
|
test "casts scaled characteristics" do
|
|
cs =
|
|
IonoObs.changeset(%IonoObs{}, %{
|
|
station_code: "BC840",
|
|
valid_time: ~U[2026-04-21 00:00:00Z],
|
|
confidence_score: 75,
|
|
fo_f2_mhz: 10.4,
|
|
fo_e_mhz: 3.2,
|
|
fo_es_mhz: 6.1,
|
|
hm_f2_km: 275.0,
|
|
mufd_mhz: 18.5
|
|
})
|
|
|
|
assert cs.valid?
|
|
assert Ecto.Changeset.get_change(cs, :fo_f2_mhz) == 10.4
|
|
assert Ecto.Changeset.get_change(cs, :confidence_score) == 75
|
|
end
|
|
|
|
test "invalid when either required field is missing" do
|
|
cs1 = IonoObs.changeset(%IonoObs{}, %{valid_time: ~U[2026-04-21 00:00:00Z]})
|
|
cs2 = IonoObs.changeset(%IonoObs{}, %{station_code: "BC840"})
|
|
|
|
refute cs1.valid?
|
|
refute cs2.valid?
|
|
assert %{station_code: ["can't be blank"]} = errors_on(cs1)
|
|
assert %{valid_time: ["can't be blank"]} = errors_on(cs2)
|
|
end
|
|
end
|
|
|
|
describe "HrrrClimatology.changeset/2" do
|
|
test "valid with the four required keys" do
|
|
cs =
|
|
HrrrClimatology.changeset(%HrrrClimatology{}, %{
|
|
lat: 32.9,
|
|
lon: -97.0,
|
|
month: 7,
|
|
hour: 15
|
|
})
|
|
|
|
assert cs.valid?
|
|
end
|
|
|
|
test "casts mean / stddev / sample_count" do
|
|
cs =
|
|
HrrrClimatology.changeset(%HrrrClimatology{}, %{
|
|
lat: 32.9,
|
|
lon: -97.0,
|
|
month: 7,
|
|
hour: 15,
|
|
mean_surface_temp_c: 30.5,
|
|
stddev_surface_temp_c: 4.2,
|
|
sample_count: 365
|
|
})
|
|
|
|
assert cs.valid?
|
|
assert Ecto.Changeset.get_change(cs, :mean_surface_temp_c) == 30.5
|
|
assert Ecto.Changeset.get_change(cs, :sample_count) == 365
|
|
end
|
|
|
|
test "invalid when any of the four cell-identity keys is missing" do
|
|
for missing <- [:lat, :lon, :month, :hour] do
|
|
attrs = Map.delete(%{lat: 32.9, lon: -97.0, month: 7, hour: 15}, missing)
|
|
|
|
cs = HrrrClimatology.changeset(%HrrrClimatology{}, attrs)
|
|
refute cs.valid?, "expected #{missing} missing to invalidate"
|
|
assert errors_on(cs)[missing] == ["can't be blank"]
|
|
end
|
|
end
|
|
end
|
|
|
|
# Local copy of the standard Phoenix.DataCase `errors_on/1` so this
|
|
# file can stay on the lightweight `ExUnit.Case` (no DB sandbox
|
|
# needed for changeset tests).
|
|
defp errors_on(changeset) do
|
|
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
|
|
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
|
|
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
|
end)
|
|
end)
|
|
end
|
|
end
|