towerops/test/towerops_web/live/insights_live_events_test.exs
2026-06-15 11:15:46 -05:00

311 lines
10 KiB
Elixir

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
alias Towerops.Workers.AiNetworkInsightWorker
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
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 Process.alive?(view.pid)
end
test "drops empty filter params", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/insights")
_ = render_hook(view, "filter", %{"source" => "", "urgency" => "", "status" => ""})
assert Process.alive?(view.pid)
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 Process.alive?(view.pid)
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 Process.alive?(view.pid)
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 Process.alive?(view.pid)
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
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)
assert_enqueued(worker: AiNetworkInsightWorker)
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