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