using Akka.Actor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using NSubstitute; using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure; using ZB.MOM.WW.ScadaBridge.Communication; using ZB.MOM.WW.ScadaBridge.Host.Actors; namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// /// PLAN-01 Task 20 (review 01 underdeveloped #2): with SBR down-if-alone + /// run-coordinated-shutdown-when-down live, a self-downed node terminates its /// ActorSystem out from under the still-running Host process. Unless the process /// then exits, the node idles forever — serving nothing, restarted by nobody. /// The recovery contract: unexpected ActorSystem termination ⇒ StopApplication() /// ⇒ the service supervisor restarts the process ⇒ it rejoins as a fresh /// incarnation. A termination that IS the host's own StopAsync must NOT trip it. /// public sealed class UnexpectedTerminationTests { private static int FreePort() { var l = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0); l.Start(); var p = ((System.Net.IPEndPoint)l.LocalEndpoint).Port; l.Stop(); return p; } private static AkkaHostedService CreateAkkaHostedService(IHostApplicationLifetime lifetime) { var port = FreePort(); var self = $"akka.tcp://scadabridge@127.0.0.1:{port}"; return new AkkaHostedService( new ServiceCollection().BuildServiceProvider(), Options.Create(new NodeOptions { Role = "Site", NodeHostname = "127.0.0.1", RemotingPort = port, SiteId = "site-a" }), Options.Create(new ClusterOptions { SeedNodes = new List { self }, AllowSingleNodeCluster = true, SplitBrainResolverStrategy = "keep-oldest", StableAfter = TimeSpan.FromSeconds(3), HeartbeatInterval = TimeSpan.FromMilliseconds(500), FailureDetectionThreshold = TimeSpan.FromSeconds(2), MinNrOfMembers = 1, DownIfAlone = true, }), Options.Create(new CommunicationOptions()), NullLogger.Instance, lifetime); } [Fact] public async Task ActorSystemTerminatedOutsideStopAsync_StopsHostApplication() { var lifetime = Substitute.For(); var service = CreateAkkaHostedService(lifetime); // Create the system (StartAsync registers actors; the system is created // by GetOrCreateActorSystem, which wires the WhenTerminated watchdog). service.GetOrCreateActorSystem(); var system = service.ActorSystem!; await system.Terminate(); // simulated self-down, NOT StopAsync await Task.Delay(500); lifetime.Received(1).StopApplication(); } [Fact] public async Task StopAsync_DoesNotTriggerStopApplication() { var lifetime = Substitute.For(); var service = CreateAkkaHostedService(lifetime); service.GetOrCreateActorSystem(); await service.StopAsync(CancellationToken.None); await Task.Delay(500); lifetime.DidNotReceive().StopApplication(); } }