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);
}
}