prop/test/microwaveprop/space_weather/observation_changesets_test.exs
Graham McIntire 1086f52c85
chore: delete dead RTMA + METAR5 code paths
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.
2026-05-04 15:09:19 -05:00

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