towerops/test/towerops_web/telemetry_test.exs
Graham McIntire 727a743c11 feat(llm): per-insight-type prompt hints + flake fixes
Generic prompts produce generic advice. This commit adds per-type
expert hints appended to the base system prompt, listing the metadata
field names the rule provides plus short domain heuristics so the
LLM has the context it needs to write a tight, specific recommendation.

Hints cover: ap_frequency_change, own_fleet_channel_conflict,
sector_overload, cpe_realign, wireless_signal_weak, wireless_snr_low,
backhaul_over_capacity, backhaul_near_capacity, qoe_degradation,
capacity_saturation. Unknown types fall back to the base prompt.

No change to rules, schema, or worker — this lifts every existing
insight's enrichment quality on the next cron run.

Also fixes two recurring flaky tests:

- agent_live_test.exs:567 now mirrors the companion show_test.exs
  pattern of accepting any of the valid update-flow flashes (Failed
  / Update command sent / No binary available). The previous
  Req.Test stub-and-allow approach was itself flaky because both
  test files stub the same module name and ownership tracking can
  leak under full-suite parallelism.
- telemetry_test.exs is now async: false. Its handle_endpoint_stop
  describe block mutates global Logger.level which races with other
  async tests; running this single module sync removes the race
  without sacrificing the per-test setup/restore pattern.
2026-05-09 17:36:07 -05:00

229 lines
6.1 KiB
Elixir

defmodule ToweropsWeb.TelemetryTest do
# Not async: this module mutates the global Logger primary level in
# the handle_endpoint_stop describe block, which races with other
# async tests that adjust Logger configuration. Running it sync makes
# the captured-log assertions deterministic.
use ExUnit.Case, async: false
import ExUnit.CaptureLog
alias ToweropsWeb.Telemetry
describe "parse_redis_info/1" do
test "parses a minimal Redis INFO response" do
info = """
# Stats
total_connections_received:12
total_commands_processed:500
instantaneous_ops_per_sec:5
"""
result = Telemetry.parse_redis_info(info)
assert result["total_connections_received"] == "12"
assert result["total_commands_processed"] == "500"
assert result["instantaneous_ops_per_sec"] == "5"
end
test "skips comment/empty lines" do
info = "\n# Comment\n\nkey:value\n# Another\n"
assert %{"key" => "value"} == Telemetry.parse_redis_info(info)
end
test "returns empty map for empty input" do
assert %{} == Telemetry.parse_redis_info("")
end
test "handles CRLF line endings" do
info = "a:1\r\nb:2\r\n"
assert %{"a" => "1", "b" => "2"} == Telemetry.parse_redis_info(info)
end
test "splits on first colon only" do
info = "url:redis://localhost:6379"
assert %{"url" => "redis://localhost:6379"} == Telemetry.parse_redis_info(info)
end
test "drops malformed entries without colon" do
info = "valid:1\njunk_no_colon\nanother:2"
assert %{"valid" => "1", "another" => "2"} == Telemetry.parse_redis_info(info)
end
end
describe "metrics/0" do
test "returns a list of metric specs" do
metrics = Telemetry.metrics()
assert is_list(metrics)
refute Enum.empty?(metrics)
end
end
describe "handle_router_exception/4" do
test "logs an error with exception metadata" do
conn = %Plug.Conn{
method: "GET",
request_path: "/api/devices",
assigns: %{request_id: "req-123"}
}
metadata = %{
conn: conn,
kind: :error,
reason: "something went wrong",
stacktrace: [],
plug: MyTestPlug
}
log =
capture_log(fn ->
Telemetry.handle_router_exception(:event, %{}, metadata, nil)
end)
assert log =~ "Router exception on Elixir.MyTestPlug GET /api/devices"
assert log =~ "[error]"
end
end
describe "handle_endpoint_stop/4" do
setup do
previous = Logger.level()
Logger.configure(level: :warning)
on_exit(fn -> Logger.configure(level: previous) end)
:ok
end
test "logs warning for slow requests over 5 seconds" do
conn = %Plug.Conn{
method: "POST",
request_path: "/slow-endpoint",
status: 200,
assigns: %{}
}
# Duration > 5000ms in native units
duration_native = System.convert_time_unit(6_000, :millisecond, :native)
log =
capture_log(fn ->
Telemetry.handle_endpoint_stop(
:stop,
%{duration: duration_native},
%{conn: conn},
nil
)
end)
assert log =~ "Slow request: POST /slow-endpoint"
assert log =~ "6000ms"
assert log =~ "[warning]"
end
test "logs error for 5xx status codes" do
conn = %Plug.Conn{
method: "GET",
request_path: "/api/broken",
status: 500,
assigns: %{request_id: "req-456"}
}
duration_native = System.convert_time_unit(100, :millisecond, :native)
log =
capture_log(fn ->
Telemetry.handle_endpoint_stop(
:stop,
%{duration: duration_native},
%{conn: conn},
nil
)
end)
assert log =~ "Server error: GET /api/broken returned 500"
assert log =~ "[error]"
end
test "does not log for fast 2xx requests" do
conn = %Plug.Conn{
method: "GET",
request_path: "/fast-endpoint",
status: 200,
assigns: %{}
}
duration_native = System.convert_time_unit(50, :millisecond, :native)
log =
capture_log(fn ->
Telemetry.handle_endpoint_stop(
:stop,
%{duration: duration_native},
%{conn: conn},
nil
)
end)
# capture_log catches every log line emitted by *any* process while the
# function runs, so a stray DB sandbox or Postgrex disconnect log from
# a concurrent test can show up here. Assert on what this function
# itself does NOT log instead of demanding empty output.
refute log =~ "Slow request"
refute log =~ "Server error"
refute log =~ "/fast-endpoint"
end
test "logs both warning and error for slow 5xx request" do
conn = %Plug.Conn{
method: "DELETE",
request_path: "/api/resource",
status: 503,
assigns: %{request_id: "req-789"}
}
duration_native = System.convert_time_unit(7_000, :millisecond, :native)
log =
capture_log(fn ->
Telemetry.handle_endpoint_stop(
:stop,
%{duration: duration_native},
%{conn: conn},
nil
)
end)
assert log =~ "Slow request: DELETE /api/resource"
assert log =~ "Server error: DELETE /api/resource returned 503"
end
test "does not log for fast 4xx requests" do
conn = %Plug.Conn{
method: "GET",
request_path: "/not-found",
status: 404,
assigns: %{}
}
duration_native = System.convert_time_unit(100, :millisecond, :native)
log =
capture_log(fn ->
Telemetry.handle_endpoint_stop(
:stop,
%{duration: duration_native},
%{conn: conn},
nil
)
end)
refute log =~ "Slow request"
refute log =~ "Server error"
refute log =~ "/not-found"
end
end
describe "publish_oban_stats/0 / publish_redis_stats/0" do
test "are no-ops in test env" do
assert :ok == Telemetry.publish_oban_stats()
assert :ok == Telemetry.publish_redis_stats()
end
end
end