worker(alarms): exact-match ack resolution (no substring false-match) + ack-by-guid tests

This commit is contained in:
Joseph Doherty
2026-06-13 09:42:00 -04:00
parent 4bd757a136
commit fd64b9260c
2 changed files with 138 additions and 19 deletions
@@ -147,9 +147,9 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
/// subtag; the operator-identity arguments are not surfaced through the
/// subtag write.
/// </summary>
/// <param name="alarmName">The alarm name (object-rooted tag name or any reference fragment).</param>
/// <param name="providerName">The provider name (unused for matching).</param>
/// <param name="groupName">The group name (unused for matching).</param>
/// <param name="alarmName">The alarm name (object-rooted tag name as the dispatcher derives it).</param>
/// <param name="providerName">The provider name used to recompose the full reference for lookup.</param>
/// <param name="groupName">The group name used to recompose the full reference for lookup.</param>
/// <param name="ackComment">The acknowledgment comment.</param>
/// <param name="ackOperatorName">The operator name (unused in subtag mode).</param>
/// <param name="ackOperatorNode">The operator node (unused in subtag mode).</param>
@@ -171,7 +171,7 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
throw new ObjectDisposedException(nameof(SubtagAlarmConsumer));
}
AlarmSubtagTarget? target = ResolveTargetByName(alarmName);
AlarmSubtagTarget? target = ResolveTargetByName(alarmName, providerName, groupName);
if (target is null)
{
return -1;
@@ -278,27 +278,21 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
record.AlarmGuid = SyntheticAlarmGuid.ForReference(reference);
}
private AlarmSubtagTarget? ResolveTargetByName(string? alarmName)
private AlarmSubtagTarget? ResolveTargetByName(string? alarmName, string? providerName, string? groupName)
{
if (string.IsNullOrEmpty(alarmName))
{
return null;
}
// Match a target whose full reference contains the supplied name. The
// dispatcher derives (name, provider, group) from the composed
// reference, so the object-rooted tag name is always a substring of the
// target's AlarmFullReference.
foreach (AlarmSubtagTarget target in targetsByReference.Values)
{
string reference = target.AlarmFullReference ?? string.Empty;
if (reference.IndexOf(alarmName!, StringComparison.OrdinalIgnoreCase) >= 0)
{
return target;
}
}
return null;
// Recompose the full reference from the same (provider, group, name)
// tuple the dispatcher derived, then do an exact OrdinalIgnoreCase
// lookup in targetsByReference. This avoids the false-positive that a
// substring scan would produce when one alarm name is a prefix of
// another (e.g. "Level.Hi" matching "Level.HiHi").
string composed = AlarmRecordTransitionMapper.ComposeFullReference(providerName, groupName, alarmName);
targetsByReference.TryGetValue(composed, out AlarmSubtagTarget? target);
return target;
}
private int WriteAckComment(AlarmSubtagTarget target, string ackComment)