feat(debugview): DV-1 native-binding linkage on AlarmStateChanged contract chain
Add two additive init-only fields to AlarmStateChanged so the Debug View can
nest live native conditions under their configured source-binding node:
- NativeSourceCanonicalName (binding canonical name, e.g. "Motor1.MotorAlarms")
- IsConfiguredPlaceholder (quiet-binding placeholder flag; default false)
Flow on BOTH cross-process paths:
- Live: proto AlarmStateUpdate fields 22/23 -> StreamRelayActor packs ->
SiteStreamGrpcClient unpacks (regenerated SiteStreamGrpc/Sitestream.cs).
- Snapshot (Newtonsoft): record defaults carry through; no special handling.
NativeAlarmActor.Emit now stamps NativeSourceCanonicalName = _source.CanonicalName.
Additive-only: no existing positional constructor or wire frame changed.
Tests: StreamRelayActorTests round-trips both fields pack->unpack;
NativeAlarmActorTests asserts the emitted event carries the binding canonical name.
This commit is contained in:
@@ -113,6 +113,52 @@ public class StreamRelayActorTests : TestKit
|
||||
Assert.Equal("90", alarm.LimitValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RelaysAlarmStateChanged_NativeBindingLinkage_SurvivesFullRoundTrip()
|
||||
{
|
||||
// DV-1: the native source-binding canonical name and the configured-placeholder
|
||||
// flag must pack into AlarmStateUpdate (StreamRelayActor) and unpack back out
|
||||
// (SiteStreamGrpcClient.ConvertToDomainEvent) — the full cross-process round trip.
|
||||
var channel = Channel.CreateUnbounded<SiteStreamEvent>();
|
||||
var actor = Sys.ActorOf(Props.Create(() =>
|
||||
new StreamRelayActor("corr-native-binding", channel.Writer)));
|
||||
|
||||
var timestamp = new DateTimeOffset(2026, 3, 21, 11, 0, 0, TimeSpan.Zero);
|
||||
var domainEvent = new AlarmStateChanged(
|
||||
"Site1.Motor01", "Motor1.MotorAlarms.Hi", AlarmState.Active, 900, timestamp)
|
||||
{
|
||||
Kind = AlarmKind.NativeOpcUa,
|
||||
SourceReference = "Motor1.MotorAlarms.Hi",
|
||||
NativeSourceCanonicalName = "Motor1.MotorAlarms",
|
||||
IsConfiguredPlaceholder = true,
|
||||
Condition = new AlarmConditionState(
|
||||
Active: true, Acknowledged: false, Confirmed: null,
|
||||
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: 900)
|
||||
};
|
||||
|
||||
actor.Tell(domainEvent);
|
||||
|
||||
var success = channel.Reader.TryRead(out var protoEvent);
|
||||
if (!success)
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
success = channel.Reader.TryRead(out protoEvent);
|
||||
}
|
||||
|
||||
Assert.True(success, "Expected a proto event on the channel");
|
||||
Assert.NotNull(protoEvent);
|
||||
|
||||
// Packed onto the wire frame by StreamRelayActor.
|
||||
Assert.Equal("Motor1.MotorAlarms", protoEvent.AlarmChanged.NativeSourceCanonicalName);
|
||||
Assert.True(protoEvent.AlarmChanged.IsConfiguredPlaceholder);
|
||||
|
||||
// Unpacked back into the domain record by SiteStreamGrpcClient.
|
||||
var roundTripped = Assert.IsType<AlarmStateChanged>(
|
||||
SiteStreamGrpcClient.ConvertToDomainEvent(protoEvent));
|
||||
Assert.Equal("Motor1.MotorAlarms", roundTripped.NativeSourceCanonicalName);
|
||||
Assert.True(roundTripped.IsConfiguredPlaceholder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetsCorrelationId_OnAllEvents()
|
||||
{
|
||||
|
||||
@@ -79,6 +79,27 @@ public class NativeAlarmActorTests : TestKit, IDisposable
|
||||
Assert.Equal(800, emitted.Condition.Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Raise_StampsNativeSourceCanonicalName_FromConfiguredBinding()
|
||||
{
|
||||
// DV-1: every emitted condition carries the canonical name of the source
|
||||
// BINDING it belongs to, so the DebugView can nest live native conditions
|
||||
// under their configured binding node. It must equal the binding's
|
||||
// CanonicalName used to construct the actor (Source().CanonicalName).
|
||||
var instance = CreateTestProbe();
|
||||
var dcl = CreateTestProbe();
|
||||
var actor = Spawn(instance.Ref, dcl.Ref);
|
||||
dcl.ExpectMsg<SubscribeAlarmsRequest>();
|
||||
|
||||
actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
|
||||
"T01.Hi", AlarmTransitionKind.Raise,
|
||||
new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800))));
|
||||
|
||||
var emitted = instance.ExpectMsg<AlarmStateChanged>();
|
||||
Assert.Equal(Source().CanonicalName, emitted.NativeSourceCanonicalName);
|
||||
Assert.Equal("Pressure", emitted.NativeSourceCanonicalName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SnapshotComplete_WithMissingPriorAlarm_EmitsReturnToNormal()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user