diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/AlarmCapableProtocols.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/AlarmCapableProtocols.cs index e7bfc151..f837f6d3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/AlarmCapableProtocols.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/AlarmCapableProtocols.cs @@ -6,8 +6,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; /// mirror native alarms). /// /// The set MUST stay in sync with the protocols registered against an -/// alarm-subscribable adapter in the DCL DataConnectionFactory: today the -/// "OpcUa" adapter (OpcUaDataConnection) and the "MxGateway" adapter +/// alarm-subscribable adapter in +/// DataConnectionLayer/DataConnectionFactory.cs: today the "OpcUa" adapter +/// (OpcUaDataConnection) and the "MxGateway" adapter /// (MxGatewayDataConnection) both implement /// . The runtime decision is made in /// DataConnectionActor via _adapter is IAlarmSubscribableConnection; @@ -21,7 +22,9 @@ public static class AlarmCapableProtocols /// /// Determines whether a data connection's protocol string resolves to an /// alarm-capable adapter (one implementing ). - /// Case-insensitive; null/blank is not alarm-capable. + /// Case-insensitive to match DataConnectionFactory's own + /// OrdinalIgnoreCase protocol-key lookup; null/blank is not + /// alarm-capable. /// /// The data connection protocol string (e.g. "OpcUa"). /// true when the protocol's adapter can subscribe native alarms; otherwise false. diff --git a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/FlatteningPipeline.cs b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/FlatteningPipeline.cs index 39ccbf68..df78fe6c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/FlatteningPipeline.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/FlatteningPipeline.cs @@ -114,8 +114,14 @@ public class FlatteningPipeline : IFlatteningPipeline // Compute the alarm-capable connection-name set so the semantic validator // can gate native-alarm-source bindings. "Alarm-capable" matches the DCL - // runtime decision (DataConnectionActor: _adapter is IAlarmSubscribableConnection), - // mapped from the protocol string via the shared AlarmCapableProtocols helper. + // runtime decision (DataConnectionActor: _adapter is IAlarmSubscribableConnection); + // here we filter connections by alarm-capable protocol, then collect their names. + // + // StringComparer.Ordinal is intentional: connection names are stored and + // matched as authored throughout the pipeline (all other name-keyed + // dictionaries in FlatteningService and SemanticValidator use the same + // case-sensitive semantics). OrdinalIgnoreCase would be inconsistent with + // the rest of the binding-resolution path. var alarmCapableConnectionNames = dataConnections.Values .Where(c => AlarmCapableProtocols.IsAlarmCapable(c.Protocol)) .Select(c => c.Name) diff --git a/tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/FlatteningPipelineNativeAlarmCapabilityTests.cs b/tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/FlatteningPipelineNativeAlarmCapabilityTests.cs index b0d6a4de..a49f35c3 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/FlatteningPipelineNativeAlarmCapabilityTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/FlatteningPipelineNativeAlarmCapabilityTests.cs @@ -83,6 +83,12 @@ public class FlatteningPipelineNativeAlarmCapabilityTests [Theory] [InlineData("OpcUa")] [InlineData("MxGateway")] + // Case variants: IsAlarmCapable uses OrdinalIgnoreCase, matching DataConnectionFactory's + // own OrdinalIgnoreCase protocol-key lookup; lock the contract with non-canonical casing. + [InlineData("OPCUA")] + [InlineData("opcua")] + [InlineData("mxgateway")] + [InlineData("MXGATEWAY")] public async Task FlattenAndValidate_NativeAlarmSourceOnAlarmCapableConnection_NoCapabilityError(string protocol) { Arrange(connectionName: "Boiler", connectionProtocol: protocol, boundConnectionName: "Boiler");