using Akka.Actor;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Host;
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
///
/// Unit coverage for the #459 down-if-alone recovery watchdog: an unexpected
/// termination (SBR self-down) must stop the host so the supervisor
/// restarts it, while a graceful host shutdown must stay silent and not re-trigger stop.
///
[Trait("Category", "Unit")]
public sealed class ActorSystemTerminationWatchdogTests
{
/// An unexpected ActorSystem termination stops the host so the supervisor restarts the node.
[Fact]
public async Task Unexpected_termination_stops_the_application()
{
var system = ActorSystem.Create("watchdog-unexpected");
var lifetime = new FakeHostApplicationLifetime();
var watchdog = new ActorSystemTerminationWatchdog(
() => system, lifetime, NullLogger.Instance);
await watchdog.StartAsync(CancellationToken.None);
// Simulate the SBR self-down: the ActorSystem terminates while the host is still running.
await system.Terminate();
await WaitForAsync(() => lifetime.StopApplicationCalls > 0, TimeSpan.FromSeconds(5));
lifetime.StopApplicationCalls.ShouldBe(1, "an out-of-band ActorSystem termination must stop the host once");
}
/// A graceful host stop (StopAsync ran) leaves the termination continuation silent.
[Fact]
public async Task Graceful_stop_does_not_stop_the_application()
{
var system = ActorSystem.Create("watchdog-graceful");
var lifetime = new FakeHostApplicationLifetime();
var watchdog = new ActorSystemTerminationWatchdog(
() => system, lifetime, NullLogger.Instance);
await watchdog.StartAsync(CancellationToken.None);
// Host is shutting down normally: StopAsync runs, THEN the system terminates.
await watchdog.StopAsync(CancellationToken.None);
await system.Terminate();
// Give any continuation a chance to run, then assert it stayed silent.
await Task.Delay(500, TestContext.Current.CancellationToken);
lifetime.StopApplicationCalls.ShouldBe(0, "a graceful shutdown must not re-trigger StopApplication");
}
/// Termination while the host is already stopping (ApplicationStopping fired) stays silent.
[Fact]
public async Task Termination_while_already_stopping_stays_silent()
{
var system = ActorSystem.Create("watchdog-already-stopping");
var lifetime = new FakeHostApplicationLifetime();
var watchdog = new ActorSystemTerminationWatchdog(
() => system, lifetime, NullLogger.Instance);
await watchdog.StartAsync(CancellationToken.None);
lifetime.TriggerStopping(); // the host is already tearing down (ApplicationStopping cancelled)
await system.Terminate();
await Task.Delay(500, TestContext.Current.CancellationToken);
lifetime.StopApplicationCalls.ShouldBe(0, "termination during an in-progress host shutdown must stay silent");
}
private static async Task WaitForAsync(Func condition, TimeSpan timeout)
{
var deadline = DateTime.UtcNow + timeout;
while (!condition() && DateTime.UtcNow < deadline)
await Task.Delay(20);
}
/// Minimal that records calls.
private sealed class FakeHostApplicationLifetime : IHostApplicationLifetime
{
private readonly CancellationTokenSource _stopping = new();
public int StopApplicationCalls { get; private set; }
public CancellationToken ApplicationStarted => CancellationToken.None;
public CancellationToken ApplicationStopping => _stopping.Token;
public CancellationToken ApplicationStopped => CancellationToken.None;
public void StopApplication() => StopApplicationCalls++;
public void TriggerStopping() => _stopping.Cancel();
}
}