using Akka.Actor; using Akka.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using ZB.MOM.WW.Health.Akka; namespace ZB.MOM.WW.Health.Akka.Tests; /// /// The states a node passes through on the way up, where the active tier must not report a booting /// node as a failed standby — now that the tier is a real 503, an Unhealthy here would pull the node /// out of an orchestrator's pool while it starts. /// public sealed class ActiveNodeStartupSafetyTests { [Fact] public async Task NoActorSystem_IsDegraded() { var check = new ActiveNodeHealthCheck(new ServiceCollection().BuildServiceProvider()); var result = await check.CheckHealthAsync(NewContext(check)); Assert.Equal(HealthStatus.Degraded, result.Status); // Description-only: an empty data dictionary is what makes the writer omit "data" entirely. Assert.Empty(result.Data); } [Fact] public async Task NoActorSystem_RoleScoped_IsDegraded() { var check = new ActiveNodeHealthCheck(new ServiceCollection().BuildServiceProvider(), "admin"); Assert.Equal(HealthStatus.Degraded, (await check.CheckHealthAsync(NewContext(check))).Status); } [Fact] public void Gate_NoActorSystem_IsNotActive() { Assert.False(new AkkaActiveNodeGate(new ServiceCollection().BuildServiceProvider()).IsActiveNode); } [Fact] public async Task ClusterNotConfigured_IsDegraded() { // ActorSystem present but Akka.Cluster not configured → Cluster.Get throws. The check must // return Degraded (the spec's startup-safety rule), not let the exception escape (→ Unhealthy). using var system = ActorSystem.Create("active-node-no-cluster"); try { var provider = new ServiceCollection().AddSingleton(system).BuildServiceProvider(); var check = new ActiveNodeHealthCheck(provider); Assert.Equal(HealthStatus.Degraded, (await check.CheckHealthAsync(NewContext(check))).Status); } finally { await system.Terminate(); } } [Fact] public async Task Gate_ClusterNotConfigured_IsNotActive() { using var system = ActorSystem.Create("active-node-gate-no-cluster"); try { var provider = new ServiceCollection().AddSingleton(system).BuildServiceProvider(); Assert.False(new AkkaActiveNodeGate(provider).IsActiveNode); } finally { await system.Terminate(); } } [Fact] public async Task ClusteredButNeverJoined_IsUnhealthy_NobodyIsActive() { // A cold start: the cluster provider is up but no member has reached Up. Nobody is active, so // nobody may claim to be — this is the one case that must NOT be Degraded, because a cluster // that never forms would otherwise sit at 200 forever. var config = ConfigurationFactory.ParseString(""" akka { loglevel = "WARNING" actor.provider = cluster remote.dot-netty.tcp { hostname = "127.0.0.1", port = 0 } cluster.seed-nodes = [] } """); var system = ActorSystem.Create("active-node-never-joined", config); try { var provider = new ServiceCollection().AddSingleton(system).BuildServiceProvider(); var check = new ActiveNodeHealthCheck(provider); var result = await check.CheckHealthAsync(NewContext(check)); Assert.Equal(HealthStatus.Unhealthy, result.Status); Assert.Contains("No Up member", result.Description); Assert.False(new AkkaActiveNodeGate(provider).IsActiveNode); } finally { await system.Terminate(); } } private static HealthCheckContext NewContext(IHealthCheck check) => new() { Registration = new HealthCheckRegistration("active-node", check, HealthStatus.Unhealthy, tags: null), }; }