feat(audit): M5.2 per-node stuck-count KPIs (T6) — repo per-node aggregation, actor message pair, CentralUI tiles
This commit is contained in:
@@ -594,6 +594,43 @@ public class SiteCallAuditActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
Assert.NotNull(response.OldestPendingAge);
|
||||
}
|
||||
|
||||
// ── Per-node KPI (T6: M5.2 per-node stuck-count KPIs) ──────────────────
|
||||
|
||||
[SkippableFact]
|
||||
public async Task PerNodeSiteCallKpiRequest_ScopesCountsToEachNode()
|
||||
{
|
||||
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
|
||||
|
||||
var nodeId = "node-" + Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||
await using var context = CreateContext();
|
||||
var repo = new SiteCallAuditRepository(context);
|
||||
var actor = CreateActor(repo, new SiteCallAuditOptions
|
||||
{
|
||||
StuckAgeThreshold = TimeSpan.FromMinutes(10),
|
||||
KpiInterval = TimeSpan.FromHours(1),
|
||||
});
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var siteId = NewSiteId();
|
||||
// Non-terminal Attempted, created 30 min ago — buffered + stuck.
|
||||
await repo.UpsertAsync(NewRow(TrackedOperationId.New(), siteId, status: "Attempted",
|
||||
createdAtUtc: now.AddMinutes(-30), sourceNode: nodeId));
|
||||
// Terminal Parked.
|
||||
await repo.UpsertAsync(NewRow(TrackedOperationId.New(), siteId, status: "Parked",
|
||||
createdAtUtc: now.AddMinutes(-5), terminal: true, sourceNode: nodeId));
|
||||
|
||||
actor.Tell(new PerNodeSiteCallKpiRequest("corr-pnk"), TestActor);
|
||||
|
||||
var response = ExpectMsg<PerNodeSiteCallKpiResponse>(TimeSpan.FromSeconds(10));
|
||||
Assert.True(response.Success);
|
||||
|
||||
var myNode = Assert.Single(response.Nodes, n => n.SourceNode == nodeId);
|
||||
Assert.Equal(1, myNode.BufferedCount);
|
||||
Assert.Equal(1, myNode.ParkedCount);
|
||||
Assert.Equal(1, myNode.StuckCount);
|
||||
Assert.NotNull(myNode.OldestPendingAge);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task PerSiteSiteCallKpiRequest_ScopesCountsToEachSite()
|
||||
{
|
||||
@@ -745,6 +782,10 @@ public class SiteCallAuditActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
public Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
_inner.ComputePerSiteKpisAsync(stuckCutoff, intervalSince, ct);
|
||||
|
||||
public Task<IReadOnlyList<SiteCallNodeKpiSnapshot>> ComputePerNodeKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
_inner.ComputePerNodeKpisAsync(stuckCutoff, intervalSince, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -790,5 +831,9 @@ public class SiteCallAuditActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
public Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
_inner.ComputePerSiteKpisAsync(stuckCutoff, intervalSince, ct);
|
||||
|
||||
public Task<IReadOnlyList<SiteCallNodeKpiSnapshot>> ComputePerNodeKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
_inner.ComputePerNodeKpisAsync(stuckCutoff, intervalSince, ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,10 @@ public class SiteCallAuditPurgeTests : TestKit
|
||||
public Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlyList<SiteCallSiteKpiSnapshot>>(Array.Empty<SiteCallSiteKpiSnapshot>());
|
||||
|
||||
public Task<IReadOnlyList<SiteCallNodeKpiSnapshot>> ComputePerNodeKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlyList<SiteCallNodeKpiSnapshot>>(Array.Empty<SiteCallNodeKpiSnapshot>());
|
||||
}
|
||||
|
||||
/// <summary>Repository whose purge always throws — to prove continue-on-error keeps the singleton alive.</summary>
|
||||
@@ -94,6 +98,7 @@ public class SiteCallAuditPurgeTests : TestKit
|
||||
public Task<IReadOnlyList<SiteCall>> QueryAsync(SiteCallQueryFilter f, SiteCallPaging p, CancellationToken ct = default) => Task.FromResult<IReadOnlyList<SiteCall>>(Array.Empty<SiteCall>());
|
||||
public Task<SiteCallKpiSnapshot> ComputeKpisAsync(DateTime a, DateTime b, CancellationToken ct = default) => Task.FromResult(new SiteCallKpiSnapshot(0, 0, 0, 0, null, 0));
|
||||
public Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync(DateTime a, DateTime b, CancellationToken ct = default) => Task.FromResult<IReadOnlyList<SiteCallSiteKpiSnapshot>>(Array.Empty<SiteCallSiteKpiSnapshot>());
|
||||
public Task<IReadOnlyList<SiteCallNodeKpiSnapshot>> ComputePerNodeKpisAsync(DateTime a, DateTime b, CancellationToken ct = default) => Task.FromResult<IReadOnlyList<SiteCallNodeKpiSnapshot>>(Array.Empty<SiteCallNodeKpiSnapshot>());
|
||||
}
|
||||
|
||||
private IActorRef CreateActor(ISiteCallAuditRepository repo, SiteCallAuditOptions options) =>
|
||||
|
||||
@@ -142,6 +142,10 @@ public class SiteCallAuditReconciliationTests : TestKit
|
||||
public Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlyList<SiteCallSiteKpiSnapshot>>(Array.Empty<SiteCallSiteKpiSnapshot>());
|
||||
|
||||
public Task<IReadOnlyList<SiteCallNodeKpiSnapshot>> ComputePerNodeKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlyList<SiteCallNodeKpiSnapshot>>(Array.Empty<SiteCallNodeKpiSnapshot>());
|
||||
}
|
||||
|
||||
private IActorRef CreateActor(
|
||||
|
||||
@@ -50,6 +50,10 @@ public class SiteCallRelayTests : TestKit
|
||||
public Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
throw new InvalidOperationException("relay must not compute per-site KPIs");
|
||||
|
||||
public Task<IReadOnlyList<SiteCallNodeKpiSnapshot>> ComputePerNodeKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
|
||||
throw new InvalidOperationException("relay must not compute per-node KPIs");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user