148 lines
5 KiB
Elixir
148 lines
5 KiB
Elixir
defmodule Towerops.LLM.NetworkInsightPromptTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Towerops.LLM.NetworkInsightPrompt
|
|
|
|
@snapshot %{
|
|
organization_id: "org-1",
|
|
organization_name: "Acme WISP",
|
|
totals: %{devices: 50, monitored_devices: 45, sites: 5, agents: 2, preseem_aps: 12},
|
|
preseem: %{
|
|
lowest_qoe_aps: [
|
|
%{preseem_id: "ap-1", name: "AP North", qoe_score: 42.0, subscriber_count: 30, model: "ePMP 3000"}
|
|
],
|
|
highest_subscriber_aps: [],
|
|
model_breakdown: [%{model: "ePMP 3000", count: 5, avg_qoe: 78.0}]
|
|
},
|
|
snmp: %{
|
|
stale_polled_devices: [],
|
|
hottest_devices: [%{device_id: "dev-1", name: "Tower-A", max_temp_c: 78.0}],
|
|
weak_signal_clients: [],
|
|
low_optical_rx: []
|
|
},
|
|
backhaul: %{high_utilization: []},
|
|
gaiia: %{untracked: 0, ghosts: 0, mismatches: 0},
|
|
agents: %{online: 2, offline: 0},
|
|
existing_insights: [%{type: "qoe_degradation", count: 3, criticals: 1, warnings: 2}]
|
|
}
|
|
|
|
describe "build/1" do
|
|
test "returns a system + user message pair" do
|
|
[%{role: "system", content: sys}, %{role: "user", content: user}] =
|
|
NetworkInsightPrompt.build(@snapshot)
|
|
|
|
assert sys =~ ~r/WISP|network/
|
|
assert sys =~ "JSON"
|
|
assert user =~ ~r/Acme WISP|org-1/
|
|
end
|
|
|
|
test "user message includes the snapshot as JSON" do
|
|
[_sys, %{content: user}] = NetworkInsightPrompt.build(@snapshot)
|
|
|
|
assert user =~ "lowest_qoe_aps"
|
|
assert user =~ "Tower-A"
|
|
assert user =~ "qoe_degradation"
|
|
end
|
|
|
|
test "system prompt instructs the model to skip findings already covered by existing_insights" do
|
|
[%{content: sys}, _user] = NetworkInsightPrompt.build(@snapshot)
|
|
assert sys =~ ~r/existing_insights|do not/
|
|
end
|
|
end
|
|
|
|
describe "parse/1" do
|
|
test "extracts observations from JSON embedded in markdown with extra text" do
|
|
content = """
|
|
Let me analyze the network snapshot:
|
|
|
|
```json
|
|
{"observations": [{"title": "Channel Congestion", "description": "Channel 6 has 5 APs on same channel", "urgency": "warning"}]}
|
|
```
|
|
|
|
This is my analysis.
|
|
"""
|
|
|
|
assert {:ok, [obs]} = NetworkInsightPrompt.parse(content)
|
|
assert obs.title == "Channel Congestion"
|
|
assert obs.urgency == "warning"
|
|
end
|
|
|
|
test "extracts JSON from text with leading/trailing content using brace extraction" do
|
|
content = """
|
|
Here's my analysis:
|
|
{"observations": [{"title": "Test", "description": "Some desc", "urgency": "info"}]}
|
|
That's all.
|
|
"""
|
|
|
|
assert {:ok, [obs]} = NetworkInsightPrompt.parse(content)
|
|
assert obs.title == "Test"
|
|
end
|
|
|
|
test "extracts deeply nested brace structure" do
|
|
content =
|
|
~s|Some text {"observations": [{"title": "Nested", "description": "escaped \\"quote\\" here", "urgency": "critical"}]} more text|
|
|
|
|
assert {:ok, [obs]} = NetworkInsightPrompt.parse(content)
|
|
assert obs.title == "Nested"
|
|
assert obs.description =~ "quote"
|
|
end
|
|
|
|
test "returns empty list for text with unmatched braces" do
|
|
content = "text with {unmatched brace"
|
|
assert {:ok, []} = NetworkInsightPrompt.parse(content)
|
|
end
|
|
|
|
test "extracts a list of observations from a JSON array" do
|
|
json =
|
|
~s|{"observations": [{"title": "Sites with thermal stress", "description": "Tower-A is at 78C.", "urgency": "warning"}]}|
|
|
|
|
assert {:ok, [obs]} = NetworkInsightPrompt.parse(json)
|
|
assert obs.title == "Sites with thermal stress"
|
|
assert obs.urgency == "warning"
|
|
assert obs.description =~ "78"
|
|
end
|
|
|
|
test "tolerates code-fenced JSON" do
|
|
content = """
|
|
```json
|
|
{"observations": [{"title": "X", "description": "Y", "urgency": "info"}]}
|
|
```
|
|
"""
|
|
|
|
assert {:ok, [obs]} = NetworkInsightPrompt.parse(content)
|
|
assert obs.title == "X"
|
|
end
|
|
|
|
test "returns empty list when JSON is malformed" do
|
|
assert {:ok, []} = NetworkInsightPrompt.parse("not json at all")
|
|
end
|
|
|
|
test "returns empty list when observations key is missing" do
|
|
assert {:ok, []} = NetworkInsightPrompt.parse(~s|{"foo": "bar"}|)
|
|
end
|
|
|
|
test "drops observations missing required fields" do
|
|
json =
|
|
~s|{"observations": [{"title": "ok", "description": "fine", "urgency": "info"}, {"description": "no title"}]}|
|
|
|
|
assert {:ok, [obs]} = NetworkInsightPrompt.parse(json)
|
|
assert obs.title == "ok"
|
|
end
|
|
|
|
test "clamps urgency to one of critical/warning/info, defaults to info" do
|
|
json = ~s|{"observations": [{"title": "x", "description": "y", "urgency": "panic"}]}|
|
|
|
|
assert {:ok, [obs]} = NetworkInsightPrompt.parse(json)
|
|
assert obs.urgency == "info"
|
|
end
|
|
|
|
test "preserves optional metadata and recommended_action" do
|
|
json =
|
|
~s|{"observations": [{"title": "x", "description": "y", "urgency": "warning", "recommended_action": "Reboot AP", "device_id": "dev-7"}]}|
|
|
|
|
assert {:ok, [obs]} = NetworkInsightPrompt.parse(json)
|
|
assert obs.recommended_action == "Reboot AP"
|
|
assert obs.metadata["device_id"] == "dev-7"
|
|
end
|
|
end
|
|
end
|