using System.Text.Json;
using Akka.Actor;
using Serilog;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
///
/// Review I-2: under the factory Props a supervision restart of the scripted-alarm host builds a fresh, EMPTY
/// engine. Nothing re-sent ApplyScriptedAlarms, so until the next deploy the host had no mux interest
/// (inputs stopped) and its ownership filter dropped every operator ack/shelve as "not owned". The host now
/// tells its parent from PostRestart,
/// and re-sends the last plans it applied. This test never re-applies by hand.
///
public sealed class DriverHostActorScriptedAlarmRestartTests : RuntimeActorTestBase
{
private static readonly NodeId TestNode = NodeId.Parse("driver-alarmhost-restart-test");
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('d', 64));
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
private static DeploymentId SeedAlarmDeployment(Microsoft.EntityFrameworkCore.IDbContextFactory db)
{
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
{
ScriptedAlarms = new[]
{
new
{
ScriptedAlarmId = "alm-1", EquipmentId = "eq-1", Name = "HighTemp", AlarmType = "AlarmCondition",
Severity = 800, MessageTemplate = "hot", PredicateScriptId = "s-1",
HistorizeToAveva = false, Retain = true, Enabled = true,
},
},
Scripts = new[] { new { ScriptId = "s-1", SourceCode = "return (int)ctx.GetTag(\"M.T\").Value > 90;" } },
});
var id = DeploymentId.NewId();
using var ctx = db.CreateDbContext();
ctx.Deployments.Add(new Deployment
{
DeploymentId = id.Value, RevisionHash = RevA.Value, Status = DeploymentStatus.Sealed,
CreatedBy = "test", SealedAtUtc = DateTime.UtcNow, ArtifactBlob = artifact,
});
ctx.SaveChanges();
return id;
}
[Fact]
public void After_a_supervision_restart_the_alarm_host_is_re_applied_and_inputs_and_acks_work_again()
{
var db = NewInMemoryDbFactory();
var deploymentId = SeedAlarmDeployment(db);
var publish = this.CreateCleanOutcomePublishProbe();
var mux = CreateTestProbe();
var coordinator = CreateTestProbe();
var host = Sys.ActorOf(DriverHostActor.Props(
db, TestNode, coordinator.Ref,
localRoles: new HashSet { "driver" },
dependencyMux: mux.Ref,
opcUaPublishActor: publish.Ref,
virtualTagEvaluator: NullVirtualTagEvaluator.Instance,
virtualTagHostOverride: CreateTestProbe().Ref,
scriptRootLogger: new ScriptRootLogger(new LoggerConfiguration().CreateLogger()),
alarmStateStore: new InMemoryAlarmStateStore()));
V3ArtifactSeeder.Dispatch(host, coordinator, deploymentId, RevA);
var first = mux.FishForMessage(r => r.TagRefs.Contains("M.T"), Timeout);
var alarmHost = first.Subscriber;
// The crash: a batch item with a null TagId makes the upstream Push throw (the documented unguarded
// path), so supervision restarts the alarm host. NO manual re-apply follows.
alarmHost.Tell(new VirtualTagActor.DependencyValueBatch(new[]
{
new VirtualTagActor.DependencyValueChanged(null!, 1, DateTime.UtcNow),
}));
mux.FishForMessage(u => u.Subscriber.Equals(alarmHost), Timeout);
// The parent re-applies on the restart signal: interest is registered again by the new incarnation.
mux.FishForMessage(r => r.TagRefs.Contains("M.T"), Timeout);
// Inputs work: a dependency push activates the alarm.
alarmHost.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
publish.FishForMessage(
m => m.AlarmNodeId == "alm-1" && m.State.Active && !m.State.Acknowledged, Timeout);
// Acks work: the restarted engine owns alm-1 again, so the command is not filtered out as unowned.
alarmHost.Tell(new AlarmCommand(AlarmId: "alm-1", Operation: "Acknowledge", User: "alice", Comment: null, UnshelveAtUtc: null));
publish.FishForMessage(
m => m.AlarmNodeId == "alm-1" && m.State.Acknowledged, Timeout)
.State.Acknowledged.ShouldBeTrue();
}
}