server(alarms): honor ExcludeAttributes GR-only contract; warn on empty config-only watch-list
This commit is contained in:
@@ -87,12 +87,19 @@ public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
|
||||
ordered.Add((include, DeriveSourceObject(include)));
|
||||
}
|
||||
|
||||
// Remove excluded references (case-insensitive). Excludes are rare, so a
|
||||
// per-entry lookup against a small set is fine.
|
||||
HashSet<string> excluded = new(discovery.ExcludeAttributes, StringComparer.OrdinalIgnoreCase);
|
||||
if (excluded.Count > 0)
|
||||
// Remove excluded references (case-insensitive), but only when GR discovery
|
||||
// is active. ExcludeAttributes is documented as "Ignored when
|
||||
// UseGalaxyRepository is false" (AlarmDiscoveryOptions.ExcludeAttributes).
|
||||
// Whitespace-only entries are skipped, consistent with the include guard above.
|
||||
if (discovery.UseGalaxyRepository)
|
||||
{
|
||||
ordered.RemoveAll(e => excluded.Contains(e.Reference));
|
||||
HashSet<string> excluded = new(
|
||||
discovery.ExcludeAttributes.Where(e => !string.IsNullOrWhiteSpace(e)),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
if (excluded.Count > 0)
|
||||
{
|
||||
ordered.RemoveAll(e => excluded.Contains(e.Reference));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Resolve subtag names with safe fallbacks.
|
||||
@@ -123,13 +130,16 @@ public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Report the resolved count; warn if subtag mode would cover nothing.
|
||||
if (targets.Count == 0 && discovery.UseGalaxyRepository)
|
||||
// 5. Report the resolved count; warn when subtag mode was expected to cover
|
||||
// something (GR enabled, or explicit includes were configured) but resolved
|
||||
// to nothing. Only emit the Debug line when there is at least one target,
|
||||
// to avoid a confusing "0 target(s)" noise line.
|
||||
if (targets.Count == 0 && (discovery.UseGalaxyRepository || discovery.IncludeAttributes.Length > 0))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Alarm subtag watch-list resolved to zero targets; subtag-polling fallback will cover no alarms.");
|
||||
}
|
||||
else
|
||||
else if (targets.Count > 0)
|
||||
{
|
||||
_logger.LogDebug("Resolved alarm subtag watch-list with {TargetCount} target(s).", targets.Count);
|
||||
}
|
||||
|
||||
@@ -208,6 +208,76 @@ public sealed class AlarmWatchListResolverTests
|
||||
Assert.Equal("StandaloneTag", result[1].SourceObjectReference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix 1: ExcludeAttributes must be ignored when UseGalaxyRepository is false.
|
||||
/// A config-only include must survive even when the same path appears in ExcludeAttributes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ExcludeIgnored_WhenGalaxyRepositoryDisabled()
|
||||
{
|
||||
// Repo is never consulted; only IncludeAttributes matters.
|
||||
StubGalaxyRepository repo = new([]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||
useGalaxyRepository: false,
|
||||
include: ["Tank01.Level.HiHi"],
|
||||
exclude: ["Tank01.Level.HiHi"]));
|
||||
|
||||
// ExcludeAttributes is ignored when GR is off — the include must be present.
|
||||
AlarmSubtagTarget target = Assert.Single(result);
|
||||
Assert.Equal("Galaxy!Tank01.Level.HiHi", target.AlarmFullReference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix 1 (GR-on path): ExcludeAttributes still prunes GR rows when
|
||||
/// UseGalaxyRepository is true.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ExcludeApplied_WhenGalaxyRepositoryEnabled()
|
||||
{
|
||||
StubGalaxyRepository repo = new(
|
||||
[
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank02.Level.HiHi", SourceObjectReference = "Tank02" },
|
||||
]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||
useGalaxyRepository: true,
|
||||
exclude: ["Tank02.Level.HiHi"]));
|
||||
|
||||
// Tank02 was excluded; only Tank01 remains.
|
||||
AlarmSubtagTarget target = Assert.Single(result);
|
||||
Assert.Contains("Tank01", target.ActiveSubtag, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix 1: A whitespace-only ExcludeAttributes entry must be skipped and must
|
||||
/// not accidentally exclude any real reference.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ResolveAsync_WhitespaceOnlyExcludeEntry_IsSkipped()
|
||||
{
|
||||
StubGalaxyRepository repo = new(
|
||||
[
|
||||
new GalaxyAlarmAttributeRow { FullTagReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01" },
|
||||
]);
|
||||
|
||||
AlarmWatchListResolver resolver = CreateResolver(repo);
|
||||
|
||||
// The exclude array contains a whitespace-only string — should be a no-op.
|
||||
IReadOnlyList<AlarmSubtagTarget> result = await resolver.ResolveAsync(Options(
|
||||
useGalaxyRepository: true,
|
||||
exclude: [" "]));
|
||||
|
||||
// Tank01 must not have been wrongly excluded.
|
||||
AlarmSubtagTarget target = Assert.Single(result);
|
||||
Assert.Contains("Tank01", target.ActiveSubtag, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>In-memory <see cref="IGalaxyRepository"/> returning a fixed alarm rowset.</summary>
|
||||
private sealed class StubGalaxyRepository(List<GalaxyAlarmAttributeRow> rows) : IGalaxyRepository
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user