diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs index d07eca49..1b36df80 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs @@ -161,6 +161,18 @@ public class SandboxAlarmsAccessor "Alarms.CurrentAsync() reads the site Instance Actor's live alarm conditions, " + "which aren't available in the central Test Run sandbox — deploy to a site to " + "exercise alarm-reading scripts."); + + /// + /// Mirrors AlarmsAccessor.MesReportableAsync for compile parity; unsupported at + /// run time in the central Test Run sandbox. + /// + /// Token used to cancel the read (unused in the sandbox). + /// Never returns; always throws in the sandbox. + public Task> MesReportableAsync(CancellationToken cancellationToken = default) + => throw new ScriptSandboxException( + "Alarms.MesReportableAsync() reads the site Instance Actor's live alarm conditions, " + + "which aren't available in the central Test Run sandbox — deploy to a site to " + + "exercise alarm-reading scripts."); } /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs index a637e652..7fa17c31 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs @@ -55,6 +55,10 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts; /// True for a placeholder row standing in for a CONFIGURED native source binding that /// currently holds no conditions. Scripts enumerating real alarms should filter these out. /// +/// +/// True when the deployed configuration flags the alarm as MES-reportable (template +/// checkbox or instance override). Always false for native conditions. +/// public sealed record ScriptAlarm( string Name, string SourceReference, @@ -76,4 +80,5 @@ public sealed record ScriptAlarm( DateTimeOffset? AckTime, string CurrentValue, string LimitValue, - bool IsConfiguredPlaceholder); + bool IsConfiguredPlaceholder, + bool IsMesReportable = false); diff --git a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs index 128e0d88..60e11667 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs @@ -154,6 +154,12 @@ public sealed class ScriptCompileSurface /// The instance's current alarm conditions. public Task> CurrentAsync(CancellationToken cancellationToken = default) => throw new NotSupportedException(CompileOnly); + + /// Mirrors AlarmsAccessor.MesReportableAsync. + /// Token used to cancel the read. + /// The MES-reportable subset of the instance's current alarm conditions. + public Task> MesReportableAsync(CancellationToken cancellationToken = default) + => throw new NotSupportedException(CompileOnly); } /// Compile-only mirror of ScriptRuntimeContext.ExternalSystemHelper. diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs index 7af567ab..884527ea 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs @@ -398,6 +398,18 @@ public class AlarmsAccessor /// The instance's current alarm conditions; empty when it holds none. public Task> CurrentAsync(CancellationToken cancellationToken = default) => _ctx.GetAlarmsAsync(cancellationToken); + + /// + /// restricted to alarms whose deployed configuration flags them + /// MES-reportable (the template checkbox, or an instance override). Only computed alarms + /// can carry the flag — the Instance Actor stamps it from the deployed config — so native + /// conditions and their placeholder rows are never included. Callers still filter on + /// Active to get only triggered alarms. + /// + /// Cancels the wait for the actor's reply. + /// The MES-reportable subset of the instance's current alarm conditions. + public Task> MesReportableAsync(CancellationToken cancellationToken = default) + => _ctx.GetMesReportableAlarmsAsync(cancellationToken); } internal static class ScopeAccessorFactory diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs index 1e2c5128..8934040c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs @@ -1272,6 +1272,12 @@ public class ScriptRuntimeContext return response.Alarms.Select(ToScriptAlarm).ToList(); } + /// restricted to rows flagged MES-reportable. + /// Cancels the wait for the actor's reply. + /// The MES-reportable subset of the instance's current alarm conditions. + internal async Task> GetMesReportableAlarmsAsync(CancellationToken cancellationToken = default) + => (await GetAlarmsAsync(cancellationToken)).Where(a => a.IsMesReportable).ToList(); + /// /// Projects a retained onto the flat, script-facing /// shape. Condition is the authority for the @@ -1301,7 +1307,8 @@ public class ScriptRuntimeContext AckTime: a.AckTime, CurrentValue: a.CurrentValue, LimitValue: a.LimitValue, - IsConfiguredPlaceholder: a.IsConfiguredPlaceholder); + IsConfiguredPlaceholder: a.IsConfiguredPlaceholder, + IsMesReportable: a.IsMesReportable); /// /// Site-local tracking-status API for cached operations. diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ScriptAnalysis/ScriptAnalysisServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ScriptAnalysis/ScriptAnalysisServiceTests.cs index 74a34efc..b70cbafa 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ScriptAnalysis/ScriptAnalysisServiceTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ScriptAnalysis/ScriptAnalysisServiceTests.cs @@ -819,7 +819,9 @@ public class ScriptAnalysisServiceTests " AckDT = a.AckTime,\n" + " AckComment = a.OperatorComment })\n" + " .ToList();\n" + - "return new { WasSuccessful = true, Alarms = rows };"; + "var r = await Alarms.MesReportableAsync();\n" + + "var f = r.Where(a => a.IsMesReportable).ToList();\n" + + "return new { WasSuccessful = true, Alarms = rows, Flagged = f.Count };"; var resp = _svc.Diagnose(new DiagnoseRequest(code)); Assert.DoesNotContain(resp.Markers, m => m.Code.StartsWith("CS")); diff --git a/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/RoslynScriptCompilerTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/RoslynScriptCompilerTests.cs index ff4deb26..b3ad0b33 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/RoslynScriptCompilerTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/RoslynScriptCompilerTests.cs @@ -56,6 +56,8 @@ public class RoslynScriptCompilerTests var scope = Scope; var currentAlarms = await Alarms.CurrentAsync(); var viaInstance = await Instance.Alarms.CurrentAsync(); + var mesReportable = await Alarms.MesReportableAsync(); + var flaggedAlarms = mesReportable.Where(a => a.IsMesReportable).ToList(); """; var diagnostics = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface)); @@ -101,7 +103,12 @@ public class RoslynScriptCompilerTests AckComment = a.OperatorComment, }).ToList(); - return new { WasSuccessful = true, ErrorText = (string)null, Alarms = infos }; + // The pre-filtered accessor + the configured flag are the supported shape for + // "which alarms does MES care about?" — both must bind at design time. + var r = await Alarms.MesReportableAsync(); + var f = r.Where(a => a.IsMesReportable).ToList(); + + return new { WasSuccessful = true, ErrorText = (string)null, Alarms = infos, Flagged = f.Count }; """; Assert.Empty(RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface))); diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/AlarmsAccessorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/AlarmsAccessorTests.cs index eb35797c..6ee41acf 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/AlarmsAccessorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/AlarmsAccessorTests.cs @@ -187,6 +187,43 @@ public class AlarmsAccessorTests : TestKit, IDisposable Assert.Equal("Z28061.HeartbeatTimeoutAlarm", real.Name); } + [Fact] + public async Task CurrentAsync_ProjectsIsMesReportable() + { + // The deployed configuration's MES-reportable flag rides the snapshot row and must + // reach the script projection verbatim — a row without it reads false. + var probe = CreateTestProbe(); + var ctx = MakeContext(probe.Ref); + + var pending = ctx.Alarms.CurrentAsync(); + RespondWith(probe, + NativeAcked() with { IsMesReportable = true }, + new AlarmStateChanged("CvdReactor01", "HighTemp", AlarmState.Active, 700, ObservedAt)); + + var alarms = await pending; + Assert.Equal(2, alarms.Count); + Assert.True(Assert.Single(alarms, a => a.Name == "Z28061.HeartbeatTimeoutAlarm").IsMesReportable); + Assert.False(Assert.Single(alarms, a => a.Name == "HighTemp").IsMesReportable); + } + + [Fact] + public async Task MesReportableAsync_FiltersToFlaggedRows() + { + // The convenience accessor is CurrentAsync restricted to the flagged subset, so a + // script does not have to repeat the filter (and cannot get it subtly wrong). + var probe = CreateTestProbe(); + var ctx = MakeContext(probe.Ref); + + var pending = ctx.Alarms.MesReportableAsync(); + RespondWith(probe, + NativeAcked() with { IsMesReportable = true }, + NativeAcked() with { AlarmName = "Z28061.UnflaggedAlarm", SourceReference = "Z28061.UnflaggedAlarm" }); + + var alarm = Assert.Single(await pending); + Assert.Equal("Z28061.HeartbeatTimeoutAlarm", alarm.Name); + Assert.True(alarm.IsMesReportable); + } + [Fact] public async Task CurrentAsync_IsNotScopePrefixed_EveryScopeSeesTheSameList() {