diff --git a/lib/microwaveprop/propagation/profiles_file.ex b/lib/microwaveprop/propagation/profiles_file.ex index c159866d..4909b810 100644 --- a/lib/microwaveprop/propagation/profiles_file.ex +++ b/lib/microwaveprop/propagation/profiles_file.ex @@ -67,11 +67,21 @@ defmodule Microwaveprop.Propagation.ProfilesFile do `{:ok, %{{lat, lon} => profile}}` or `{:error, :enoent}` if the file doesn't exist. Used by `Microwaveprop.Weather` to warm `GridCache` on pod startup without touching the database. + + Does *not* pass `[:safe]` to `binary_to_term/1`. The file was written + by `write!/2` in the same code base, so we trust it. The previous + `[:safe]` flag was rejecting our own files at app-startup warm time + because some atoms in the persisted profile (`:base_m`, `:top_m`, + `:thickness_m`, `:min_freq_ghz`, `:native_min_gradient`, etc.) live + in modules that aren't eagerly loaded during boot, so they weren't + in the runtime atom table when `warm_grid_cache_from_latest_profile` + ran. The fix is not "load more modules eagerly" — it's "trust files + we wrote ourselves." """ @spec read(DateTime.t()) :: {:ok, %{{float(), float()} => map()}} | {:error, :enoent} def read(%DateTime{} = valid_time) do case File.read(path_for(valid_time)) do - {:ok, binary} -> {:ok, :erlang.binary_to_term(binary, [:safe])} + {:ok, binary} -> {:ok, :erlang.binary_to_term(binary)} {:error, _} -> {:error, :enoent} end end diff --git a/test/microwaveprop/propagation/profiles_file_test.exs b/test/microwaveprop/propagation/profiles_file_test.exs index 825583ba..80bf45e4 100644 --- a/test/microwaveprop/propagation/profiles_file_test.exs +++ b/test/microwaveprop/propagation/profiles_file_test.exs @@ -56,6 +56,47 @@ defmodule Microwaveprop.Propagation.ProfilesFileTest do test "returns :enoent when no file exists" do assert ProfilesFile.read(~U[2026-04-14 18:00:00Z]) == {:error, :enoent} end + + test "round-trips the full production data shape including duct fields" do + valid_time = ~U[2026-04-14 18:00:00Z] + + grid_data = %{ + {32.75, -97.125} => %{ + surface_temp_c: 25.0, + surface_dewpoint_c: 15.0, + surface_pressure_mb: 1013.25, + hpbl_m: 750.0, + pwat_mm: 32.5, + run_time: ~U[2026-04-14 17:00:00Z], + precip_mm: 0.0, + wind_u: 3.2, + wind_v: -1.7, + cloud_cover_pct: 25.0, + native_min_gradient: -78.4, + best_duct_freq_ghz: 24.0, + duct_count: 1, + max_duct_thickness_m: 200.0, + ducts: [ + %{base_m: 200.0, top_m: 400.0, thickness_m: 200.0, min_freq_ghz: 10.0} + ], + profile: [ + %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 15.0, "hght" => 50.0} + ] + } + } + + ProfilesFile.write!(valid_time, grid_data) + + assert {:ok, round_tripped} = ProfilesFile.read(valid_time) + + point = round_tripped[{32.75, -97.125}] + assert point[:surface_temp_c] == 25.0 + assert point[:run_time] == ~U[2026-04-14 17:00:00Z] + assert [duct] = point[:ducts] + assert duct[:base_m] == 200.0 + assert duct[:thickness_m] == 200.0 + assert duct[:min_freq_ghz] == 10.0 + end end describe "list_valid_times/0" do