Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions.Tests/MetricsAggregationHelperTests.cs
T
Oliver Kieselbach 908fb8e7ac fix(metrics): clamp duration aggregates, reconcile status buckets, guard slowest-apps sample size
Three data-hygiene fixes for the MCP metric tools, all driven by
incomplete/abandoned sessions distorting aggregates:

- get_usage_metrics: clamp each session duration to the shared 7-day
  ceiling before avg/median/p95/p99, so stuck non-terminal sessions
  (unclamped now-StartedAt wall-clock) no longer push p99 into weeks.
  Surface SampleCount + ClampedSessionCount so the skew stays visible.
- get_metrics: tally Pending/Stalled into their own buckets and route
  any other status into `other`, so the per-tenant buckets reconcile to
  total by construction (closes the silent 214/134 gap). Extracted the
  fold into SessionStatusBuckets.
- slowestApps: require >=3 successful installs via the shared
  MetricsMath.SelectSlowestApps helper, so a single N=1 (often
  unfinished, legacy pre-clamp) install cannot dominate the ranking;
  also de-dupes the copy-pasted block across the tenant + global
  app-metrics functions.

Adds xUnit regression cover for all three.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:39:49 +02:00

63 lines
2.5 KiB
C#

using System.Linq;
using AutopilotMonitor.Functions.Helpers;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Covers the pure metric-aggregation helpers behind get_metrics: status bucketing must reconcile
/// to the total (Pending/Stalled no longer vanish), and slowest-apps ranking must drop N&lt;min
/// samples so a single install can't dominate.
/// </summary>
public class MetricsAggregationHelperTests
{
[Fact]
public void SessionStatusBuckets_reconcile_to_total_across_all_statuses()
{
// Includes the previously-dropped Pending/Stalled plus unrecognised + null statuses.
var statuses = new[] { "Succeeded", "Succeeded", "Failed", "InProgress", "Pending", "Stalled", "Unknown", "WeirdNew", null };
var b = statuses.Aggregate(default(SessionStatusBuckets), (acc, s) => acc.Add(s));
Assert.Equal(9, b.Total);
Assert.Equal(2, b.Succeeded);
Assert.Equal(1, b.Failed);
Assert.Equal(1, b.InProgress);
Assert.Equal(1, b.Pending);
Assert.Equal(1, b.Stalled);
Assert.Equal(3, b.Other); // Unknown + WeirdNew + null
// The invariant that closes the bucket-gap finding: components sum to the total exactly.
Assert.Equal(b.Total, b.Succeeded + b.Failed + b.InProgress + b.Pending + b.Stalled + b.Other);
}
[Fact]
public void SelectSlowestApps_drops_below_min_sample_and_orders_desc()
{
var apps = new[]
{
new { name = "n1-huge", succeeded = 1, avg = 99999.0 }, // N=1 artefact -> excluded
new { name = "ok-slow", succeeded = 5, avg = 500.0 },
new { name = "ok-fast", succeeded = 10, avg = 100.0 },
new { name = "n2", succeeded = 2, avg = 8000.0 }, // N=2 -> excluded
};
var result = MetricsMath.SelectSlowestApps(apps, a => a.succeeded, a => a.avg, minSamples: 3, take: 10);
Assert.Equal(2, result.Count);
Assert.Equal("ok-slow", result[0].name); // slowest of the qualifying apps first
Assert.Equal("ok-fast", result[1].name);
}
[Fact]
public void SelectSlowestApps_honours_take_limit()
{
var apps = Enumerable.Range(0, 20)
.Select(i => new { succeeded = 5, avg = (double)i })
.ToArray();
var result = MetricsMath.SelectSlowestApps(apps, a => a.succeeded, a => a.avg, minSamples: 3, take: 10);
Assert.Equal(10, result.Count);
Assert.Equal(19.0, result[0].avg); // highest avg first
}
}