feat(streaming): carry IsMesReportable on AlarmStateUpdate (25) and DebugAlarmStateDto (20) (ScadaBridge#60)
Central's Debug View live stream and Alarm Summary cache always saw false because neither wire message had the field. Additive proto fields, vendored C# regenerated with docker/regen-proto.sh (sitestream + sitecommand), mapped both ways in StreamRelayActor/SiteStreamGrpcClient and SiteCommandDtoMapper. The live stream publishes Alarm Actor events, which never carried the flag, so InstanceActor.HandleAlarmStateChanged now stamps it before retaining and publishing. A pre-#60 site sends the proto3 default. No UI badge yet. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -209,6 +209,40 @@ public class StreamRelayActorTests : TestKit
|
||||
SiteStreamGrpcClient.ConvertToDomainEvent(unackedProto)).AckTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the MES-reportable flag survives the relay → proto → central decode
|
||||
/// round trip (Gitea #60, <c>AlarmStateUpdate</c> field 25), and that an unflagged alarm
|
||||
/// arrives as false.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RelaysAlarmStateChanged_IsMesReportable_SurvivesFullRoundTrip()
|
||||
{
|
||||
var channel = Channel.CreateUnbounded<SiteStreamEvent>();
|
||||
var actor = Sys.ActorOf(Props.Create(() =>
|
||||
new StreamRelayActor("corr-mes", channel.Writer)));
|
||||
|
||||
var flagged = new AlarmStateChanged(
|
||||
"Site1.Oven1", "HighTemp", AlarmState.Active, 700,
|
||||
new DateTimeOffset(2026, 11, 9, 12, 0, 0, TimeSpan.Zero))
|
||||
{
|
||||
IsMesReportable = true
|
||||
};
|
||||
|
||||
actor.Tell(flagged);
|
||||
var flaggedProto = ReadProtoEvent(channel);
|
||||
Assert.True(flaggedProto.AlarmChanged.IsMesReportable);
|
||||
Assert.True(
|
||||
Assert.IsType<AlarmStateChanged>(
|
||||
SiteStreamGrpcClient.ConvertToDomainEvent(flaggedProto)).IsMesReportable);
|
||||
|
||||
actor.Tell(flagged with { IsMesReportable = false });
|
||||
var plainProto = ReadProtoEvent(channel);
|
||||
Assert.False(plainProto.AlarmChanged.IsMesReportable);
|
||||
Assert.False(
|
||||
Assert.IsType<AlarmStateChanged>(
|
||||
SiteStreamGrpcClient.ConvertToDomainEvent(plainProto)).IsMesReportable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the next relayed proto event, retrying once after a short pause because the
|
||||
/// relay actor writes to the channel asynchronously (mirrors the existing round-trip
|
||||
|
||||
@@ -275,12 +275,16 @@ internal static class SiteCommandSamples
|
||||
private static AttributeValueChanged AttrValueList() =>
|
||||
new("Site1.Pump1", "Pump1.Trend", "Trend", new List<object?> { 1, 2.5d, "x", null }, "Good", T1);
|
||||
|
||||
/// <summary>A computed alarm: <c>Condition</c> left implicit, which is the common case.</summary>
|
||||
/// <summary>
|
||||
/// A computed alarm: <c>Condition</c> left implicit, which is the common case. Flagged
|
||||
/// MES-reportable so the golden pins <c>DebugAlarmStateDto</c> field 20 (Gitea #60).
|
||||
/// </summary>
|
||||
private static AlarmStateChanged ComputedAlarm() =>
|
||||
new("Site1.Pump1", "HighSpeed", AlarmState.Active, 700, T1)
|
||||
{
|
||||
Level = AlarmLevel.HighHigh,
|
||||
Message = "Speed critically high"
|
||||
Message = "Speed critically high",
|
||||
IsMesReportable = true
|
||||
};
|
||||
|
||||
/// <summary>A mirrored native alarm: every native enrichment field populated, <c>Condition</c> explicit.</summary>
|
||||
|
||||
+40
-3
@@ -8,16 +8,18 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming;
|
||||
using ZB.MOM.WW.ScadaBridge.TestSupport;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
||||
|
||||
/// <summary>
|
||||
/// The deployed alarm's <c>IsMesReportable</c> flag must reach the site-local alarm
|
||||
/// snapshot that <c>Alarms.CurrentAsync()</c> and the Debug View query both build (the
|
||||
/// flag does not cross <c>DebugAlarmStateDto</c> to central in this slice). It is stamped in ONE
|
||||
/// place — <c>BuildAlarmStatesSnapshot</c> — so a computed alarm that has fired (served
|
||||
/// snapshot that <c>Alarms.CurrentAsync()</c> and the Debug View query both build, and the
|
||||
/// events published to the site stream (Gitea #60). The snapshot projection stamps every
|
||||
/// row in <c>BuildAlarmStatesSnapshot</c>, so a computed alarm that has fired (served
|
||||
/// from the retained <c>AlarmStateChanged</c>) and one that never has (served from the
|
||||
/// bare Normal fallback) can never disagree about the flag.
|
||||
/// </summary>
|
||||
@@ -154,6 +156,41 @@ public class InstanceActorMesReportableSnapshotTests : TestKit, IDisposable
|
||||
Assert.False(Assert.Single(snapshot.Alarms, a => a.AlarmName == "TempSpike").IsMesReportable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the event the Instance Actor publishes to the site stream — the one the
|
||||
/// Debug View live feed and central's Alarm Summary cache consume — carries the deployed
|
||||
/// MES-reportable flag, not only the snapshot (Gitea #60).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PublishedAlarmEvent_CarriesIsMesReportable()
|
||||
{
|
||||
const string instance = "Oven3";
|
||||
var streamManager = new SiteStreamManager(_options, NullLogger<SiteStreamManager>.Instance);
|
||||
streamManager.Initialize(Sys);
|
||||
var subscriber = CreateTestProbe();
|
||||
streamManager.Subscribe(instance, subscriber.Ref);
|
||||
|
||||
var actor = ActorOf(Props.Create(() => new InstanceActor(
|
||||
instance,
|
||||
JsonSerializer.Serialize(TwoComputedAlarmsConfig(instance)),
|
||||
_storage,
|
||||
_compilationService,
|
||||
_sharedScriptLibrary,
|
||||
streamManager,
|
||||
_options,
|
||||
NullLogger<InstanceActor>.Instance,
|
||||
null)));
|
||||
|
||||
actor.Tell(new SetStaticAttributeCommand(
|
||||
"corr-temp", instance, "Temperature", "90", DateTimeOffset.UtcNow));
|
||||
Assert.True(ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(5)).Success);
|
||||
|
||||
var published = subscriber.FishForMessage<AlarmStateChanged>(
|
||||
m => m.AlarmName == "HighTemp" && m.State == AlarmState.Active,
|
||||
TimeSpan.FromSeconds(5));
|
||||
Assert.True(published.IsMesReportable);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user