using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi; namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Kpi; /// /// Tests for — the gauge-vs-rate /// classification the hourly rollup fold consults. Covers totality over every /// metric the four IKpiSampleSource implementations actually emit, the /// specific Rate set, a gauge sampling, and the unknown-metric default. /// public class KpiMetricAggregationTests { // Every (source, metric) pair emitted by the four sample sources, transcribed // from the source code. The catalog must classify all of them (totality). public static readonly (string Source, string Metric)[] AllEmittedMetrics = { // NotificationOutboxKpiSampleSource (KpiSources.NotificationOutbox, "queueDepth"), (KpiSources.NotificationOutbox, "stuckCount"), (KpiSources.NotificationOutbox, "parkedCount"), (KpiSources.NotificationOutbox, "deliveredLastInterval"), (KpiSources.NotificationOutbox, "oldestPendingAgeSeconds"), // SiteCallAuditKpiSampleSource (KpiSources.SiteCallAudit, "buffered"), (KpiSources.SiteCallAudit, "parked"), (KpiSources.SiteCallAudit, "failedLastInterval"), (KpiSources.SiteCallAudit, "deliveredLastInterval"), (KpiSources.SiteCallAudit, "stuck"), (KpiSources.SiteCallAudit, "oldestPendingAgeSeconds"), // AuditLogKpiSampleSource (KpiSources.AuditLog, "totalEventsLastHour"), (KpiSources.AuditLog, "errorEventsLastHour"), (KpiSources.AuditLog, "backlogTotal"), // SiteHealthKpiSampleSource (KpiSources.SiteHealth, "connectionsUp"), (KpiSources.SiteHealth, "connectionsDown"), (KpiSources.SiteHealth, "scriptErrors"), (KpiSources.SiteHealth, "alarmEvalErrors"), (KpiSources.SiteHealth, "sfBufferDepth"), (KpiSources.SiteHealth, "deadLetters"), (KpiSources.SiteHealth, "parkedMessages"), (KpiSources.SiteHealth, "deployedInstances"), (KpiSources.SiteHealth, "enabledInstances"), (KpiSources.SiteHealth, "disabledInstances"), (KpiSources.SiteHealth, "auditBacklogPending"), (KpiSources.SiteHealth, "eventLogWriteFailures"), // Register row 33. Both are instantaneous readings, so both fold as Gauge (the // default) — summing an oplog depth per hour would be meaningless, and summing a // 0/1 link flag doubly so. Both are CONDITIONALLY emitted: a site that reports no // reading writes no sample rather than a fabricated 0. (KpiSources.SiteHealth, "localDbOplogBacklog"), (KpiSources.SiteHealth, "localDbReplicationConnected"), }; // The (source, metric) pairs that must fold as Rate (sum-per-hour). public static readonly (string Source, string Metric)[] RateMetrics = { (KpiSources.NotificationOutbox, "deliveredLastInterval"), (KpiSources.SiteCallAudit, "failedLastInterval"), (KpiSources.SiteCallAudit, "deliveredLastInterval"), (KpiSources.SiteHealth, "scriptErrors"), (KpiSources.SiteHealth, "alarmEvalErrors"), (KpiSources.SiteHealth, "deadLetters"), (KpiSources.SiteHealth, "eventLogWriteFailures"), }; // A representative sampling of gauges — including the two *LastHour audit // metrics that are rolling last-hour gauges, NOT rates (deliberate override). public static readonly (string Source, string Metric)[] GaugeSampling = { (KpiSources.NotificationOutbox, "queueDepth"), (KpiSources.NotificationOutbox, "oldestPendingAgeSeconds"), (KpiSources.SiteCallAudit, "buffered"), (KpiSources.SiteCallAudit, "stuck"), (KpiSources.AuditLog, "backlogTotal"), (KpiSources.AuditLog, "totalEventsLastHour"), (KpiSources.AuditLog, "errorEventsLastHour"), (KpiSources.SiteHealth, "connectionsUp"), (KpiSources.SiteHealth, "connectionsDown"), (KpiSources.SiteHealth, "sfBufferDepth"), (KpiSources.SiteHealth, "deployedInstances"), (KpiSources.SiteHealth, "localDbOplogBacklog"), (KpiSources.SiteHealth, "localDbReplicationConnected"), }; /// Supplies every emitted source and metric pair as theory data. /// The theory data rows of source and metric names. public static IEnumerable AllEmittedCases() => AllEmittedMetrics.Select(m => new object[] { m.Source, m.Metric }); /// Supplies the rate metric source and metric pairs as theory data. /// The theory data rows of source and metric names. public static IEnumerable RateCases() => RateMetrics.Select(m => new object[] { m.Source, m.Metric }); /// Supplies the gauge-sampled source and metric pairs as theory data. /// The theory data rows of source and metric names. public static IEnumerable GaugeCases() => GaugeSampling.Select(m => new object[] { m.Source, m.Metric }); /// Verifies that every emitted metric resolves to a defined aggregation. /// The KPI source name. /// The metric name. [Theory] [MemberData(nameof(AllEmittedCases))] public void Resolve_EveryEmittedMetric_ReturnsDefinedAggregation(string source, string metric) { var result = KpiMetricAggregationCatalog.Resolve(source, metric); Assert.True( result is KpiRollupAggregation.Gauge or KpiRollupAggregation.Rate, $"({source}, {metric}) resolved to an undefined aggregation: {result}"); } /// Verifies that rate metrics are classified as rate. /// The KPI source name. /// The metric name. [Theory] [MemberData(nameof(RateCases))] public void Resolve_RateMetrics_ClassifiedAsRate(string source, string metric) { Assert.Equal(KpiRollupAggregation.Rate, KpiMetricAggregationCatalog.Resolve(source, metric)); } /// Verifies that gauge-sampled metrics are classified as gauge. /// The KPI source name. /// The metric name. [Theory] [MemberData(nameof(GaugeCases))] public void Resolve_GaugeSampling_ClassifiedAsGauge(string source, string metric) { Assert.Equal(KpiRollupAggregation.Gauge, KpiMetricAggregationCatalog.Resolve(source, metric)); } /// Verifies that last-hour audit metrics resolve as gauges rather than rates. [Fact] public void Resolve_LastHourAuditMetrics_AreGaugeNotRate() { // Rolling trailing-one-hour counts: last-value-per-hour is correct; summing // 60 overlapping minute samples would ~60x over-count. Guard the decision. Assert.Equal( KpiRollupAggregation.Gauge, KpiMetricAggregationCatalog.Resolve(KpiSources.AuditLog, "totalEventsLastHour")); Assert.Equal( KpiRollupAggregation.Gauge, KpiMetricAggregationCatalog.Resolve(KpiSources.AuditLog, "errorEventsLastHour")); } /// Verifies that an unknown metric defaults to a gauge. /// The KPI source name. /// The metric name. [Theory] [InlineData(KpiSources.NotificationOutbox, "someBrandNewMetricAddedLater")] [InlineData("AnEntirelyNewSource", "anyMetric")] [InlineData(KpiSources.SiteHealth, "")] public void Resolve_UnknownMetric_DefaultsToGauge(string source, string metric) { // The fold must never crash on a metric added after this catalog was updated; // Gauge (last-value) is the safe, non-fabricating default. Assert.Equal(KpiRollupAggregation.Gauge, KpiMetricAggregationCatalog.Resolve(source, metric)); } /// Verifies that the same metric name under a different source is keyed on the source and metric pair. [Fact] public void Resolve_SameMetricNameDifferentSource_KeysOnSourcePair() { // deliveredLastInterval is Rate under both sources here, but the point is the // API keys on (source, metric) rather than metric alone — both resolve. Assert.Equal( KpiRollupAggregation.Rate, KpiMetricAggregationCatalog.Resolve(KpiSources.NotificationOutbox, "deliveredLastInterval")); Assert.Equal( KpiRollupAggregation.Rate, KpiMetricAggregationCatalog.Resolve(KpiSources.SiteCallAudit, "deliveredLastInterval")); } // ---- Catalog metric literals promoted to shared KpiMetrics constants (R4) ---- /// Verifies that the promoted metric constants lock the historical persisted values. [Fact] public void PromotedMetricConstants_LockHistoricalPersistedValues() { // Persisted data contract — these are symbol promotions, NOT renames. Assert.Equal("deliveredLastInterval", KpiMetrics.SiteCallAudit.DeliveredLastInterval); Assert.Equal("alarmEvalErrors", KpiMetrics.SiteHealth.AlarmEvalErrors); Assert.Equal("eventLogWriteFailures", KpiMetrics.SiteHealth.EventLogWriteFailures); } /// Verifies that the promoted rate pairs still classify as rate. /// The KPI source name. /// The metric name. [Theory] [InlineData(KpiSources.SiteCallAudit, KpiMetrics.SiteCallAudit.DeliveredLastInterval)] [InlineData(KpiSources.SiteHealth, KpiMetrics.SiteHealth.AlarmEvalErrors)] [InlineData(KpiSources.SiteHealth, KpiMetrics.SiteHealth.EventLogWriteFailures)] public void Resolve_PromotedRatePairs_StillClassifyAsRate(string source, string metric) => Assert.Equal(KpiRollupAggregation.Rate, KpiMetricAggregationCatalog.Resolve(source, metric)); }