feat(debugview): DV-2 emit placeholder rows for quiet native alarm bindings

InstanceActor.BuildAlarmStatesSnapshot now adds an IsConfiguredPlaceholder
row per configured native source binding that currently has no live
condition, so the Debug View tree can show the binding node even when
quiet. A binding is "quiet" when no retained AlarmStateChanged carries its
NativeSourceCanonicalName (DV-1).

Kind derivation: reuses the exact nativeKind value already computed via
ResolveNativeKind(nativeSource.ConnectionName) at the NativeAlarmActor
creation site and stored in a new _nativeAlarmKinds dictionary -- the
accurate per-binding kind (NativeOpcUa vs NativeMxAccess), not the
NativeOpcUa default.

Tests: Snapshot_QuietNativeBinding_EmitsPlaceholder,
Snapshot_NativeBindingWithLiveCondition_NoPlaceholder.
This commit is contained in:
Joseph Doherty
2026-06-17 15:00:20 -04:00
parent 899ad6e106
commit 5d07ac24cb
2 changed files with 81 additions and 0 deletions
@@ -95,6 +95,56 @@ public class InstanceActorNativeAlarmTests : TestKit, IDisposable
a.SourceReference == "T01.Hi" && a.Kind == AlarmKind.NativeOpcUa && a.Condition.Severity == 800);
}
[Fact]
public void Snapshot_QuietNativeBinding_EmitsPlaceholder()
{
var dcl = CreateTestProbe();
var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);
// No live condition is emitted: the configured "Pressure" binding is quiet.
actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
var snap = ExpectMsg<DebugViewSnapshot>();
var placeholders = snap.AlarmStates
.Where(a => a.NativeSourceCanonicalName == "Pressure" && a.IsConfiguredPlaceholder)
.ToList();
Assert.Single(placeholders);
var placeholder = placeholders[0];
Assert.Equal(AlarmState.Normal, placeholder.State);
Assert.Equal("Pressure", placeholder.NativeSourceCanonicalName);
Assert.Equal(AlarmKind.NativeOpcUa, placeholder.Kind);
}
[Fact]
public void Snapshot_NativeBindingWithLiveCondition_NoPlaceholder()
{
var dcl = CreateTestProbe();
var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst"), dcl.Ref);
// The NativeAlarmActor emits a live condition stamped with the binding's
// canonical name (DV-1: NativeSourceCanonicalName), so the binding is "active".
actor.Tell(new AlarmStateChanged("inst", "Pressure.Hi", AlarmState.Active, 800, DateTimeOffset.UtcNow)
{
Kind = AlarmKind.NativeOpcUa,
SourceReference = "ns=2;s=T01.Hi",
NativeSourceCanonicalName = "Pressure",
Condition = new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800)
});
actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
var snap = ExpectMsg<DebugViewSnapshot>();
// The live condition is present...
Assert.Contains(snap.AlarmStates, a =>
a.SourceReference == "ns=2;s=T01.Hi" && a.NativeSourceCanonicalName == "Pressure"
&& a.Kind == AlarmKind.NativeOpcUa && a.Condition.Severity == 800);
// ...and NO placeholder row is emitted for that binding.
Assert.DoesNotContain(snap.AlarmStates, a =>
a.NativeSourceCanonicalName == "Pressure" && a.IsConfiguredPlaceholder);
}
void IDisposable.Dispose()
{
Shutdown();