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.
163 lines
4.4 KiB
Elixir
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
|