Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions.Tests/MetricsCutoffRowKeyTests.cs
T
Oliver Kieselbach eee65ba000 fix(metrics): honor days time-window in MCP get_metrics, get_usage_metrics, get_platform_metrics
The three metrics MCP tools previously ignored the time-window: get_metrics never
read the days query param (returned all-time totals), get_usage_metrics didn't
expose days at all and the service was hardcoded to 90, get_platform_metrics
had a fixed 500-most-recent-sessions cap. A "last 7 days" question therefore
returned cross-tenant all-time aggregates (~1300) instead of the actual 7-day
count (~500).

End-to-end fix:
- Plumbing days through MetricsSummaryFunction → IMetricsRepository →
  TableStorageService.GetMetricsSummaryAsync, with a server-side RowKey cutoff
  on SessionsIndex (reuses the inverted-tick D19 prefix pattern; extracted as
  ComputeCutoffRowKeyPrefix and the two pre-existing call sites consolidated).
- UsageMetricsService and PlatformMetricsService now accept int days, with
  per-window in-memory caches and a WindowDays field echoed on the response.
- MCP zod schemas accept any integer 1-365 (replacing the [7,30,90] whitelist),
  so callers can now ask for 5 or 12 days and get the right answer.
- Function-layer defaults stay at 90 for /usage and /platform routes (no
  dashboard regression) and 30 for /summary (Agent API, MCP-only path).

Tests: 22 new xUnit cases across MetricsCutoffRowKeyTests,
UsageMetricsServiceWindowTests, PlatformMetricsServiceWindowTests covering
boundary, clamping, and end-to-end window semantics. Backend 808/808 green,
web vitest 208/208 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 08:45:55 +02:00

58 lines
2.4 KiB
C#

using AutopilotMonitor.Functions.Services;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Verifies ComputeCutoffRowKeyPrefix produces a SessionsIndex RowKey upper bound that
/// lexicographically separates sessions newer than `now - days` (kept) from older ones (excluded).
/// </summary>
public class MetricsCutoffRowKeyTests
{
private static string IndexRowKey(DateTime startedAt, string sessionId)
=> $"{(DateTime.MaxValue.Ticks - startedAt.Ticks):D19}_{sessionId}";
[Theory]
[InlineData(1)]
[InlineData(5)]
[InlineData(7)]
[InlineData(12)]
[InlineData(30)]
[InlineData(90)]
[InlineData(365)]
public void Cutoff_keeps_sessions_inside_window_excludes_outside(int days)
{
var cutoffPrefix = TableStorageService.ComputeCutoffRowKeyPrefix(days);
// Inside the window: 1 hour newer than the cutoff.
var insideRowKey = IndexRowKey(DateTime.UtcNow.AddDays(-days).AddHours(1), Guid.NewGuid().ToString());
// Outside the window: 1 hour older than the cutoff.
var outsideRowKey = IndexRowKey(DateTime.UtcNow.AddDays(-days).AddHours(-1), Guid.NewGuid().ToString());
// RowKey lt cutoffPrefix → include
Assert.True(string.CompareOrdinal(insideRowKey, cutoffPrefix) < 0,
$"days={days}: session inside window should be < cutoff (inside={insideRowKey}, cutoff={cutoffPrefix})");
Assert.False(string.CompareOrdinal(outsideRowKey, cutoffPrefix) < 0,
$"days={days}: session outside window should NOT be < cutoff (outside={outsideRowKey}, cutoff={cutoffPrefix})");
}
[Fact]
public void Cutoff_prefix_is_19_digits_padded()
{
var prefix = TableStorageService.ComputeCutoffRowKeyPrefix(30);
Assert.Equal(19, prefix.Length);
Assert.True(prefix.All(char.IsDigit), $"Prefix must be all digits, got: {prefix}");
}
[Fact]
public void Larger_window_produces_lexicographically_larger_prefix()
{
// Older cutoff → larger inverted ticks → larger prefix string.
var prefix7 = TableStorageService.ComputeCutoffRowKeyPrefix(7);
var prefix30 = TableStorageService.ComputeCutoffRowKeyPrefix(30);
var prefix90 = TableStorageService.ComputeCutoffRowKeyPrefix(90);
Assert.True(string.CompareOrdinal(prefix7, prefix30) < 0);
Assert.True(string.CompareOrdinal(prefix30, prefix90) < 0);
}
}