defmodule ToweropsWeb.InsightsLive.IndexEventsTest do @moduledoc """ Drives event handlers in the Insights LiveView (filter, dismiss, toggle_select, select_all, deselect_all, bulk_dismiss). """ use ToweropsWeb.ConnCase, async: true use Oban.Testing, repo: Towerops.Repo import Phoenix.LiveViewTest alias Towerops.Preseem.Insight alias Towerops.Repo setup :register_and_log_in_user setup %{user: user} do {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Insights Org"}, user.id) insights = for i <- 1..3 do {:ok, insight} = Repo.insert( Insight.changeset(%Insight{}, %{ organization_id: organization.id, type: "snmp_cpu_high", urgency: "warning", status: "active", channel: "proactive", title: "Insight #{i}", source: "preseem" }) ) insight end %{organization: organization, insights: insights} end describe "filter event" do test "patches the URL with chosen filters", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/insights") _ = render_hook(view, "filter", %{ "source" => "preseem", "urgency" => "warning", "status" => "active" }) assert true end test "drops empty filter params", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/insights") _ = render_hook(view, "filter", %{"source" => "", "urgency" => "", "status" => ""}) assert true end end describe "select / toggle / deselect events" do test "select_all + deselect_all flow", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/insights") _ = render_hook(view, "select_all", %{}) _ = render_hook(view, "deselect_all", %{}) assert true end test "toggle_select flips membership of an id", %{conn: conn, insights: [i1 | _]} do {:ok, view, _html} = live(conn, ~p"/insights") _ = render_hook(view, "toggle_select", %{"id" => i1.id}) _ = render_hook(view, "toggle_select", %{"id" => i1.id}) assert true end end describe "dismiss + bulk_dismiss" do test "dismiss marks a single insight as dismissed", %{conn: conn, insights: [i1 | _]} do {:ok, view, _html} = live(conn, ~p"/insights") _ = render_hook(view, "dismiss", %{"id" => i1.id}) reloaded = Repo.get!(Insight, i1.id) assert reloaded.status == "dismissed" end test "bulk_dismiss is a no-op when nothing selected", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/insights") _ = render_hook(view, "bulk_dismiss", %{}) assert true end test "bulk_dismiss clears selection and dismisses all selected", %{conn: conn, insights: [i1, i2 | _]} do {:ok, view, _html} = live(conn, ~p"/insights") _ = render_hook(view, "toggle_select", %{"id" => i1.id}) _ = render_hook(view, "toggle_select", %{"id" => i2.id}) _ = render_hook(view, "bulk_dismiss", %{}) assert Repo.get!(Insight, i1.id).status == "dismissed" assert Repo.get!(Insight, i2.id).status == "dismissed" end end describe "LLM enrichment rendering" do test "shows llm_summary, recommended_action, and AI-generated label when present", %{ conn: conn, organization: organization } do {:ok, _enriched} = Repo.insert( Insight.changeset(%Insight{}, %{ organization_id: organization.id, type: "wireless_signal_weak", urgency: "warning", status: "active", channel: "proactive", title: "Enriched insight", source: "snmp", llm_summary: "AP has 3 clients pinned to MCS0; downstream is congested.", recommended_action: "Re-aim CPE at -82 dBm or move it to AP-7.", llm_model: "deepseek-v4-pro", llm_enriched_at: ~U[2026-05-09 12:00:00Z] }) ) {:ok, _view, html} = live(conn, ~p"/insights") assert html =~ "AP has 3 clients pinned to MCS0; downstream is congested." assert html =~ "Re-aim CPE at -82 dBm or move it to AP-7." assert html =~ "AI-generated" # Model identity is intentionally not exposed in the UI. refute html =~ "deepseek-v4-pro" end test "does not render the summary block when llm_summary is nil", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/insights") refute html =~ "Recommended action:" refute html =~ "AI-generated" end end describe "sector_overload rendering" do test "shows free airtime, subscriber count, QoE score", %{conn: conn, organization: organization} do {:ok, _} = Repo.insert( Insight.changeset(%Insight{}, %{ organization_id: organization.id, type: "sector_overload", urgency: "warning", status: "active", channel: "proactive", source: "preseem", title: "Sector overload on AP-North: 22.0% airtime free", metadata: %{ "airtime_remaining_pct" => 22.0, "subscriber_count" => 45, "qoe_score" => 75.0, "ap_model" => "ePMP-3000", "ap_firmware" => "5.2" } }) ) {:ok, _view, html} = live(conn, ~p"/insights") assert html =~ "Free airtime" assert html =~ "22.0%" assert html =~ "45" assert html =~ "ePMP-3000" end end describe "cpe_realign rendering" do test "shows signal, SNR, TX/RX rate, distance", %{conn: conn, organization: organization} do {:ok, _} = Repo.insert( Insight.changeset(%Insight{}, %{ organization_id: organization.id, type: "cpe_realign", urgency: "warning", status: "active", channel: "proactive", source: "snmp", title: "Re-aim CPE AA:BB:CC:DD:EE:FF: -82 dBm signal, 14 dB SNR", metadata: %{ "dedup_key" => "cpe_realign:device-1:AA:BB:CC:DD:EE:FF", "cpe_mac" => "AA:BB:CC:DD:EE:FF", "cpe_hostname" => "house-42", "signal_strength_dbm" => -82, "snr_db" => 14, "tx_rate" => 18, "rx_rate" => 24, "distance_m" => 1750 } }) ) {:ok, _view, html} = live(conn, ~p"/insights") assert html =~ "-82 dBm" assert html =~ "14 dB" assert html =~ "18/24 Mbps" assert html =~ "1750 m" assert html =~ "house-42" end end describe "regenerate_insights event" do alias Towerops.Workers.CapacityInsightWorker alias Towerops.Workers.DeviceHealthInsightWorker alias Towerops.Workers.GaiiaInsightWorker alias Towerops.Workers.InsightLlmEnrichmentWorker alias Towerops.Workers.PreseemBaselineWorker alias Towerops.Workers.SystemInsightWorker alias Towerops.Workers.WirelessInsightWorker test "non-superusers do not see the regenerate button", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/insights") refute html =~ "Regenerate insights" end test "superusers see the regenerate button", %{conn: conn, user: user} do promote_to_superuser(user) {:ok, _view, html} = live(conn, ~p"/insights") assert html =~ "Regenerate insights" end test "regenerate_insights enqueues all insight workers for superusers", %{conn: conn, user: user} do promote_to_superuser(user) {:ok, view, _html} = live(conn, ~p"/insights") _ = render_hook(view, "regenerate_insights", %{}) assert_enqueued(worker: PreseemBaselineWorker) assert_enqueued(worker: CapacityInsightWorker) assert_enqueued(worker: DeviceHealthInsightWorker) assert_enqueued(worker: GaiiaInsightWorker) assert_enqueued(worker: SystemInsightWorker) assert_enqueued(worker: WirelessInsightWorker) assert_enqueued(worker: InsightLlmEnrichmentWorker) end test "regenerate_insights is rejected for non-superusers", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/insights") _ = render_hook(view, "regenerate_insights", %{}) refute_enqueued(worker: PreseemBaselineWorker) refute_enqueued(worker: CapacityInsightWorker) end defp promote_to_superuser(user) do user |> Ecto.Changeset.change(is_superuser: true) |> Repo.update!() end end describe "ap_frequency_change rendering" do test "shows current vs recommended channel and the top offender", %{ conn: conn, organization: organization } do {:ok, _} = Repo.insert( Insight.changeset(%Insight{}, %{ organization_id: organization.id, type: "ap_frequency_change", urgency: "warning", status: "active", channel: "proactive", source: "system", title: "AP-North should change channel: 149 → 161", metadata: %{ "current_channel" => 149, "recommended_channel" => 161, "current_score" => 25.4, "recommended_score" => 1.0, "improvement_db" => 14.0, "channel_width_mhz" => 80, "current_frequency_mhz" => 5745, "top_offenders" => [ %{ "bssid" => "AA:BB:CC:DD:EE:FF", "ssid" => "Neighbor-A", "rssi_dbm" => -55, "channel" => 149, "frequency_mhz" => 5745, "is_own_fleet" => true, "seen_at" => "2026-05-09T12:00:00Z" } ] } }) ) {:ok, _view, html} = live(conn, ~p"/insights") assert html =~ "Current channel" assert html =~ "149" assert html =~ "161" assert html =~ "AA:BB:CC:DD:EE:FF" assert html =~ "Neighbor-A" assert html =~ "-55 dBm" assert html =~ "own fleet" assert html =~ "14.0 dB cleaner" end end end