feat(sitecallaudit): query, KPI and detail backend for the Site Calls page
This commit is contained in:
@@ -338,6 +338,104 @@ public class SiteCallAuditRepositoryTests : IClassFixture<MsSqlMigrationFixture>
|
||||
Assert.NotNull(await repo.GetAsync(recentTerminalId));
|
||||
}
|
||||
|
||||
// --- KPI snapshot tests -------------------------------------------------
|
||||
|
||||
[SkippableFact]
|
||||
public async Task ComputeKpisAsync_CountsBufferedParkedFailedDeliveredAndStuck()
|
||||
{
|
||||
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
|
||||
|
||||
var site = NewSiteId();
|
||||
await using var context = CreateContext();
|
||||
var repo = new SiteCallAuditRepository(context);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var stuckCutoff = now.AddMinutes(-10);
|
||||
var intervalSince = now.AddHours(-1);
|
||||
|
||||
// Buffered + stuck (non-terminal Attempted, created 30 min ago).
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), site, status: "Attempted", createdAtUtc: now.AddMinutes(-30)));
|
||||
// Buffered but NOT stuck (non-terminal Attempted, created 2 min ago).
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), site, status: "Attempted", createdAtUtc: now.AddMinutes(-2)));
|
||||
// Parked (terminal).
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), site, status: "Parked",
|
||||
createdAtUtc: now.AddMinutes(-5), updatedAtUtc: now.AddMinutes(-4),
|
||||
terminal: true, terminalAtUtc: now.AddMinutes(-4)));
|
||||
// Delivered within the interval.
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), site, status: "Delivered",
|
||||
createdAtUtc: now.AddMinutes(-4), updatedAtUtc: now.AddMinutes(-1),
|
||||
terminal: true, terminalAtUtc: now.AddMinutes(-1)));
|
||||
// Failed within the interval.
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), site, status: "Failed",
|
||||
createdAtUtc: now.AddMinutes(-6), updatedAtUtc: now.AddMinutes(-2),
|
||||
terminal: true, terminalAtUtc: now.AddMinutes(-2)));
|
||||
// Delivered OUTSIDE the interval (2 hours ago) — must not count.
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), site, status: "Delivered",
|
||||
createdAtUtc: now.AddHours(-3), updatedAtUtc: now.AddHours(-2),
|
||||
terminal: true, terminalAtUtc: now.AddHours(-2)));
|
||||
|
||||
var snapshot = await repo.ComputeKpisAsync(stuckCutoff, intervalSince);
|
||||
|
||||
// Counts are global; assert the floor since the table is shared with
|
||||
// other tests. The OUTSIDE-interval Delivered row proves the window
|
||||
// bounds the throughput counts.
|
||||
Assert.True(snapshot.BufferedCount >= 2);
|
||||
Assert.True(snapshot.ParkedCount >= 1);
|
||||
Assert.True(snapshot.StuckCount >= 1);
|
||||
Assert.True(snapshot.DeliveredLastInterval >= 1);
|
||||
Assert.True(snapshot.FailedLastInterval >= 1);
|
||||
Assert.NotNull(snapshot.OldestPendingAge);
|
||||
Assert.True(snapshot.OldestPendingAge >= TimeSpan.FromMinutes(25));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task ComputePerSiteKpisAsync_ScopesCountsToEachSite()
|
||||
{
|
||||
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
|
||||
|
||||
var siteA = NewSiteId();
|
||||
var siteB = NewSiteId();
|
||||
await using var context = CreateContext();
|
||||
var repo = new SiteCallAuditRepository(context);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var stuckCutoff = now.AddMinutes(-10);
|
||||
var intervalSince = now.AddHours(-1);
|
||||
|
||||
// siteA: 2 buffered (one stuck), 1 parked.
|
||||
await repo.UpsertAsync(NewRow(TrackedOperationId.New(), siteA, status: "Attempted", createdAtUtc: now.AddMinutes(-30)));
|
||||
await repo.UpsertAsync(NewRow(TrackedOperationId.New(), siteA, status: "Attempted", createdAtUtc: now.AddMinutes(-2)));
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), siteA, status: "Parked",
|
||||
createdAtUtc: now.AddMinutes(-5), updatedAtUtc: now.AddMinutes(-4),
|
||||
terminal: true, terminalAtUtc: now.AddMinutes(-4)));
|
||||
// siteB: 1 delivered within interval only.
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), siteB, status: "Delivered",
|
||||
createdAtUtc: now.AddMinutes(-4), updatedAtUtc: now.AddMinutes(-1),
|
||||
terminal: true, terminalAtUtc: now.AddMinutes(-1)));
|
||||
|
||||
var perSite = await repo.ComputePerSiteKpisAsync(stuckCutoff, intervalSince);
|
||||
|
||||
var a = Assert.Single(perSite, s => s.SourceSite == siteA);
|
||||
Assert.Equal(2, a.BufferedCount);
|
||||
Assert.Equal(1, a.ParkedCount);
|
||||
Assert.Equal(1, a.StuckCount);
|
||||
Assert.NotNull(a.OldestPendingAge);
|
||||
|
||||
var b = Assert.Single(perSite, s => s.SourceSite == siteB);
|
||||
Assert.Equal(0, b.BufferedCount);
|
||||
Assert.Equal(1, b.DeliveredLastInterval);
|
||||
// siteB has no non-terminal rows — no oldest-pending age.
|
||||
Assert.Null(b.OldestPendingAge);
|
||||
}
|
||||
|
||||
// --- helpers ------------------------------------------------------------
|
||||
|
||||
private ScadaLinkDbContext CreateContext()
|
||||
|
||||
Reference in New Issue
Block a user