Dependency updates:
- phoenix_live_view: ~> 1.2.0 (was ~> 1.1.0)
- Transitive bumps: bandit 1.11.1→1.12.0, credo 1.7.18→1.7.19,
phoenix 1.8.7→1.8.8, req 0.5.18→0.6.1, swoosh 1.26.0→1.26.1
Fixes:
- core_components.ex: add 'type' to button :global allowlist
(Phoenix 1.8+ attribute validation)
- status_live_test.exs: invalidate {:all_stats} cache key to
prevent stale ETS cache from leaking between tests
- path_live_test.exs: accept all valid async states in assertion
(race between progress frame and compute completion)
- skewt_live_test.exs: accept loading or completed state in assertion
HEEx auto-formatting from mix format (self-closing tag normalization)
240 lines
8.1 KiB
Elixir
240 lines
8.1 KiB
Elixir
defmodule MicrowavepropWeb.StatusLiveTest do
|
|
use MicrowavepropWeb.ConnCase, async: false
|
|
|
|
import Ecto.Query
|
|
import Microwaveprop.AccountsFixtures
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Microwaveprop.Cache
|
|
alias Microwaveprop.Radio.Contact
|
|
alias Microwaveprop.Repo
|
|
alias Microwaveprop.Weather.NarrProfile
|
|
|
|
setup %{conn: conn} do
|
|
Cache.invalidate({MicrowavepropWeb.StatusLive, :count_unprocessed})
|
|
Cache.invalidate({MicrowavepropWeb.StatusLive, :stats})
|
|
Cache.invalidate({MicrowavepropWeb.StatusLive, :db_stats})
|
|
Cache.invalidate({MicrowavepropWeb.StatusLive, :grid_tasks_stats})
|
|
Cache.invalidate({MicrowavepropWeb.StatusLive, :all_stats})
|
|
|
|
# Wipe grid_tasks, contacts, and NARR profiles so tests don't interfere
|
|
Repo.delete_all(from(t in "grid_tasks", where: true))
|
|
Repo.delete_all(from(p in NarrProfile, where: true))
|
|
# Only delete contacts we created in tests — not the entire table
|
|
Repo.delete_all(
|
|
from(c in Contact,
|
|
where: c.station1 in ["W5TEST", "W5OFF", "W5QUE", "W5POST", "W5PUB", "W5PRV", "W5MAR", "K5MAR", "W5JUL"]
|
|
)
|
|
)
|
|
|
|
user = user_fixture()
|
|
{:ok, admin} = user |> Ecto.Changeset.change(%{is_admin: true}) |> Repo.update()
|
|
conn = log_in_user(conn, admin)
|
|
{:ok, conn: conn}
|
|
end
|
|
|
|
describe "grid_tasks panel — Rust prop-grid-rs worker" do
|
|
defp insert_grid_task(attrs) do
|
|
id = Ecto.UUID.bingenerate()
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
row =
|
|
Map.merge(
|
|
%{
|
|
id: id,
|
|
run_time: ~N[2026-04-19 19:00:00],
|
|
forecast_hour: 5,
|
|
valid_time: ~N[2026-04-20 00:00:00],
|
|
status: "queued",
|
|
attempt: 0,
|
|
inserted_at: now,
|
|
updated_at: now
|
|
},
|
|
attrs
|
|
)
|
|
|
|
{1, nil} = Repo.insert_all("grid_tasks", [row])
|
|
id
|
|
end
|
|
|
|
test "shows an in-flight row with the forecast hour it's working on", %{conn: conn} do
|
|
insert_grid_task(%{
|
|
status: "running",
|
|
forecast_hour: 7,
|
|
attempt: 1,
|
|
claimed_at: DateTime.truncate(DateTime.utc_now(), :microsecond)
|
|
})
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/status")
|
|
panel = lv |> element("#grid-tasks-panel") |> render()
|
|
assert panel =~ "prop-grid-rs"
|
|
assert panel =~ "f07"
|
|
assert panel =~ "running"
|
|
end
|
|
|
|
test "shows queued and completed counts", %{conn: conn} do
|
|
insert_grid_task(%{forecast_hour: 1, status: "queued"})
|
|
insert_grid_task(%{forecast_hour: 2, status: "queued"})
|
|
|
|
insert_grid_task(%{
|
|
forecast_hour: 3,
|
|
status: "done",
|
|
completed_at: DateTime.truncate(DateTime.utc_now(), :microsecond)
|
|
})
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/status")
|
|
panel = lv |> element("#grid-tasks-panel") |> render()
|
|
|
|
assert panel =~ ~r/Queued[^0-9]*<\/th>.*?<td>2<\/td>/s
|
|
assert panel =~ ~r/Done \(1h\)[^0-9]*<\/th>.*?<td>1<\/td>/s
|
|
end
|
|
end
|
|
|
|
defp create_narr_candidate(attrs \\ []) do
|
|
lat = Keyword.get(attrs, :lat, 33.0)
|
|
lon = Keyword.get(attrs, :lon, -97.0)
|
|
ts = Keyword.get(attrs, :qso_timestamp, ~U[2010-06-15 18:00:00Z])
|
|
|
|
{:ok, contact} =
|
|
%Contact{}
|
|
|> Contact.changeset(%{
|
|
station1: "W5TEST",
|
|
station2: "K5TEST",
|
|
qso_timestamp: ts,
|
|
mode: "CW",
|
|
band: Decimal.new("1296"),
|
|
grid1: "EM12",
|
|
grid2: "EM00",
|
|
pos1: %{"lat" => lat, "lon" => lon},
|
|
pos2: %{"lat" => lat + 0.1, "lon" => lon + 0.1},
|
|
distance_km: Decimal.new("300")
|
|
})
|
|
|> Ecto.Changeset.change(%{hrrr_status: :unavailable})
|
|
|> Repo.insert()
|
|
|
|
contact
|
|
end
|
|
|
|
# NARR fetches land in era5_profiles (table rename is a follow-up migration).
|
|
# The status UI labels this "NARR" now.
|
|
test "NARR progress bar numerator is actual profile matches not total-minus-candidates", %{conn: conn} do
|
|
create_narr_candidate(lat: 33.1, lon: -97.1)
|
|
|
|
# No NARR profile yet — candidates = 1, done = 0
|
|
{:ok, lv, _html} = live(conn, ~p"/status")
|
|
narr_row = lv |> element(~s|[data-progress-key="narr"]|) |> render()
|
|
assert narr_row =~ "NARR"
|
|
assert narr_row =~ "0 / 1"
|
|
|
|
# Insert a matching profile row — done should become 1
|
|
{:ok, _} =
|
|
Repo.insert(%NarrProfile{
|
|
valid_time: ~U[2010-06-15 18:00:00Z],
|
|
lat: 33.1,
|
|
lon: -97.1,
|
|
ducting_detected: false
|
|
})
|
|
|
|
Cache.invalidate({MicrowavepropWeb.StatusLive, :count_unprocessed})
|
|
Cache.invalidate({MicrowavepropWeb.StatusLive, :all_stats})
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/status")
|
|
narr_row = lv |> element(~s|[data-progress-key="narr"]|) |> render()
|
|
assert narr_row =~ "1 / 1"
|
|
end
|
|
|
|
# NARR serves pre-2014 contacts only (NCEI archive ends 2014-10-02). A
|
|
# post-2014 contact with hrrr_status=:unavailable is a HRRR retry case,
|
|
# not a NARR candidate, and must not inflate the NARR denominator.
|
|
test "NARR progress excludes post-2014 hrrr-unavailable contacts", %{conn: conn} do
|
|
# pre-2014 candidate (counts toward NARR)
|
|
create_narr_candidate(lat: 33.2, lon: -97.2)
|
|
|
|
# post-2014 hrrr-unavailable contact (does NOT count toward NARR)
|
|
{:ok, _} =
|
|
%Contact{}
|
|
|> Contact.changeset(%{
|
|
station1: "W5POST",
|
|
station2: "K5POST",
|
|
qso_timestamp: ~U[2020-06-15 18:00:00Z],
|
|
mode: "CW",
|
|
band: Decimal.new("1296"),
|
|
grid1: "EM12",
|
|
grid2: "EM00",
|
|
pos1: %{"lat" => 33.2, "lon" => -97.2},
|
|
pos2: %{"lat" => 33.3, "lon" => -97.3},
|
|
distance_km: Decimal.new("300")
|
|
})
|
|
|> Ecto.Changeset.change(%{hrrr_status: :unavailable})
|
|
|> Repo.insert()
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/status")
|
|
narr_row = lv |> element(~s|[data-progress-key="narr"]|) |> render()
|
|
assert narr_row =~ "0 / 1"
|
|
end
|
|
|
|
# NARR analyses are archived at 3-hour marks (00/03/06/09/12/15/18/21 UTC).
|
|
# `NarrClient.snap_to_analysis_hour/1` floors the QSO timestamp to the
|
|
# previous 3-hour mark before fetch. The status numerator query must mirror
|
|
# that snap — a naive ±30 min window around the raw qso_timestamp misses
|
|
# the stored profile whenever the contact lands more than 30 min past the
|
|
# last analysis hour.
|
|
test "NARR progress counts contacts whose qso_timestamp doesn't land on a 3-hour analysis mark",
|
|
%{conn: conn} do
|
|
{:ok, _} =
|
|
%Contact{}
|
|
|> Contact.changeset(%{
|
|
station1: "W5OFF",
|
|
station2: "K5OFF",
|
|
qso_timestamp: ~U[2010-06-15 18:50:00Z],
|
|
mode: "CW",
|
|
band: Decimal.new("1296"),
|
|
grid1: "EM12",
|
|
grid2: "EM00",
|
|
pos1: %{"lat" => 33.3, "lon" => -97.3},
|
|
pos2: %{"lat" => 33.4, "lon" => -97.4},
|
|
distance_km: Decimal.new("300")
|
|
})
|
|
|> Ecto.Changeset.change(%{hrrr_status: :unavailable})
|
|
|> Repo.insert()
|
|
|
|
{:ok, _} =
|
|
Repo.insert(%NarrProfile{
|
|
valid_time: ~U[2010-06-15 18:00:00Z],
|
|
lat: 33.3,
|
|
lon: -97.3,
|
|
ducting_detected: false
|
|
})
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/status")
|
|
narr_row = lv |> element(~s|[data-progress-key="narr"]|) |> render()
|
|
assert narr_row =~ "1 / 1"
|
|
end
|
|
|
|
# NARR eligibility is defined by qso_timestamp, not hrrr_status. A
|
|
# pre-2014 contact whose hrrr_status is still :queued (the ping-pong
|
|
# window before reconcile flips it) must still be counted — otherwise
|
|
# the denominator misses rows for several cron cycles after submission.
|
|
test "NARR candidates include pre-2014 contacts regardless of hrrr_status", %{conn: conn} do
|
|
{:ok, _} =
|
|
%Contact{}
|
|
|> Contact.changeset(%{
|
|
station1: "W5QUE",
|
|
station2: "K5QUE",
|
|
qso_timestamp: ~U[2010-06-15 18:00:00Z],
|
|
mode: "CW",
|
|
band: Decimal.new("1296"),
|
|
grid1: "EM12",
|
|
grid2: "EM00",
|
|
pos1: %{"lat" => 33.4, "lon" => -97.4},
|
|
pos2: %{"lat" => 33.5, "lon" => -97.5},
|
|
distance_km: Decimal.new("300")
|
|
})
|
|
|> Ecto.Changeset.change(%{hrrr_status: :queued})
|
|
|> Repo.insert()
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/status")
|
|
narr_row = lv |> element(~s|[data-progress-key="narr"]|) |> render()
|
|
assert narr_row =~ "0 / 1"
|
|
end
|
|
end
|