prop/test/microwaveprop/propagation/pipeline_status_test.exs
Graham McIntire 434fca2ad1
Show forecast-hour progress on the map pipeline chip
PropagationGridWorker now broadcasts a {:propagation_pipeline_progress,
%{forecast_hour:, valid_time:}} message on the propagation:pipeline
PubSub topic at the start of each process_forecast_hour/4 call.
MapLive subscribes on mount, stashes the last message in the new
:pipeline_progress assign, and clears it on the refresh tick whenever
PipelineStatus flips out of :running.

The chip component consults a new PipelineStatus.running_label/1
helper that formats the payload as "Updating propagation · now" for
f00 and "Updating propagation · +Nh" for f01-f18, falling back to
the base running label until the first progress message arrives.
Covered by unit tests on the formatter and an integration test in
MapLiveTest that seeds an executing grid job, broadcasts progress,
and asserts the rendered chip.
2026-04-14 11:12:51 -05:00

163 lines
4.4 KiB
Elixir

defmodule Microwaveprop.Propagation.PipelineStatusTest do
use Microwaveprop.DataCase, async: false
alias Microwaveprop.Propagation.PipelineStatus
alias Microwaveprop.Repo
@grid_worker "Microwaveprop.Workers.PropagationGridWorker"
@asos_worker "Microwaveprop.Workers.AsosAdjustmentWorker"
# Bypass Oban's normal insert pipeline (which executes inline in test)
# by writing directly to the Oban.Job schema. The SQL sandbox keeps
# Oban's executor from seeing these rows.
defp insert_oban_job(overrides) do
now = DateTime.utc_now()
defaults = %{
state: "available",
queue: "propagation",
worker: @grid_worker,
args: %{},
attempt: 0,
max_attempts: 3,
inserted_at: now,
scheduled_at: now
}
attrs = Map.merge(defaults, Map.new(overrides))
Repo.insert!(struct!(Oban.Job, attrs))
end
defp minutes_ago(n) do
DateTime.add(DateTime.utc_now(), -n * 60, :second)
end
describe "current/0" do
test "returns :unknown when no pipeline jobs have ever run" do
status = PipelineStatus.current()
assert status.state == :unknown
assert status.last_update_at == nil
assert is_binary(status.label)
end
test "returns :running with grid worker label when PropagationGridWorker is executing" do
insert_oban_job(%{
state: "executing",
worker: @grid_worker,
attempted_at: minutes_ago(5)
})
status = PipelineStatus.current()
assert status.state == :running
assert status.label =~ "HRRR"
end
test "returns :running with ASOS label when AsosAdjustmentWorker is executing" do
insert_oban_job(%{
state: "executing",
worker: @asos_worker,
attempted_at: minutes_ago(1)
})
status = PipelineStatus.current()
assert status.state == :running
assert status.label =~ "ASOS"
end
test "returns :idle with Up to date label when last grid run completed recently" do
completed = minutes_ago(15)
insert_oban_job(%{
state: "completed",
worker: @grid_worker,
attempted_at: completed,
completed_at: completed
})
status = PipelineStatus.current()
assert status.state == :idle
assert status.last_update_at
assert status.label =~ "Up to date"
end
test "returns :stale when last completed was more than 120 minutes ago" do
stale = minutes_ago(180)
insert_oban_job(%{
state: "completed",
worker: @grid_worker,
attempted_at: stale,
completed_at: stale
})
status = PipelineStatus.current()
assert status.state == :stale
assert status.label =~ "stale"
end
test "prefers :running over :idle when a job is executing even if a recent job completed" do
recent = minutes_ago(5)
insert_oban_job(%{
state: "completed",
worker: @grid_worker,
attempted_at: recent,
completed_at: recent
})
insert_oban_job(%{
state: "executing",
worker: @grid_worker,
attempted_at: minutes_ago(1)
})
status = PipelineStatus.current()
assert status.state == :running
end
test "ignores completed jobs from unrelated workers" do
# WeatherFetchWorker enriches individual QSOs — it's not part of
# the CONUS grid update pipeline, so its recent success should
# not make the map page show 'Up to date'.
completed = minutes_ago(5)
insert_oban_job(%{
state: "completed",
worker: "Microwaveprop.Workers.WeatherFetchWorker",
queue: "weather",
attempted_at: completed,
completed_at: completed
})
status = PipelineStatus.current()
assert status.state == :unknown
end
end
describe "running_label/1" do
test "forecast hour 0 renders as 'now'" do
assert PipelineStatus.running_label(%{forecast_hour: 0}) ==
"Updating propagation · now"
end
test "positive forecast hours render as +Nh" do
assert PipelineStatus.running_label(%{forecast_hour: 1}) ==
"Updating propagation · +1h"
assert PipelineStatus.running_label(%{forecast_hour: 18}) ==
"Updating propagation · +18h"
end
test "returns nil when no forecast hour is provided" do
assert PipelineStatus.running_label(nil) == nil
assert PipelineStatus.running_label(%{}) == nil
end
end
end