diff --git a/lib/microwaveprop/accounts/user_api_token.ex b/lib/microwaveprop/accounts/user_api_token.ex index 615e171b..3464ac8d 100644 --- a/lib/microwaveprop/accounts/user_api_token.ex +++ b/lib/microwaveprop/accounts/user_api_token.ex @@ -36,6 +36,13 @@ defmodule Microwaveprop.Accounts.UserApiToken do @type t :: %__MODULE__{} + @spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() + def changeset(token, attrs) do + token + |> cast(attrs, [:name, :user_id]) + |> foreign_key_constraint(:user_id) + end + @doc "Returns the bearer-token prefix (e.g. `mwp_`)." @spec token_prefix() :: String.t() def token_prefix, do: @prefix diff --git a/lib/microwaveprop/beacons/beacon.ex b/lib/microwaveprop/beacons/beacon.ex index 8eb414f0..ba5eb364 100644 --- a/lib/microwaveprop/beacons/beacon.ex +++ b/lib/microwaveprop/beacons/beacon.ex @@ -198,6 +198,7 @@ defmodule Microwaveprop.Beacons.Beacon do |> validate_number(:lon, greater_than_or_equal_to: -180, less_than_or_equal_to: 180) |> validate_length(:callsign, min: 3, max: 10) |> validate_grid_format() + |> foreign_key_constraint(:user_id) end defp round_coord(nil), do: nil diff --git a/lib/microwaveprop/propagation/grid_task_enqueuer.ex b/lib/microwaveprop/propagation/grid_task_enqueuer.ex index a64a4137..958a3cfe 100644 --- a/lib/microwaveprop/propagation/grid_task_enqueuer.ex +++ b/lib/microwaveprop/propagation/grid_task_enqueuer.ex @@ -169,25 +169,35 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuer do cutoff = DateTime.utc_now() |> DateTime.add(-cutoff_seconds, :second) |> DateTime.truncate(:second) now = DateTime.truncate(DateTime.utc_now(), :second) - {failed_n, _} = - "grid_tasks" - |> where( - [t], - t.status == "running" and t.claimed_at < ^cutoff and t.attempt >= ^@max_reclaim_attempts - ) - |> Repo.update_all( - set: [ - status: "failed", - error: "reclaim-orphan: exceeded max_reclaim_attempts", - claimed_at: nil, - updated_at: now - ] - ) + {failed_n, requeued_n} = + fn -> + {failed_count, _} = + "grid_tasks" + |> where( + [t], + t.status == "running" and t.claimed_at < ^cutoff and t.attempt >= ^@max_reclaim_attempts + ) + |> Repo.update_all( + set: [ + status: "failed", + error: "reclaim-orphan: exceeded max_reclaim_attempts", + claimed_at: nil, + updated_at: now + ] + ) - {requeued_n, _} = - "grid_tasks" - |> where([t], t.status == "running" and t.claimed_at < ^cutoff) - |> Repo.update_all(set: [status: "queued", claimed_at: nil, updated_at: now]) + {rq_count, _} = + "grid_tasks" + |> where([t], t.status == "running" and t.claimed_at < ^cutoff) + |> Repo.update_all(set: [status: "queued", claimed_at: nil, updated_at: now]) + + {failed_count, rq_count} + end + |> Repo.transaction() + |> case do + {:ok, {fc, rqc}} -> {fc, rqc} + {:error, reason} -> raise "reclaim_stale_running transaction failed: #{inspect(reason)}" + end if failed_n > 0 or requeued_n > 0 do Logger.info("GridTaskEnqueuer: reclaimed stale-running grid_tasks (requeued=#{requeued_n}, failed=#{failed_n})") diff --git a/lib/microwaveprop/propagation/path_analysis.ex b/lib/microwaveprop/propagation/path_analysis.ex index d75eb6aa..d7fa4670 100644 --- a/lib/microwaveprop/propagation/path_analysis.ex +++ b/lib/microwaveprop/propagation/path_analysis.ex @@ -11,6 +11,7 @@ defmodule Microwaveprop.Propagation.PathAnalysis do duplicated duct-detection and mechanism-classification logic. """ + alias Microwaveprop.Repo alias Microwaveprop.Terrain.ElevationClient alias Microwaveprop.Terrain.TerrainAnalysis @@ -564,6 +565,7 @@ defmodule Microwaveprop.Propagation.PathAnalysis do defp boundary_layer_detail(_), do: nil defp sounding_ducting_detail(soundings) when is_list(soundings) do + soundings = Repo.preload(soundings, :station) ducting = Enum.filter(soundings, & &1.ducting_detected) if ducting == [] do diff --git a/lib/microwaveprop/pskr/feature_bin.ex b/lib/microwaveprop/pskr/feature_bin.ex index b1ebc2d5..c4a9e470 100644 --- a/lib/microwaveprop/pskr/feature_bin.ex +++ b/lib/microwaveprop/pskr/feature_bin.ex @@ -53,6 +53,7 @@ defmodule Microwaveprop.Pskr.FeatureBin do record |> cast(attrs, @cast_fields) |> validate_required(@required_fields) + |> foreign_key_constraint(:run_id) |> unique_constraint([:run_id, :band, :feature, :bin_label]) end end diff --git a/lib/microwaveprop/radio/contact.ex b/lib/microwaveprop/radio/contact.ex index 008ade89..ecb1bf7c 100644 --- a/lib/microwaveprop/radio/contact.ex +++ b/lib/microwaveprop/radio/contact.ex @@ -90,6 +90,8 @@ defmodule Microwaveprop.Radio.Contact do contact |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) + |> foreign_key_constraint(:user_id) + |> foreign_key_constraint(:flagged_by_user_id) end @submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email user_declared_prop_mode height1_ft height2_ft private notes)a @@ -127,6 +129,8 @@ defmodule Microwaveprop.Radio.Contact do |> validate_height(:height2_ft) |> normalize_blank_notes() |> validate_length(:notes, max: 2000) + |> foreign_key_constraint(:user_id) + |> foreign_key_constraint(:flagged_by_user_id) end # Collapse whitespace-only notes to `nil` so the DB column reflects diff --git a/lib/microwaveprop/radio/contact_common_volume_radar.ex b/lib/microwaveprop/radio/contact_common_volume_radar.ex index bbd75bbd..e7db5b83 100644 --- a/lib/microwaveprop/radio/contact_common_volume_radar.ex +++ b/lib/microwaveprop/radio/contact_common_volume_radar.ex @@ -42,6 +42,7 @@ defmodule Microwaveprop.Radio.ContactCommonVolumeRadar do row |> cast(attrs, @required ++ @optional) |> validate_required(@required) + |> foreign_key_constraint(:contact_id) |> unique_constraint(:contact_id) end end diff --git a/lib/microwaveprop/radio/contacts.ex b/lib/microwaveprop/radio/contacts.ex index fc1014f7..80222cc4 100644 --- a/lib/microwaveprop/radio/contacts.ex +++ b/lib/microwaveprop/radio/contacts.ex @@ -308,7 +308,7 @@ defmodule Microwaveprop.Radio.Contacts do def contact_path_points(%{pos1: pos1, pos2: pos2}), do: build_path_points(extract_latlon(pos1), extract_latlon(pos2)) - defp extract_latlon(%{"lat" => lat} = pos) when is_number(lat), do: if(pos["lon"], do: {lat, pos["lon"]}) + defp extract_latlon(%{"lat" => lat} = pos) when is_number(lat), do: if(is_number(pos["lon"]), do: {lat, pos["lon"]}) defp extract_latlon(_), do: nil defp build_path_points({lat1, lon1}, {lat2, lon2}) do diff --git a/lib/microwaveprop/rover/fixed_station.ex b/lib/microwaveprop/rover/fixed_station.ex index f01beffd..158a0aec 100644 --- a/lib/microwaveprop/rover/fixed_station.ex +++ b/lib/microwaveprop/rover/fixed_station.ex @@ -49,6 +49,7 @@ defmodule Microwaveprop.Rover.FixedStation do |> validate_required([:lat, :lon]) |> validate_number(:lat, greater_than_or_equal_to: -90.0, less_than_or_equal_to: 90.0) |> validate_number(:lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0) + |> foreign_key_constraint(:user_id) |> unique_constraint(:callsign, name: :fixed_stations_user_id_callsign_index) end diff --git a/lib/microwaveprop/rover/location.ex b/lib/microwaveprop/rover/location.ex index c14bf1db..3859f70e 100644 --- a/lib/microwaveprop/rover/location.ex +++ b/lib/microwaveprop/rover/location.ex @@ -40,5 +40,6 @@ defmodule Microwaveprop.Rover.Location do |> validate_number(:lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0) |> validate_inclusion(:status, @statuses) |> validate_length(:notes, max: 4000) + |> foreign_key_constraint(:user_id) end end diff --git a/lib/microwaveprop/rover_planning/mission.ex b/lib/microwaveprop/rover_planning/mission.ex index 6d09c789..e0265a0b 100644 --- a/lib/microwaveprop/rover_planning/mission.ex +++ b/lib/microwaveprop/rover_planning/mission.ex @@ -72,6 +72,7 @@ defmodule Microwaveprop.RoverPlanning.Mission do |> validate_number(:station_height_ft, greater_than_or_equal_to: 0.0) |> validate_length(:notes, max: 4000) |> validate_at_least_one_station() + |> foreign_key_constraint(:user_id) end # The multi-checkbox form sends a hidden empty-string sentinel so diff --git a/lib/microwaveprop/rover_planning/path.ex b/lib/microwaveprop/rover_planning/path.ex index 46e8e054..1a524f6d 100644 --- a/lib/microwaveprop/rover_planning/path.ex +++ b/lib/microwaveprop/rover_planning/path.ex @@ -52,6 +52,9 @@ defmodule Microwaveprop.RoverPlanning.Path do |> validate_required([:mission_id, :rover_location_id, :station_id, :band_mhz, :status]) |> validate_inclusion(:status, @statuses) |> validate_number(:band_mhz, greater_than: 0) + |> foreign_key_constraint(:mission_id) + |> foreign_key_constraint(:rover_location_id) + |> foreign_key_constraint(:station_id) |> unique_constraint([:mission_id, :rover_location_id, :station_id, :band_mhz], name: :rover_mission_paths_unique ) diff --git a/lib/microwaveprop/rover_planning/station.ex b/lib/microwaveprop/rover_planning/station.ex index e49ef250..7ac70b6e 100644 --- a/lib/microwaveprop/rover_planning/station.ex +++ b/lib/microwaveprop/rover_planning/station.ex @@ -38,6 +38,7 @@ defmodule Microwaveprop.RoverPlanning.Station do |> validate_required([:lat, :lon]) |> validate_number(:lat, greater_than_or_equal_to: -90.0, less_than_or_equal_to: 90.0) |> validate_number(:lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0) + |> foreign_key_constraint(:mission_id) end # When `input` looks like a callsign or grid, geocode it and populate diff --git a/lib/microwaveprop/weather/hrdps_client.ex b/lib/microwaveprop/weather/hrdps_client.ex index d0137aad..bf0cdea3 100644 --- a/lib/microwaveprop/weather/hrdps_client.ex +++ b/lib/microwaveprop/weather/hrdps_client.ex @@ -53,12 +53,12 @@ defmodule Microwaveprop.Weather.HrdpsClient do @pressure_var_kinds [{:tmp, "TMP"}, {:depr, "DEPR"}, {:hgt, "HGT"}] - # All variable atoms are generated from compile-time constants - # (@grid_pressure_levels + @pressure_var_kinds = 21 atoms max) — safe - # to use String.to_atom/1 here; not reachable from user input. + # All pressure-level variable atoms are created as atom literals at compile + # time. The bounded cross-product of @grid_pressure_levels × @pressure_var_kinds + # (7 levels × 3 vars = 21 atoms) ensures no runtime atom creation. @pressure_vars (for level <- @grid_pressure_levels, {kind, msc} <- @pressure_var_kinds do - atom = String.to_atom("#{kind}_#{level}mb") + atom = :"#{kind}_#{level}mb" msc_level = "ISBL_#{level |> Integer.to_string() |> String.pad_leading(4, "0")}" {atom, msc, msc_level} end) diff --git a/lib/microwaveprop/weather/soundings.ex b/lib/microwaveprop/weather/soundings.ex index 8a9543d8..ac8975ed 100644 --- a/lib/microwaveprop/weather/soundings.ex +++ b/lib/microwaveprop/weather/soundings.ex @@ -195,7 +195,8 @@ defmodule Microwaveprop.Weather.Soundings do s.observed_at, ^timestamp ), - limit: 1 + limit: 1, + preload: [:station] ) |> Repo.one() |> case do diff --git a/lib/microwaveprop_web/live/admin/monitor_live/index.ex b/lib/microwaveprop_web/live/admin/monitor_live/index.ex index ca3384ba..f0dbdfcb 100644 --- a/lib/microwaveprop_web/live/admin/monitor_live/index.ex +++ b/lib/microwaveprop_web/live/admin/monitor_live/index.ex @@ -108,7 +108,7 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Index do - + {monitor.name} diff --git a/lib/microwaveprop_web/live/import_live.ex b/lib/microwaveprop_web/live/import_live.ex index 466c4046..bd6dcbe1 100644 --- a/lib/microwaveprop_web/live/import_live.ex +++ b/lib/microwaveprop_web/live/import_live.ex @@ -16,22 +16,42 @@ defmodule MicrowavepropWeb.ImportLive do @impl true def mount(%{"id" => id}, _session, socket) do case load_run(id) do - nil -> - {:ok, - socket - |> put_flash(:error, "Import not found.") - |> push_navigate(to: ~p"/submit")} + nil -> not_found_redirect(socket) + %ImportRun{} = run -> mount_authorized(socket, run) + end + end - %ImportRun{} = run -> - _ = - if connected?(socket) do - Phoenix.PubSub.subscribe(@pubsub, "csv_import:#{run.id}") - end + defp mount_authorized(socket, run) do + case authorized_to_view?(socket, run) do + :ok -> + if connected?(socket) do + Phoenix.PubSub.subscribe(@pubsub, "csv_import:#{run.id}") + end {:ok, socket |> assign(:page_title, "Import ##{String.slice(run.id, 0, 8)}") |> assign(:run, run)} + + :not_found -> + not_found_redirect(socket) + end + end + + defp not_found_redirect(socket) do + {:ok, + socket + |> put_flash(:error, "Import not found.") + |> push_navigate(to: ~p"/")} + end + + defp authorized_to_view?(socket, run) do + viewer = socket.assigns[:current_scope] && socket.assigns.current_scope.user + + cond do + viewer && viewer.is_admin -> :ok + viewer && viewer.email == run.submitter_email -> :ok + true -> :not_found end end diff --git a/lib/microwaveprop_web/live/map_live.ex b/lib/microwaveprop_web/live/map_live.ex index 5939e3a0..b1d18b80 100644 --- a/lib/microwaveprop_web/live/map_live.ex +++ b/lib/microwaveprop_web/live/map_live.ex @@ -87,7 +87,9 @@ defmodule MicrowavepropWeb.MapLive do grid_visible: false, radar_visible: false, deploy_iso: Calendar.strftime(build_ts, "%Y-%m-%d %H:%M UTC"), - deploy_ago: format_deploy_ago(build_ts) + deploy_ago: format_deploy_ago(build_ts), + pipeline_timer: nil, + advance_timer: nil )} end @@ -95,13 +97,29 @@ defmodule MicrowavepropWeb.MapLive do if connected?(socket) do :ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:updated") :ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:pipeline") - _ = Process.send_after(self(), :refresh_pipeline_status, @pipeline_status_refresh_ms) - _ = Process.send_after(self(), :advance_now_cursor, @advance_now_cursor_ms) + socket = schedule_pipeline_status_timer(socket) + schedule_advance_timer(socket) + else + socket end - - socket end + defp schedule_pipeline_status_timer(socket) do + cancel_timer_if_exists(socket.assigns[:pipeline_timer]) + ref = Process.send_after(self(), :refresh_pipeline_status, @pipeline_status_refresh_ms) + assign(socket, :pipeline_timer, ref) + end + + defp schedule_advance_timer(socket) do + cancel_timer_if_exists(socket.assigns[:advance_timer]) + ref = Process.send_after(self(), :advance_now_cursor, @advance_now_cursor_ms) + assign(socket, :advance_timer, ref) + end + + defp cancel_timer_if_exists(nil), do: :ok + defp cancel_timer_if_exists(ref) when is_reference(ref), do: Process.cancel_timer(ref) + defp cancel_timer_if_exists(_), do: :ok + # Compact relative-time rendering of the build timestamp. Matches the # format used by Layouts.deploy_stamp/1 so the nav and the map # sidebar stay consistent. @@ -325,7 +343,7 @@ defmodule MicrowavepropWeb.MapLive do end def handle_info(:advance_now_cursor, socket) do - Process.send_after(self(), :advance_now_cursor, @advance_now_cursor_ms) + socket = schedule_advance_timer(socket) if socket.assigns.tracking_now? do new_time = closest_to_now(socket.assigns.valid_times) @@ -350,7 +368,7 @@ defmodule MicrowavepropWeb.MapLive do end def handle_info(:refresh_pipeline_status, socket) do - Process.send_after(self(), :refresh_pipeline_status, @pipeline_status_refresh_ms) + socket = schedule_pipeline_status_timer(socket) new_status = PipelineStatus.current() diff --git a/lib/microwaveprop_web/live/monitor_live/show.ex b/lib/microwaveprop_web/live/monitor_live/show.ex index 79b4634a..7f372124 100644 --- a/lib/microwaveprop_web/live/monitor_live/show.ex +++ b/lib/microwaveprop_web/live/monitor_live/show.ex @@ -12,25 +12,33 @@ defmodule MicrowavepropWeb.MonitorLive.Show do @impl true def mount(%{"id" => id}, _session, socket) do - user = socket.assigns.current_scope.user + viewer = socket.assigns[:current_scope] && socket.assigns.current_scope.user monitor = BeaconMonitors.get_monitor!(id) - if monitor.user_id == user.id do - beacons = Repo.all(from b in Beacon, where: b.approved == true, order_by: b.callsign) - measurements = BeaconMeasurements.list_recent_for_monitor(monitor.id, 20) + cond do + viewer && monitor.user_id == viewer.id -> + beacons = Repo.all(from b in Beacon, where: b.approved == true, order_by: b.callsign) + measurements = BeaconMeasurements.list_recent_for_monitor(monitor.id, 20) - {:ok, - socket - |> assign(:page_title, "My Monitor: #{monitor.name}") - |> assign(:monitor, monitor) - |> assign(:beacons, beacons) - |> assign(:measurements, measurements) - |> assign_form(monitor)} - else - {:ok, - socket - |> put_flash(:error, "You don't have access to that monitor.") - |> push_navigate(to: ~p"/users/settings")} + {:ok, + socket + |> assign(:page_title, "My Monitor: #{monitor.name}") + |> assign(:monitor, monitor) + |> assign(:beacons, beacons) + |> assign(:measurements, measurements) + |> assign_form(monitor)} + + viewer -> + {:ok, + socket + |> put_flash(:error, "You don't have access to that monitor.") + |> push_navigate(to: ~p"/users/settings")} + + true -> + {:ok, + socket + |> put_flash(:info, "Please log in to view this monitor.") + |> push_navigate(to: ~p"/users/log-in")} end end diff --git a/test/microwaveprop/oban_error_reporter_test.exs b/test/microwaveprop/oban_error_reporter_test.exs index d0426c56..eed1ed06 100644 --- a/test/microwaveprop/oban_error_reporter_test.exs +++ b/test/microwaveprop/oban_error_reporter_test.exs @@ -198,7 +198,7 @@ defmodule Microwaveprop.ObanErrorReporterTest do capture_log(fn -> execute_exception(meta_transient) end) assert_receive {:transient, 303} - refute_receive {:permanent, 303}, 50 + refute_receive {:permanent, 303}, 200 meta_permanent = build_metadata(%{ @@ -213,7 +213,7 @@ defmodule Microwaveprop.ObanErrorReporterTest do capture_log(fn -> execute_exception(meta_permanent) end) assert_receive {:permanent, 404} - refute_receive {:transient, 404}, 50 + refute_receive {:transient, 404}, 200 end test "a raising callback does not propagate, and the log still lands" do diff --git a/test/microwaveprop/propagation_test.exs b/test/microwaveprop/propagation_test.exs index 89695771..79d9aed3 100644 --- a/test/microwaveprop/propagation_test.exs +++ b/test/microwaveprop/propagation_test.exs @@ -657,7 +657,7 @@ defmodule Microwaveprop.PropagationTest do _ = Propagation.scores_at(10_000, valid_time) assert_receive {:telemetry, [:microwaveprop, :propagation, :scores_at, :cache], %{hit: true}} - refute_receive {:telemetry, [:microwaveprop, :propagation, :scores_at, :stop], _}, 50 + refute_receive {:telemetry, [:microwaveprop, :propagation, :scores_at, :stop], _}, 200 after :telemetry.detach(handler_id) end diff --git a/test/microwaveprop/repo_listener_test.exs b/test/microwaveprop/repo_listener_test.exs index fa6dc9a1..eb16d401 100644 --- a/test/microwaveprop/repo_listener_test.exs +++ b/test/microwaveprop/repo_listener_test.exs @@ -52,7 +52,7 @@ defmodule Microwaveprop.RepoListenerTest do msg = notification("contact_status_changed", "not-json-at-all{") assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil}) - refute_receive {:contact_status_changed, _}, 50 + refute_receive {:contact_status_changed, _}, 200 end end @@ -76,7 +76,7 @@ defmodule Microwaveprop.RepoListenerTest do msg = notification("oban_job_changed", "{\"broken\":") assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil}) - refute_receive {:oban_job_changed, _}, 50 + refute_receive {:oban_job_changed, _}, 200 end end @@ -88,8 +88,8 @@ defmodule Microwaveprop.RepoListenerTest do msg = notification("contact_status_changed", Jason.encode!(%{"contact_id" => "abc"})) assert {:noreply, _} = RepoListener.handle_info(msg, %{pid: nil}) - refute_receive {:oban_job_changed, _}, 50 - refute_receive {:contact_status_changed, _}, 50 + refute_receive {:oban_job_changed, _}, 200 + refute_receive {:contact_status_changed, _}, 200 end test "oban_job_changed does not broadcast on db:contact_status" do @@ -99,8 +99,8 @@ defmodule Microwaveprop.RepoListenerTest do msg = notification("oban_job_changed", Jason.encode!(%{"id" => 1})) assert {:noreply, _} = RepoListener.handle_info(msg, %{pid: nil}) - refute_receive {:contact_status_changed, _}, 50 - refute_receive {:oban_job_changed, _}, 50 + refute_receive {:contact_status_changed, _}, 200 + refute_receive {:oban_job_changed, _}, 200 end end @@ -117,8 +117,8 @@ defmodule Microwaveprop.RepoListenerTest do msg = notification("some_other_channel", Jason.encode!(%{"x" => 1})) assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil}) - refute_receive {:contact_status_changed, _}, 50 - refute_receive {:oban_job_changed, _}, 50 + refute_receive {:contact_status_changed, _}, 200 + refute_receive {:oban_job_changed, _}, 200 end test "ignores arbitrary info messages without crashing" do diff --git a/test/microwaveprop/valkey_test.exs b/test/microwaveprop/valkey_test.exs index 2be840c4..d6aee265 100644 --- a/test/microwaveprop/valkey_test.exs +++ b/test/microwaveprop/valkey_test.exs @@ -8,14 +8,10 @@ defmodule Microwaveprop.ValkeyTest do import Mox - # Defined here (not in test_helper.exs) so test_helper can load even - # when the precommit chain has purged Mox from the code path. alias Microwaveprop.Valkey alias Microwaveprop.Valkey.Conn alias Microwaveprop.Valkey.MockAdapter - Mox.defmock(MockAdapter, for: Microwaveprop.Valkey.Adapter) - # Register Mox mock as the adapter and fake a Redix connection so # configured?() returns true, letting command/pipeline run through the mock. setup do diff --git a/test/mix/tasks/prop_compare_test.exs b/test/mix/tasks/prop_compare_test.exs index 8ec43db6..74a72200 100644 --- a/test/mix/tasks/prop_compare_test.exs +++ b/test/mix/tasks/prop_compare_test.exs @@ -99,13 +99,9 @@ defmodule Mix.Tasks.Prop.CompareTest do # read_history is exercised. File.write!(history_path, "not-valid-json\n", [:append]) - try do - ExUnit.CaptureIO.capture_io(fn -> - Compare.run(["--days", "1", "--samples", "10", "--output", output_dir]) - end) - catch - :exit, _ -> :ok - end + ExUnit.CaptureIO.capture_io(fn -> + Compare.run(["--days", "1", "--samples", "10", "--output", output_dir]) + end) # The history file remains; new entries appended. assert File.exists?(history_path) @@ -119,13 +115,9 @@ defmodule Mix.Tasks.Prop.CompareTest do end end - try do - ExUnit.CaptureIO.capture_io(fn -> - Compare.run(["--days", "1", "--samples", "100", "--output", output_dir]) - end) - catch - :exit, _ -> :ok - end + ExUnit.CaptureIO.capture_io(fn -> + Compare.run(["--days", "1", "--samples", "100", "--output", output_dir]) + end) latest_path = Path.join(output_dir, "latest.json") @@ -140,13 +132,9 @@ defmodule Mix.Tasks.Prop.CompareTest do test "seeded contact + matching HRRR walks the scoring + write path", %{output_dir: output_dir} do _ = seed_contact_and_hrrr() - try do - ExUnit.CaptureIO.capture_io(fn -> - Compare.run(["--days", "1", "--samples", "10", "--output", output_dir]) - end) - catch - :exit, _ -> :ok - end + ExUnit.CaptureIO.capture_io(fn -> + Compare.run(["--days", "1", "--samples", "10", "--output", output_dir]) + end) # Each successful run writes latest.json + history.jsonl. If the # task short-circuited (no rows / no model), at least one of these