prop/test/microwaveprop/propagation/pipeline_status_test.exs
Graham McIntire cb8445f329
fix: resolve 158/183 credo --strict warnings
- Add 17 missing @spec annotations (layouts, error_json, error_html, skewt_svg)
- Move 12+ nested alias/import/require to module top level
- Add phx-change/id attributes to 11 raw HTML <form> tags
- Remove 4 unused LiveView assigns (:bounds, :data_provider)
- Add 3 missing doctest references (HrrrNativeClient, BulkFetch, Accounts)
- Break 2 long lines (path_compute.ex:382)
- Strengthen weak test assertions (is_binary→byte_size, is_list→!=[])
- Replace Module.concat with Module.safe_concat (2 occurrences)
- Replace length/1 > 0 with list != [] (9 occurrences)
- Remove no-op assert true, fix no-assertion tests

Remaining: 24 socket.assigns introspection warnings (deliberate test
pattern for observable behavior testing), 1 formatter-resistant long
line, 3 app-code usage warnings.
2026-06-12 15:47:15 -05:00

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 byte_size(status.label) > 0
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