prop/test/microwaveprop_web/live/map_live_test.exs
Graham McIntire 0ed47db8b6
Process one forecast hour per PropagationGridWorker perform
A full f00-f18 sweep takes ~170 min of wall time, longer than the
~90 min gap between deploys on this cluster. Over the last 7 days
every run was killed mid-sweep and zero runs completed. The
propagation_scores table only ever held the scraps from partial
runs that landed before the pod died, which is why the map looked
like it was showing "the current hour" (or nothing).

The worker now processes exactly one forecast hour per perform/1
(~8-10 min) and enqueues the next hour as a fresh Oban job. A cron
fire with empty args seeds the chain at f00; subsequent runs carry
run_time + forecast_hour args. At f18 the chain stops and the
pruner runs. Each step broadcasts propagation:updated immediately
so new hours appear on the map as they land.

Wall time per perform drops from ~170 min to ~10 min, so Lifeline's
2h rescue window is no longer a factor and a pod restart loses at
most one forecast hour instead of the whole sweep. Retries and
max_attempts now describe a single fh, not the whole chain.

Also: bump the :propagation queue from 1 to 2 slots so
PropagationPruneWorker can run alongside the chain job, drop the
worker timeout/1 from 90 min to 20 min to match single-fh runs,
and pull Lifeline rescue_after from 120 min to 45 min to keep the
safety net above the step timeout.

Adds a "Data from HH:MM UTC · Nh ago/now/+Nh" indicator above the
pipeline chip in both the mobile and desktop sidebars so the user
can tell which valid_time the map is showing when the bottom
timeline isn't visible.
2026-04-14 13:24:59 -05:00

190 lines
6 KiB
Elixir

defmodule MicrowavepropWeb.MapLiveTest do
use MicrowavepropWeb.ConnCase, async: false
import Phoenix.LiveViewTest
describe "mount" do
test "renders full-page map", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map")
assert html =~ ~s(id="propagation-map")
assert html =~ ~s(phx-hook="PropagationMap")
end
test "renders band dropdown with all bands", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map")
assert html =~ "10 GHz"
assert html =~ "24 GHz"
assert html =~ "47 GHz"
assert html =~ "75 GHz"
assert html =~ "241 GHz"
end
test "defaults to 10 GHz selected", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map")
assert html =~ ~s(class="active")
end
test "includes data-scores attribute", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map")
assert html =~ "data-scores"
end
test "renders grid toggle", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map")
assert html =~ "Grid squares"
assert html =~ ~s(phx-click="toggle_grid")
end
test "renders scoring algorithm link", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map")
assert html =~ "Scoring Algorithm"
assert html =~ ~s(/algo)
end
test "renders submit QSO link", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map")
assert html =~ "Submit a Contact"
assert html =~ ~s(/submit)
end
end
describe "select_band" do
test "updates selected band on event", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/map")
html = render_click(lv, "select_band", %{"value" => 24_000})
assert html =~ "24 GHz"
end
end
describe "antenna height" do
test "renders antenna height input with default 33 ft", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map")
assert html =~ ~s(name="height_ft")
assert html =~ ~s(value="33")
assert html =~ "ft"
end
test "updates antenna height on change", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/map")
html = render_change(lv, "set_antenna_height", %{"height_ft" => "20"})
assert html =~ ~s(value="20")
end
end
describe "compute_viewshed" do
test "accepts compute_viewshed event without crashing", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/map")
# Event should be accepted; async computation may fail (no SRTM in test)
# but the LiveView should not crash
render_click(lv, "compute_viewshed", %{"lat" => 32.9, "lon" => -97.0})
# If we get here without crash, the event handler exists
assert render(lv) =~ "propagation-map"
end
end
describe "toggle_grid" do
test "toggles grid visibility", %{conn: conn} do
{:ok, lv, html} = live(conn, ~p"/map")
refute html =~ "checked"
html = render_click(lv, "toggle_grid")
assert html =~ "checked"
html = render_click(lv, "toggle_grid")
refute html =~ "checked"
end
end
describe "data timestamp indicator" do
test "renders a 'Data from …' line on both mobile and desktop sidebars", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map")
assert html =~ ~s(id="data-timestamp-mobile")
assert html =~ ~s(id="data-timestamp-desktop")
# Clean test DB has no scores; the indicator falls back to "No data".
assert html =~ "No data"
end
end
describe "format_data_timestamp/1" do
alias MicrowavepropWeb.MapLive
test "returns 'No data' for nil" do
assert MapLive.format_data_timestamp(nil) == "No data"
end
test "renders the valid_time and a 'now' suffix when within ±30 minutes" do
now = DateTime.utc_now()
label = MapLive.format_data_timestamp(now)
assert label =~ "UTC"
assert label =~ "now"
end
test "renders a '+Nh' suffix when the valid_time is in the future" do
future = DateTime.add(DateTime.utc_now(), 3 * 3600, :second)
assert MapLive.format_data_timestamp(future) =~ "+3h"
end
test "renders a 'Nh ago' suffix when the valid_time is in the past" do
past = DateTime.add(DateTime.utc_now(), -4 * 3600, :second)
assert MapLive.format_data_timestamp(past) =~ "4h ago"
end
end
describe "pipeline status chip" do
test "renders the aggregated pipeline status chip on mount", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map")
# Both the mobile and desktop sidebars carry their own chip.
assert html =~ ~s(id="pipeline-status-mobile")
assert html =~ ~s(id="pipeline-status-desktop")
# With a clean test DB no pipeline workers have ever run.
assert html =~ ~s(data-pipeline-state="unknown")
end
test "shows the current forecast hour while PropagationGridWorker runs", %{conn: conn} do
# Put a grid-worker row in executing state so PipelineStatus.current/0
# reports :running. Bypassing Oban's normal insert keeps the SQL
# sandbox from running the worker inline.
now = DateTime.utc_now()
Microwaveprop.Repo.insert!(
struct!(Oban.Job, %{
state: "executing",
queue: "propagation",
worker: "Microwaveprop.Workers.PropagationGridWorker",
args: %{},
attempt: 1,
max_attempts: 3,
inserted_at: now,
scheduled_at: now,
attempted_at: now
})
)
{:ok, lv, html} = live(conn, ~p"/map")
# Before any progress message arrives the chip uses the base label.
assert html =~ "Updating propagation"
assert html =~ ~s(data-pipeline-state="running")
Phoenix.PubSub.broadcast(
Microwaveprop.PubSub,
"propagation:pipeline",
{:propagation_pipeline_progress, %{forecast_hour: 3, valid_time: ~U[2026-04-14 18:00:00Z]}}
)
html = render(lv)
assert html =~ "+3h"
Phoenix.PubSub.broadcast(
Microwaveprop.PubSub,
"propagation:pipeline",
{:propagation_pipeline_progress, %{forecast_hour: 0, valid_time: ~U[2026-04-14 15:00:00Z]}}
)
html = render(lv)
assert html =~ "now"
end
end
end