137 lines
4 KiB
Elixir
137 lines
4 KiB
Elixir
defmodule Towerops.LLM.NetworkSnapshotTest do
|
|
use Towerops.DataCase, async: false
|
|
|
|
import Ecto.Query
|
|
import Towerops.AccountsFixtures
|
|
import Towerops.DevicesFixtures
|
|
import Towerops.OrganizationsFixtures
|
|
|
|
alias Towerops.LLM.NetworkSnapshot
|
|
alias Towerops.Preseem.AccessPoint
|
|
alias Towerops.Preseem.Insight
|
|
alias Towerops.Repo
|
|
|
|
setup do
|
|
user = user_fixture()
|
|
org = organization_fixture(user.id)
|
|
%{org: org}
|
|
end
|
|
|
|
describe "build/1 — empty org" do
|
|
test "returns a struct with zero counts and empty lists", %{org: org} do
|
|
snap = NetworkSnapshot.build(org.id)
|
|
|
|
assert snap.organization_id == org.id
|
|
assert snap.totals.devices >= 0
|
|
assert snap.preseem.lowest_qoe_aps == []
|
|
assert snap.snmp.hottest_devices == []
|
|
assert snap.backhaul.high_utilization == []
|
|
assert snap.existing_insights == []
|
|
end
|
|
end
|
|
|
|
describe "build/1 — devices and counts" do
|
|
test "totals.devices reflects monitored device count", %{org: org} do
|
|
device_fixture(%{organization_id: org.id, monitoring_enabled: true})
|
|
device_fixture(%{organization_id: org.id, monitoring_enabled: true})
|
|
device_fixture(%{organization_id: org.id, monitoring_enabled: false})
|
|
|
|
snap = NetworkSnapshot.build(org.id)
|
|
|
|
assert snap.totals.devices == 3
|
|
assert snap.totals.monitored_devices == 2
|
|
end
|
|
|
|
test "stale_polled_devices includes devices with old last_snmp_poll_at", %{org: org} do
|
|
d1 = device_fixture(%{organization_id: org.id, monitoring_enabled: true})
|
|
cutoff = DateTime.add(DateTime.utc_now(), -48, :hour)
|
|
|
|
Repo.update_all(
|
|
from(d in Towerops.Devices.Device, where: d.id == ^d1.id),
|
|
set: [last_snmp_poll_at: cutoff]
|
|
)
|
|
|
|
snap = NetworkSnapshot.build(org.id)
|
|
|
|
assert Enum.any?(snap.snmp.stale_polled_devices, &(&1.device_id == d1.id))
|
|
end
|
|
end
|
|
|
|
describe "build/1 — Preseem APs" do
|
|
test "lowest_qoe_aps is sorted ascending by qoe_score and capped", %{org: org} do
|
|
for i <- 1..15 do
|
|
%AccessPoint{}
|
|
|> AccessPoint.changeset(%{
|
|
organization_id: org.id,
|
|
preseem_id: "ap-#{i}",
|
|
name: "AP #{i}",
|
|
qoe_score: i * 5.0,
|
|
subscriber_count: i,
|
|
model: "ePMP 3000"
|
|
})
|
|
|> Repo.insert!()
|
|
end
|
|
|
|
snap = NetworkSnapshot.build(org.id)
|
|
|
|
qoe_scores = Enum.map(snap.preseem.lowest_qoe_aps, & &1.qoe_score)
|
|
assert qoe_scores == Enum.sort(qoe_scores)
|
|
assert length(snap.preseem.lowest_qoe_aps) <= 10
|
|
assert hd(qoe_scores) == 5.0
|
|
end
|
|
|
|
test "skips APs with nil qoe_score", %{org: org} do
|
|
%AccessPoint{}
|
|
|> AccessPoint.changeset(%{
|
|
organization_id: org.id,
|
|
preseem_id: "no-qoe",
|
|
name: "Quiet AP",
|
|
qoe_score: nil
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
snap = NetworkSnapshot.build(org.id)
|
|
|
|
refute Enum.any?(snap.preseem.lowest_qoe_aps, &(&1.preseem_id == "no-qoe"))
|
|
end
|
|
end
|
|
|
|
describe "build/1 — existing insights" do
|
|
test "groups active insights by type with counts", %{org: org} do
|
|
for type <- ["qoe_degradation", "qoe_degradation", "device_overheating"] do
|
|
%Insight{}
|
|
|> Insight.changeset(%{
|
|
organization_id: org.id,
|
|
type: type,
|
|
urgency: "warning",
|
|
channel: "proactive",
|
|
title: "x"
|
|
})
|
|
|> Repo.insert!()
|
|
end
|
|
|
|
snap = NetworkSnapshot.build(org.id)
|
|
|
|
counts = Map.new(snap.existing_insights, &{&1.type, &1.count})
|
|
assert counts["qoe_degradation"] == 2
|
|
assert counts["device_overheating"] == 1
|
|
end
|
|
|
|
test "ignores dismissed and resolved insights", %{org: org} do
|
|
%Insight{}
|
|
|> Insight.changeset(%{
|
|
organization_id: org.id,
|
|
type: "agent_offline",
|
|
urgency: "critical",
|
|
channel: "proactive",
|
|
title: "x",
|
|
status: "resolved"
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
snap = NetworkSnapshot.build(org.id)
|
|
|
|
refute Enum.any?(snap.existing_insights, &(&1.type == "agent_offline"))
|
|
end
|
|
end
|
|
end
|