diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs
index 0f69625b..c3a48688 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs
@@ -84,7 +84,11 @@ public static class HealthEndpoints
"akka",
failureStatus: null,
tags: new[] { ZbHealthTags.Ready, ZbHealthTags.Active },
- args: AkkaClusterStatusPolicy.OtOpcUaCompat)
+ // Shared SPEC Default policy (lmxopcua#771). OtOpcUaCompat was a migration shim that kept the
+ // pre-adoption two-way behaviour (anything not Up = Degraded), which can never fail /health/ready
+ // — Degraded passes. Nothing relies on that: Down/Removed/Joining-failed now answer 503 on the
+ // ready tier, Leaving/Exiting stay Degraded (a draining node keeps serving).
+ args: AkkaClusterStatusPolicy.Default)
// "Is this node in charge of its mesh?" — Healthy on the one node that owns the active
// work for its Cluster, Unhealthy (503) on its partner, so Traefik pins the AdminUI to the
// node actually hosting the cluster singletons.
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Health/ActiveTierRegistrationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Health/ActiveTierRegistrationTests.cs
index 45ff262b..bca0ea88 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Health/ActiveTierRegistrationTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests/Health/ActiveTierRegistrationTests.cs
@@ -1,3 +1,6 @@
+using Akka.Actor;
+using Akka.Configuration;
+using Akka.TestKit.Xunit2;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
@@ -92,3 +95,31 @@ public sealed class ActiveTierRegistrationTests
ClusterPrimaryRegistration(hasAdmin: true).Tags.ShouldNotContain(ZbHealthTags.Ready);
}
}
+
+/// lmxopcua#771 — the akka check must use the shared SPEC Default policy so a node that is not a live
+/// cluster member makes /health/ready fail (the retired OtOpcUaCompat policy could only say Degraded).
+public sealed class AkkaReadyPolicyTests : TestKit
+{
+ public AkkaReadyPolicyTests()
+ : base(ConfigurationFactory.ParseString(
+ "akka.actor.provider = cluster\nakka.remote.dot-netty.tcp.port = 0\nakka.remote.dot-netty.tcp.hostname = 127.0.0.1"))
+ {
+ }
+
+ [Fact]
+ public async Task Akka_check_is_Unhealthy_when_self_has_not_joined_a_cluster()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton(Sys);
+ services.AddOtOpcUaHealth(hasAdmin: true);
+ var sp = services.BuildServiceProvider();
+ var registration = sp.GetRequiredService>()
+ .Value.Registrations.Single(r => r.Name == "akka");
+ registration.Tags.ShouldContain(ZbHealthTags.Ready);
+
+ var result = await registration.Factory(sp).CheckHealthAsync(
+ new HealthCheckContext { Registration = registration }, CancellationToken.None);
+
+ result.Status.ShouldBe(HealthStatus.Unhealthy);
+ }
+}