using Serilog; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Scripting; namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests; /// /// lmxopcua#779 — an engine built without an explicit scriptTimeout (which is how the host built every /// production engine) ran its predicates on the 250 ms Phase-7 default instead of the 2 s budget virtual tags /// and Calculation tags get. The default is now . /// [Trait("Category", "Unit")] public sealed class ScriptedAlarmEngineBudgetTests { /// A predicate that busy-waits ~600 ms and then fires: over the old 250 ms budget, well inside 2 s. private const string SixHundredMsPredicate = """var t = ctx.GetTag("Temp"); var end = System.DateTime.UtcNow.AddMilliseconds(600); while (System.DateTime.UtcNow < end) { } return true;"""; private static ScriptedAlarmEngine Build(FakeUpstream up, ScriptingOptions? scripting = null) { var logger = new LoggerConfiguration().CreateLogger(); return new ScriptedAlarmEngine(up, new InMemoryAlarmStateStore(), new ScriptLoggerFactory(logger), logger, scripting: scripting); } [Fact] public void Without_an_explicit_timeout_the_budget_is_the_Scripting_RunTimeout_default_not_250ms() { using var eng = Build(new FakeUpstream()); eng.ScriptTimeout.ShouldBe(ScriptingOptions.DefaultRunTimeout); eng.ScriptTimeout.ShouldBe(TimeSpan.FromSeconds(2)); } [Fact] public void A_configured_RunTimeout_is_the_budget() { using var eng = Build(new FakeUpstream(), new ScriptingOptions { RunTimeout = TimeSpan.FromSeconds(7) }); eng.ScriptTimeout.ShouldBe(TimeSpan.FromSeconds(7)); } [Fact] public async Task A_predicate_slower_than_250ms_but_inside_the_budget_completes_and_activates() { var up = new FakeUpstream(); up.Set("Temp", 50); using var eng = Build(up); await eng.LoadAsync( [new ScriptedAlarmDefinition(AlarmId: "slow", EquipmentPath: "Plant/Line1/Reactor", AlarmName: "slow", Kind: AlarmKind.AlarmCondition, Severity: AlarmSeverity.High, MessageTemplate: "m", PredicateScriptSource: SixHundredMsPredicate)], TestContext.Current.CancellationToken); // LoadAsync evaluates once (startup recovery). Under the old 250 ms default that run timed out and the // condition stayed Inactive with a Bad annotation. eng.RunGuard.StartedRunCount.ShouldBe(1); eng.GetState("slow")!.Active.ShouldBe(AlarmActiveState.Active); } }