using System.Data.Common; using Microsoft.EntityFrameworkCore.Diagnostics; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit; using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories; namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests; /// /// Query-shape coverage for the Site Call Audit KPI snapshots (review 2026-10-21 /// 06/PERF-3 + 08/P3). Each snapshot must be a single conditional-aggregation query /// (plus at most one bounded oldest-pending lookup) — the old shape issued 7 (global) /// and 6 (per-site / per-node) sequential round trips. Correctness against the real /// schema is covered by the MS SQL SiteCallAuditRepositoryTests. /// public class SiteCallAuditRepositoryKpiQueryShapeTests { private static SiteCall NewRow( string site, string? node, string status, DateTime createdAt, DateTime? terminalAt = null) => new() { TrackedOperationId = TrackedOperationId.New(), Channel = "ApiOutbound", Target = "ERP.GetOrder", SourceSite = site, SourceNode = node, Status = status, RetryCount = 0, CreatedAtUtc = createdAt, UpdatedAtUtc = terminalAt ?? createdAt, TerminalAtUtc = terminalAt, IngestedAtUtc = createdAt, }; /// Verifies that all three KPI snapshots issue at most two queries and return the right counts. /// A task that represents the asynchronous operation. [Fact] public async Task AllThreeKpiSnapshots_IssueAtMostTwoQueries() { var counter = new SelectCountingInterceptor(); await using var ctx = SqliteTestHelper.CreateInMemoryContext(counter); var now = DateTime.UtcNow; ctx.SiteCalls.Add(NewRow("plant-a", "node-a", "Attempted", now.AddMinutes(-30))); ctx.SiteCalls.Add(NewRow("plant-a", "node-a", "Attempted", now.AddMinutes(-1))); ctx.SiteCalls.Add(NewRow("plant-a", "node-a", "Parked", now.AddMinutes(-40), now.AddMinutes(-35))); ctx.SiteCalls.Add(NewRow("plant-a", "node-b", "Delivered", now.AddMinutes(-3), now.AddMinutes(-2))); ctx.SiteCalls.Add(NewRow("plant-b", null, "Failed", now.AddMinutes(-3), now.AddMinutes(-2))); // Historical terminal row outside the interval — contributes to no metric. ctx.SiteCalls.Add(NewRow("plant-c", "node-c", "Delivered", now.AddDays(-3), now.AddDays(-3))); await ctx.SaveChangesAsync(); var repo = new SiteCallAuditRepository(ctx); var stuckCutoff = now.AddMinutes(-10); var intervalSince = now.AddMinutes(-5); counter.Reset(); var global = await repo.ComputeKpisAsync(stuckCutoff, intervalSince); Assert.True(counter.Count <= 2, $"ComputeKpisAsync issued {counter.Count} queries against SiteCalls; expected <= 2"); Assert.Equal(2, global.BufferedCount); Assert.Equal(1, global.ParkedCount); Assert.Equal(1, global.FailedLastInterval); Assert.Equal(1, global.DeliveredLastInterval); Assert.Equal(1, global.StuckCount); Assert.NotNull(global.OldestPendingAge); Assert.True(global.OldestPendingAge >= TimeSpan.FromMinutes(29)); counter.Reset(); var perSite = await repo.ComputePerSiteKpisAsync(stuckCutoff, intervalSince); Assert.True(counter.Count <= 2, $"ComputePerSiteKpisAsync issued {counter.Count} queries against SiteCalls; expected <= 2"); // plant-c contributes to no metric, so it does not surface as an all-zero row. Assert.Equal(new[] { "plant-a", "plant-b" }, perSite.Select(s => s.SourceSite).ToArray()); var a = perSite[0]; Assert.Equal((2, 1, 0, 1, 1), (a.BufferedCount, a.ParkedCount, a.FailedLastInterval, a.DeliveredLastInterval, a.StuckCount)); Assert.NotNull(a.OldestPendingAge); var b = perSite[1]; Assert.Equal((0, 0, 1, 0, 0), (b.BufferedCount, b.ParkedCount, b.FailedLastInterval, b.DeliveredLastInterval, b.StuckCount)); Assert.Null(b.OldestPendingAge); counter.Reset(); var perNode = await repo.ComputePerNodeKpisAsync(stuckCutoff, intervalSince); Assert.True(counter.Count <= 2, $"ComputePerNodeKpisAsync issued {counter.Count} queries against SiteCalls; expected <= 2"); // The NULL-node row and the metric-less node-c are excluded. Assert.Equal(new[] { "node-a", "node-b" }, perNode.Select(n => n.SourceNode).ToArray()); var na = perNode[0]; Assert.Equal((2, 1, 0, 0, 1), (na.BufferedCount, na.ParkedCount, na.FailedLastInterval, na.DeliveredLastInterval, na.StuckCount)); Assert.NotNull(na.OldestPendingAge); var nb = perNode[1]; Assert.Equal((0, 0, 0, 1, 0), (nb.BufferedCount, nb.ParkedCount, nb.FailedLastInterval, nb.DeliveredLastInterval, nb.StuckCount)); Assert.Null(nb.OldestPendingAge); } /// /// Records the text of every reader command touching the SiteCalls table. /// private sealed class SelectCountingInterceptor : DbCommandInterceptor { private readonly List _commands = new(); /// The number of reader commands recorded so far. public int Count => _commands.Count; /// Clears all recorded commands. public void Reset() => _commands.Clear(); private void Record(DbCommand command) { if (command.CommandText.Contains("SiteCalls", StringComparison.OrdinalIgnoreCase)) { _commands.Add(command.CommandText); } } /// public override InterceptionResult ReaderExecuting( DbCommand command, CommandEventData eventData, InterceptionResult result) { Record(command); return base.ReaderExecuting(command, eventData, result); } /// public override ValueTask> ReaderExecutingAsync( DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { Record(command); return base.ReaderExecutingAsync(command, eventData, result, cancellationToken); } } }