using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
using ZB.MOM.WW.LocalDb.Replication;
using ZB.MOM.WW.OtOpcUa.Host.Health;
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
///
/// LocalDb Phase 1 (Task 10) — the replication health check.
///
///
/// The load-bearing case is the default-OFF one: a plain driver node with no peer and no
/// listener must report Healthy, or every unreplicated node in the fleet degrades the
/// moment this check ships. Beyond that, "connected" is not sufficient for healthy — an unknown
/// backlog (a failed oplog poll) must not read as zero.
///
public sealed class LocalDbReplicationHealthCheckTests
{
// ---- Pure decision core (table-tested) --------------------------------------------------
[Fact]
public void Unconfigured_IsHealthy_SoDefaultOffNodesDoNotDegrade()
{
LocalDbReplicationDecision.Evaluate(
peerConfigured: false, connected: false, oplogBacklog: null, degradedThreshold: 100_000)
.Status.ShouldBe(HealthStatus.Healthy);
}
[Fact]
public void ConfiguredButNotConnected_IsDegraded()
{
LocalDbReplicationDecision.Evaluate(
peerConfigured: true, connected: false, oplogBacklog: null, degradedThreshold: 100_000)
.Status.ShouldBe(HealthStatus.Degraded);
}
[Fact]
public void ConnectedButBacklogUnknown_IsDegraded()
{
// null backlog = the oplog poll failed. Reporting Healthy here would call a pair that cannot
// read its own oplog perfectly fine.
LocalDbReplicationDecision.Evaluate(
peerConfigured: true, connected: true, oplogBacklog: null, degradedThreshold: 100_000)
.Status.ShouldBe(HealthStatus.Degraded);
}
[Fact]
public void ConnectedWithSmallBacklog_IsHealthy()
{
LocalDbReplicationDecision.Evaluate(
peerConfigured: true, connected: true, oplogBacklog: 12, degradedThreshold: 100_000)
.Status.ShouldBe(HealthStatus.Healthy);
}
[Fact]
public void ConnectedWithBacklogOverThreshold_IsDegraded()
{
// A backlog past the threshold means the peer is falling behind faster than it drains —
// connected, but not keeping up.
LocalDbReplicationDecision.Evaluate(
peerConfigured: true, connected: true, oplogBacklog: 100_001, degradedThreshold: 100_000)
.Status.ShouldBe(HealthStatus.Degraded);
}
// ---- IHealthCheck wrapper ---------------------------------------------------------------
[Fact]
public async Task Wrapper_WithNoSyncStatusRegistered_IsHealthy()
{
// Admin-only graphs never register the replication engine. The check is registered
// unconditionally (AddOtOpcUaHealth takes no role argument), so it must no-op to Healthy
// rather than throw or degrade when LocalDb is simply absent.
var check = new LocalDbReplicationHealthCheck(
syncStatus: null,
options: Options.Create(new ReplicationOptions()),
syncListenPort: 0);
var result = await check.CheckHealthAsync(new HealthCheckContext(), TestContext.Current.CancellationToken);
result.Status.ShouldBe(HealthStatus.Healthy);
}
[Fact]
public async Task Wrapper_ListenerConfiguredButNotConnected_IsDegraded()
{
// A passive node (no PeerAddress) still counts as "replication configured" when it is
// listening — otherwise a passive node that never gets dialled would look healthy while
// silently accepting nothing.
var check = new LocalDbReplicationHealthCheck(
syncStatus: new StubSyncStatus { Connected = false },
options: Options.Create(new ReplicationOptions { PeerAddress = "" }),
syncListenPort: 9001);
var result = await check.CheckHealthAsync(new HealthCheckContext(), TestContext.Current.CancellationToken);
result.Status.ShouldBe(HealthStatus.Degraded);
}
[Fact]
public async Task Wrapper_PeerConfiguredAndConnectedAndDraining_IsHealthy()
{
var check = new LocalDbReplicationHealthCheck(
syncStatus: new StubSyncStatus { Connected = true, OplogBacklog = 3 },
options: Options.Create(new ReplicationOptions { PeerAddress = "https://peer:9001" }),
syncListenPort: 9001);
var result = await check.CheckHealthAsync(new HealthCheckContext(), TestContext.Current.CancellationToken);
result.Status.ShouldBe(HealthStatus.Healthy);
}
private sealed class StubSyncStatus : ISyncStatus
{
public bool Connected { get; init; }
public string? PeerNodeId { get; init; }
public DateTimeOffset? LastSyncUtc { get; init; }
public long? OplogBacklog { get; init; }
public long ConnectionAttempts { get; init; }
}
}