using System.Reflection; namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// /// WP-16: Tests for CoordinatedShutdown configuration. /// Verifies no Environment.Exit calls exist in source and HOCON config is correct. /// public class CoordinatedShutdownTests { [Fact] public void HostSource_DoesNotContainEnvironmentExit() { var hostProjectDir = FindHostProjectDirectory(); Assert.NotNull(hostProjectDir); var sourceFiles = Directory.GetFiles(hostProjectDir, "*.cs", SearchOption.AllDirectories); Assert.NotEmpty(sourceFiles); foreach (var file in sourceFiles) { var content = File.ReadAllText(file); Assert.DoesNotContain("Environment.Exit", content, StringComparison.Ordinal); } } [Fact] public void AkkaHostedService_HoconConfig_IncludesCoordinatedShutdownSettings() { // Read the AkkaHostedService source to verify HOCON configuration var hostProjectDir = FindHostProjectDirectory(); Assert.NotNull(hostProjectDir); var akkaServiceFile = Path.Combine(hostProjectDir, "Actors", "AkkaHostedService.cs"); Assert.True(File.Exists(akkaServiceFile), $"AkkaHostedService.cs not found at {akkaServiceFile}"); var content = File.ReadAllText(akkaServiceFile); // Verify critical HOCON settings are present Assert.Contains("run-by-clr-shutdown-hook = on", content); Assert.Contains("run-coordinated-shutdown-when-down = on", content); } [Fact] public void AllCentralSingletons_RegisterThroughRegistrarWithDrain() { var hostProjectDir = FindHostProjectDirectory(); Assert.NotNull(hostProjectDir); var content = File.ReadAllText(Path.Combine(hostProjectDir!, "Actors", "AkkaHostedService.cs")); // Review 01 [Medium]: notification-outbox and audit-log-ingest were the // only central singletons WITHOUT a cluster-leave drain task. All seven // now go through SingletonRegistrar.Start, which always adds the // PhaseClusterLeave GracefulStop drain. Assert.Contains("SingletonRegistrar.Start(", content); foreach (var name in new[] { "notification-outbox", "audit-log-ingest", "site-call-audit", "audit-log-purge", "site-audit-reconciliation", "kpi-history-recorder", "pending-deployment-purge" }) { Assert.Contains($"\"{name}\"", content); } // No hand-rolled ClusterSingletonManager registrations remain anywhere: // the two role-scoped SITE singletons now also go through the registrar // (SingletonRegistrar.Start(..., role: siteRole)), so every singleton in // the file is created through the registrar with a drain task. Assert.Equal(0, CountOccurrences(content, "ClusterSingletonManager.Props(")); } [Fact] public void SiteSingletons_RegisterThroughRegistrarWithDrain() { var hostProjectDir = FindHostProjectDirectory(); Assert.NotNull(hostProjectDir); var content = File.ReadAllText(Path.Combine(hostProjectDir!, "Actors", "AkkaHostedService.cs")); // Round-2 N5: the two role-scoped SITE singletons previously stayed // hand-rolled (bare PoisonPill, no PhaseClusterLeave drain). They now // go through SingletonRegistrar.Start(..., role: siteRole), which // always adds the GracefulStop drain — in-flight SQLite writes // (static overrides, native_alarm_state) complete before handover. Assert.Contains("\"deployment-manager\"", content); Assert.Contains("\"event-log-handler\"", content); Assert.Contains("role: siteRole", content); Assert.Equal(0, CountOccurrences(content, "ClusterSingletonManager.Props(")); } private static int CountOccurrences(string haystack, string needle) { int count = 0, i = 0; while ((i = haystack.IndexOf(needle, i, StringComparison.Ordinal)) >= 0) { count++; i += needle.Length; } return count; } private static string? FindHostProjectDirectory() { var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; var dir = new DirectoryInfo(assemblyDir); while (dir != null) { var hostPath = Path.Combine(dir.FullName, "src", "ZB.MOM.WW.ScadaBridge.Host"); if (Directory.Exists(hostPath)) return hostPath; dir = dir.Parent; } return null; } }