diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/Sdk2HostConcurrentDisposalTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/Sdk2HostConcurrentDisposalTests.cs
new file mode 100644
index 00000000..12414b79
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/Sdk2HostConcurrentDisposalTests.cs
@@ -0,0 +1,99 @@
+using Opc.Ua;
+using Opc.Ua.Configuration;
+using Opc.Ua.Server;
+using Shouldly;
+using Xunit;
+using ClientSession = Opc.Ua.Client.ISession;
+
+namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
+
+public sealed partial class Sdk2LifecycleSoakTests
+{
+ /// Concurrent host disposal awaits real encrypted-session shutdown before clearing ownership.
+ [Fact]
+ public async Task Concurrent_host_disposal_waits_for_the_actual_encrypted_session_to_close()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ var clientRoot = Path.Combine(Path.GetTempPath(), $"otua-dispose-client-{Guid.NewGuid():N}");
+ ApplicationInstance? application = null;
+ ServerEventRig? rig = null;
+ ClientSession? client = null;
+ ISessionManager? sessionManager = null;
+ SessionEventHandler? closing = null;
+ using var release = new ManualResetEventSlim(false);
+ var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var calls = new List();
+ var timedOut = 0;
+ var callbackCount = 0;
+ try
+ {
+ var telemetry = DefaultTelemetry.Create(_ => { });
+ var security = TestClientSecurity.Build(clientRoot);
+ security.ApplicationCertificate!.SubjectName = "CN=SDK2LifecycleSoak";
+ var config = new ApplicationConfiguration(telemetry)
+ {
+ ApplicationName = "SDK2LifecycleSoak", ApplicationUri = $"urn:SDK2LifecycleSoak:{Guid.NewGuid():N}",
+ ApplicationType = ApplicationType.Client, SecurityConfiguration = security,
+ ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 15_000 },
+ TransportQuotas = new TransportQuotas { OperationTimeout = 5000 },
+ };
+ application = new ApplicationInstance(telemetry) { ApplicationConfiguration = config };
+ await config.ValidateAsync(ApplicationType.Client, ct);
+ await application.CheckApplicationInstanceCertificatesAsync(true, null, ct);
+ rig = await ServerEventRig.StartAsync("SDK2ConcurrentDisposal", encrypted: true, ct);
+ Configure(rig);
+ client = await ConnectAsync(rig, config, null, ct);
+ sessionManager = rig.Server.CurrentInstance.SessionManager;
+ sessionManager.GetSessions().Count.ShouldBe(1);
+ var sessionId = client.SessionId;
+ closing = (session, reason) =>
+ {
+ if (session.Id != sessionId || reason != SessionEventReason.Closing) return;
+ Interlocked.Increment(ref callbackCount);
+ entered.TrySetResult();
+ // SDK swallows event exceptions; keep an explicit failure flag for this bounded barrier.
+ if (!release.Wait(TimeSpan.FromSeconds(15))) Interlocked.Exchange(ref timedOut, 1);
+ };
+ sessionManager.SessionClosing += closing;
+ var host = RequiredField(rig, "_host");
+ var manager = rig.Server.CertificateManager!;
+ calls.Add(Task.Run(async () => await host.DisposeAsync(), ct));
+ await entered.Task.WaitAsync(TimeSpan.FromSeconds(10), ct);
+ for (var i = 0; i < 3; i++) calls.Add(host.DisposeAsync().AsTask());
+ calls.ShouldAllBe(call => !call.IsCompleted, "every caller must await the actual shutdown owner");
+ host.Server.ShouldBeSameAs(rig.Server);
+ RequiredField(manager, "m_disposed").ShouldBeFalse();
+ release.Set();
+ await Task.WhenAll(calls).WaitAsync(TimeSpan.FromSeconds(20), ct);
+ Volatile.Read(ref timedOut).ShouldBe(0);
+ Volatile.Read(ref callbackCount).ShouldBe(1);
+ host.Server.ShouldBeNull();
+ host.ApplicationInstance.ShouldBeNull();
+ RequiredField(manager, "m_disposed").ShouldBeTrue();
+ CertificateCores(manager).Sum(CachedStores).ShouldBe(0);
+ }
+ finally
+ {
+ // Removing a handler takes the SDK's event lock: release before awaiting or unsubscribing.
+ release.Set();
+ try { await Task.WhenAll(calls).WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); }
+ finally
+ {
+ try { if (sessionManager is not null && closing is not null) sessionManager.SessionClosing -= closing; }
+ finally
+ {
+ try { client?.Dispose(); }
+ finally
+ {
+ try { if (rig is not null) await rig.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); }
+ finally
+ {
+ try { if (application is not null) await application.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); }
+ finally { if (Directory.Exists(clientRoot)) Directory.Delete(clientRoot, true); }
+ }
+ }
+ }
+ }
+ }
+ }
+}