The main map page now reads band, selected time, and map center/zoom from URL params and updates the URL as the user pans, zooms, picks a band, or scrubs the timeline. Pan/zoom use push_patch replace-mode so browser history isn't spammed, while band and time changes push fresh entries. Also fix the 'Up to date · just now ago' pipeline chip — 'just now' already reads as an instant so the trailing 'ago' is redundant. Any older duration keeps it.
213 lines
6.4 KiB
Elixir
213 lines
6.4 KiB
Elixir
defmodule Microwaveprop.Propagation.PipelineStatusTest do
|
|
use Microwaveprop.DataCase, async: false
|
|
|
|
alias Microwaveprop.Propagation.PipelineStatus
|
|
alias Microwaveprop.Propagation.ScoresFile
|
|
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 a single grid-worker detail when only 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 == "Updating propagation"
|
|
assert [%{worker: :grid, label: grid_label}] = status.details
|
|
assert grid_label =~ "HRRR"
|
|
end
|
|
|
|
test "returns :running with a single ASOS detail when only 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 == "Updating propagation"
|
|
assert [%{worker: :asos, label: asos_label}] = status.details
|
|
assert asos_label =~ "ASOS"
|
|
end
|
|
|
|
test "returns :running with BOTH workers when grid + ASOS are executing at the same time" do
|
|
# Queue concurrency was bumped to 2 so the chain-step grid worker
|
|
# and the 10-minute ASOS nudge can run in parallel. The chip
|
|
# should show one sub-line per running worker, in a deterministic
|
|
# grid-before-asos order so the eye doesn't see them flicker.
|
|
insert_oban_job(%{
|
|
state: "executing",
|
|
worker: @grid_worker,
|
|
attempted_at: minutes_ago(5)
|
|
})
|
|
|
|
insert_oban_job(%{
|
|
state: "executing",
|
|
worker: @asos_worker,
|
|
attempted_at: minutes_ago(1)
|
|
})
|
|
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :running
|
|
assert status.label == "Updating propagation"
|
|
assert [grid, asos] = status.details
|
|
assert grid.worker == :grid
|
|
assert grid.label =~ "HRRR"
|
|
assert asos.worker == :asos
|
|
assert asos.label =~ "ASOS"
|
|
end
|
|
|
|
test "returns :idle with Up to date label when a recent ScoresFile exists" do
|
|
ScoresFile.write!(
|
|
10_000,
|
|
minutes_ago(15),
|
|
[%{lat: 25.0, lon: -125.0, score: 50}]
|
|
)
|
|
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :idle
|
|
assert status.last_update_at
|
|
assert status.label =~ "Up to date"
|
|
end
|
|
|
|
test "drops the 'ago' suffix when the data is brand new" do
|
|
ScoresFile.write!(
|
|
10_000,
|
|
DateTime.utc_now(),
|
|
[%{lat: 25.0, lon: -125.0, score: 50}]
|
|
)
|
|
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :idle
|
|
assert status.label =~ "Up to date · just now"
|
|
refute status.label =~ "just now ago"
|
|
end
|
|
|
|
test "returns :stale when the newest ScoresFile is more than 120 minutes old" do
|
|
ScoresFile.write!(
|
|
10_000,
|
|
minutes_ago(180),
|
|
[%{lat: 25.0, lon: -125.0, score: 50}]
|
|
)
|
|
|
|
status = PipelineStatus.current()
|
|
|
|
assert status.state == :stale
|
|
assert status.label =~ "stale"
|
|
end
|
|
|
|
test "prefers :running over :idle when a job is executing even with a recent ScoresFile" do
|
|
ScoresFile.write!(
|
|
10_000,
|
|
minutes_ago(5),
|
|
[%{lat: 25.0, lon: -125.0, score: 50}]
|
|
)
|
|
|
|
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_detail/1" do
|
|
test "valid_time within half an hour of now renders as 'through now'" do
|
|
now = DateTime.utc_now()
|
|
assert PipelineStatus.running_detail(%{valid_time: now}) == "through now"
|
|
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 20 * 60, :second)}) == "through now"
|
|
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, -20 * 60, :second)}) == "through now"
|
|
end
|
|
|
|
test "valid_time in the future renders as 'through +Nh'" do
|
|
now = DateTime.utc_now()
|
|
|
|
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 3 * 3600, :second)}) ==
|
|
"through +3h"
|
|
|
|
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 12 * 3600, :second)}) ==
|
|
"through +12h"
|
|
end
|
|
|
|
test "valid_time in the past renders as 'through Nh ago'" do
|
|
now = DateTime.utc_now()
|
|
|
|
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, -2 * 3600, :second)}) ==
|
|
"through 2h ago"
|
|
end
|
|
|
|
test "returns nil when no valid_time is provided" do
|
|
assert PipelineStatus.running_detail(nil) == nil
|
|
assert PipelineStatus.running_detail(%{}) == nil
|
|
assert PipelineStatus.running_detail(%{forecast_hour: 3}) == nil
|
|
end
|
|
end
|
|
end
|