using Akka.Actor; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.ControlPlane.Coordinators; using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness; namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests; public sealed class ConfigPublishCoordinatorTests : ControlPlaneActorTestBase { private static readonly RevisionHash TestRevision = RevisionHash.Parse(new string('a', 64)); [Fact] public void EmptyCluster_dispatch_seals_immediately() { // With no driver-role cluster members in scope, the coordinator has nobody to wait for // and seals the deployment right after writing the AwaitingApplyAcks status. var dbFactory = NewInMemoryDbFactory(); var deploymentId = SeedDispatchingDeployment(dbFactory); var actor = Sys.ActorOf(ConfigPublishCoordinator.Props(dbFactory)); actor.Tell(new DispatchDeployment(deploymentId, TestRevision, CorrelationId.NewId())); AwaitAssert(() => { using var db = dbFactory.CreateDbContext(); var status = db.Deployments.Single().Status; status.ShouldBe(DeploymentStatus.Sealed); }, duration: TimeSpan.FromSeconds(3)); } [Fact] public void Stale_ApplyAck_after_seal_is_ignored() { var dbFactory = NewInMemoryDbFactory(); var deploymentId = SeedDispatchingDeployment(dbFactory); var actor = Sys.ActorOf(ConfigPublishCoordinator.Props(dbFactory)); actor.Tell(new DispatchDeployment(deploymentId, TestRevision, CorrelationId.NewId())); // Wait for seal. AwaitAssert(() => { using var db = dbFactory.CreateDbContext(); db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed); }, duration: TimeSpan.FromSeconds(3)); // Now send a late ApplyAck for the just-sealed deployment. Should be a no-op — neither // crash the actor nor modify the row. We give it a beat and re-check the status. actor.Tell(new ApplyAck(deploymentId, NodeId.Parse("ghost-node"), ApplyAckOutcome.Applied, null, CorrelationId.NewId())); ExpectNoMsg(TimeSpan.FromMilliseconds(250)); using var db = dbFactory.CreateDbContext(); db.Deployments.Single().Status.ShouldBe(DeploymentStatus.Sealed); } private static DeploymentId SeedDispatchingDeployment( Microsoft.EntityFrameworkCore.IDbContextFactory dbFactory) { var id = DeploymentId.NewId(); using var db = dbFactory.CreateDbContext(); db.Deployments.Add(new Configuration.Entities.Deployment { DeploymentId = id.Value, RevisionHash = TestRevision.Value, Status = DeploymentStatus.Dispatching, CreatedBy = "test", }); db.SaveChanges(); return id; } }