using Akka.Actor; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Runtime.Drivers; using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers; public sealed class DriverInstanceActorTests : RuntimeActorTestBase { [Fact] public async Task ApplyDelta_when_Connected_calls_ReinitializeAsync_and_replies_success() { var driver = new StubDriver(); var actor = Sys.ActorOf(DriverInstanceActor.Props(driver)); // Drive: Initialize → Connected. actor.Tell(new DriverInstanceActor.InitializeRequested("{}")); AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2)); // Issue ApplyDelta and capture the reply via Ask. var correlation = CorrelationId.NewId(); var reply = await actor.Ask( new DriverInstanceActor.ApplyDelta("{\"changed\":true}", correlation), TimeSpan.FromSeconds(3)); reply.Success.ShouldBeTrue(); reply.Correlation.ShouldBe(correlation); driver.ReinitializeCount.ShouldBe(1); } [Fact] public void Initialize_failure_keeps_actor_in_Reconnecting_state() { var driver = new StubDriver { InitializeShouldThrow = true }; var actor = Sys.ActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromMilliseconds(50))); actor.Tell(new DriverInstanceActor.InitializeRequested("{}")); // The actor should keep trying — we expect multiple Initialize calls because the // reconnect timer fires every 50ms. AwaitCondition(() => driver.InitializeCount >= 3, TimeSpan.FromSeconds(2)); } [Fact] public async Task Write_against_non_IWritable_driver_returns_failure() { var driver = new StubDriver(); // IDriver only, no IWritable. var actor = Sys.ActorOf(DriverInstanceActor.Props(driver)); actor.Tell(new DriverInstanceActor.InitializeRequested("{}")); AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2)); var reply = await actor.Ask( new DriverInstanceActor.WriteAttribute("tag-1", 42), TimeSpan.FromSeconds(3)); reply.Success.ShouldBeFalse(); reply.Reason!.ShouldContain("IWritable"); } private sealed class StubDriver : IDriver { public bool InitializeShouldThrow { get; set; } public int InitializeCount; public int ReinitializeCount; public string DriverInstanceId => "stub-driver-1"; public string DriverType => "Stub"; public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { Interlocked.Increment(ref InitializeCount); if (InitializeShouldThrow) throw new InvalidOperationException("stub-init-fail"); return Task.CompletedTask; } public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { Interlocked.Increment(ref ReinitializeCount); return Task.CompletedTask; } public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask; public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null); public long GetMemoryFootprint() => 0; public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask; } }