using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Host.Health; namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Health; /// /// Pins the rule the active tier now uses to decide which node is in charge, and the startup path /// that must not report a booting node as a failed standby. /// /// /// The cluster-membership half of the rule — oldest Up member, never RoleLeader — is pinned /// against a real two-node cluster in RedundancyPrimaryElectionTests, including parity with /// the redundancy snapshot's own election. What is left to cover here is the role-selection rule and /// the startup guard. /// public sealed class ClusterPrimaryHealthCheckTests { [Fact] public void Fused_node_answers_for_the_admin_role() { // Admin wins on a node carrying both, because the cluster singletons and the AdminUI that // Traefik routes to are pinned to that role. Answering for 'driver' here could name a // different node the moment an admin-only or driver-only member joins the mesh. ClusterPrimaryHealthCheck.ActiveRoleFor(["admin", "driver", "cluster-MAIN"]) .ShouldBe("admin"); } [Fact] public void Driver_only_node_answers_for_the_driver_role() { // The regression this whole change exists for. Under the previous admin-scoped check every // site node returned Healthy — "or not a role member" — so all four called themselves // active and no consumer could find the Primary of a site Cluster. lmxopcua#494. ClusterPrimaryHealthCheck.ActiveRoleFor(["driver", "cluster-SITE-A"]) .ShouldBe("driver"); } [Fact] public void Admin_only_node_answers_for_the_admin_role() { ClusterPrimaryHealthCheck.ActiveRoleFor(["admin"]).ShouldBe("admin"); } [Fact] public void Node_in_neither_role_has_no_active_role() { ClusterPrimaryHealthCheck.ActiveRoleFor(["cluster-MAIN"]).ShouldBeNull(); } [Fact] public async Task No_actor_system_is_degraded_not_unhealthy() { // A node that is still booting has not lost an election. Reporting Unhealthy here would make // every start look like a failed standby, and — now that the tier is a real 503 — would pull // the node out of Traefik's pool on the way up. var check = new ClusterPrimaryHealthCheck(new ServiceCollection().BuildServiceProvider()); var result = await check.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); result.Status.ShouldBe(HealthStatus.Degraded); } }