From 84bfe6cd20285ccf136998998ef5c051917d8201 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 10 Nov 2026 16:51:31 -0500 Subject: [PATCH 1/3] test: cover SDK1 terminal server disposal ownership --- .../OpcUaApplicationHostDisposalTests.cs | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostDisposalTests.cs diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostDisposalTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostDisposalTests.cs new file mode 100644 index 00000000..529befa8 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostDisposalTests.cs @@ -0,0 +1,164 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Opc.Ua; +using Opc.Ua.Server; +using Shouldly; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; + +/// Verifies terminal server ownership on the SDK1 host (#807). +public sealed class OpcUaApplicationHostDisposalTests : IDisposable +{ + private readonly string _pki = Path.Combine(Path.GetTempPath(), $"otopcua-disposal-{Guid.NewGuid():N}"); + private static CancellationToken Ct => TestContext.Current.CancellationToken; + + /// Normal and repeated host disposal releases the actual SDK request-queue owner once. + [Fact] + public async Task Host_terminally_disposes_its_booted_server_once() + { + var (host, server) = await BootAsync(); + try + { + await host.DisposeAsync(); + server.TerminalDisposals.ShouldBe(1); + await host.DisposeAsync(); + server.TerminalDisposals.ShouldBe(1); + } + finally { await CleanupAsync(host, server); } + } + + /// A throwing Stop cannot bypass the separately owned terminal Dispose. + [Fact] + public async Task Stop_failure_still_terminally_disposes_the_server() + { + var (host, server) = await BootAsync(); + server.ThrowAfterStop = true; + try + { + await host.DisposeAsync(); + server.StopCalls.ShouldBe(1); + server.TerminalDisposals.ShouldBe(1); + } + finally { await CleanupAsync(host, server); } + } + + /// A server accepted before startup fails remains owned by the host. + [Fact] + public async Task Partial_startup_failure_does_not_strand_the_server() + { + var host = NewHost(OpcUaHostStart.AllocateFreePort()); + var server = new RecordingServer { ThrowDuringStart = true }; + try + { + var failure = await Should.ThrowAsync(() => host.StartAsync(server, Ct)); + failure.ToString().ShouldContain("controlled partial startup"); + await host.DisposeAsync(); + server.TerminalDisposals.ShouldBe(1); + } + finally { await CleanupAsync(host, server); } + } + + /// Every concurrent caller waits for the one terminal cleanup owner. + [Fact] + public async Task Concurrent_disposers_share_completion_and_one_cleanup_owner() + { + var (host, server) = await BootAsync(); + server.BlockStop = true; + var calls = new List(); + try + { + calls.Add(host.DisposeAsync().AsTask()); + await server.StopEntered.Task.WaitAsync(TimeSpan.FromSeconds(10), Ct); + for (var i = 0; i < 3; i++) calls.Add(host.DisposeAsync().AsTask()); + calls.ShouldAllBe(task => !task.IsCompleted); + server.StopCalls.ShouldBe(1); + server.TerminalDisposals.ShouldBe(0); + server.ReleaseStop.TrySetResult(); + await Task.WhenAll(calls).WaitAsync(TimeSpan.FromSeconds(20), Ct); + server.TerminalDisposals.ShouldBe(1); + } + finally + { + server.ReleaseStop.TrySetResult(); + try { await Task.WhenAll(calls).WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); } + finally { await CleanupAsync(host, server); } + } + } + + /// Terminal disposal before startup refuses a new owner instead of silently stranding it. + [Fact] + public async Task Disposed_host_rejects_start_without_taking_server_ownership() + { + var host = NewHost(OpcUaHostStart.AllocateFreePort()); + var server = new RecordingServer(); + try + { + await host.DisposeAsync(); + await Should.ThrowAsync(() => host.StartAsync(server, Ct)); + server.TerminalDisposals.ShouldBe(0, "the rejected server still belongs to its caller"); + } + finally { await CleanupAsync(host, server); } + } + + private Task<(OpcUaApplicationHost Host, RecordingServer Server)> BootAsync() => + OpcUaHostStart.StartAsync(NewHost, () => new RecordingServer(), Ct); + + private OpcUaApplicationHost NewHost(int port) => new(new OpcUaApplicationHostOptions + { + ApplicationName = "OtOpcUa.Disposal", ApplicationUri = "urn:OtOpcUa.Disposal", + PublicHostname = "localhost", OpcUaPort = port, PkiStoreRoot = _pki, + }, NullLogger.Instance); + + private static async Task CleanupAsync(OpcUaApplicationHost host, RecordingServer server) + { + try { await host.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); } + finally + { + // Red tests still release their test-owned server after recording the missing host disposal. + if (server.TerminalDisposals == 0) server.Dispose(); + } + } + + private sealed class RecordingServer : StandardServer + { + private int _terminalDisposals; + private int _stopCalls; + internal int TerminalDisposals => Volatile.Read(ref _terminalDisposals); + internal int StopCalls => Volatile.Read(ref _stopCalls); + internal bool ThrowDuringStart { get; init; } + internal bool ThrowAfterStop { get; set; } + internal bool BlockStop { get; set; } + internal TaskCompletionSource StopEntered { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + internal TaskCompletionSource ReleaseStop { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + protected override void OnServerStarting(ApplicationConfiguration configuration) + { + base.OnServerStarting(configuration); + if (ThrowDuringStart) throw new InvalidOperationException("controlled partial startup"); + } + + public override async ValueTask StopAsync(CancellationToken cancellationToken = default) + { + Interlocked.Increment(ref _stopCalls); + if (BlockStop) + { + StopEntered.TrySetResult(); + await ReleaseStop.Task.WaitAsync(cancellationToken); + } + await base.StopAsync(cancellationToken); + if (ThrowAfterStop) throw new InvalidOperationException("controlled Stop failure"); + } + + protected override void Dispose(bool disposing) + { + if (disposing) Interlocked.Increment(ref _terminalDisposals); + base.Dispose(disposing); + } + } + + /// Removes only this test's private fixture. + public void Dispose() + { + if (Directory.Exists(_pki)) Directory.Delete(_pki, true); + } +} From 10edc44f94afc42300518f1504091c58217a89a9 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 10 Nov 2026 16:53:34 -0500 Subject: [PATCH 2/3] fix: terminally dispose SDK1 server ownership once --- .../OpcUaApplicationHost.cs | 86 ++++++++++++++----- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs index 6a806fab..73272688 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs @@ -173,6 +173,9 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable private ApplicationInstance? _application; private StandardServer? _server; private ImpersonateEventHandler? _impersonateHandler; + private readonly object _lifetimeLock = new(); + private readonly TaskCompletionSource _disposeCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously); + private int _disposeStarted; /// Initializes a new instance of the OPC UA application host. /// The host configuration options. @@ -200,16 +203,18 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable /// A task that represents the asynchronous operation. public async Task StartAsync(StandardServer server, CancellationToken cancellationToken) { - _server = server; - // 1.5.378 requires an ITelemetryContext on the ApplicationInstance ctor (the parameterless ctor - // is obsolete). DefaultTelemetry.Create wires the SDK's internal logging; an empty builder keeps - // the SDK's trace off our ILogger (the host keeps its own _logger) — sufficient for the bootstrap. - _application = new ApplicationInstance(DefaultTelemetry.Create(_ => { })) + lock (_lifetimeLock) { - ApplicationName = _options.ApplicationName, - ApplicationType = ApplicationType.Server, - ConfigSectionName = "OtOpcUa", - }; + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposeStarted) != 0, this); + _server = server; + // 1.5.378 requires telemetry on ApplicationInstance; the empty builder keeps SDK tracing separate. + _application = new ApplicationInstance(DefaultTelemetry.Create(_ => { })) + { + ApplicationName = _options.ApplicationName, + ApplicationType = ApplicationType.Server, + ConfigSectionName = "OtOpcUa", + }; + } _ = await BuildConfigurationAsync(cancellationToken); await EnsureApplicationCertificateAsync(cancellationToken).ConfigureAwait(false); @@ -657,25 +662,60 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable /// Disposes the application host and cleans up resources. /// A task that represents the asynchronous operation. - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() { + if (Interlocked.CompareExchange(ref _disposeStarted, 1, 0) == 0) + _ = CompleteDisposalAsync(); + return new ValueTask(_disposeCompletion.Task); + } + + private async Task CompleteDisposalAsync() + { + Exception? failure = null; try { - // An explicit SDK stop may already have halted CurrentInstance; still continue disposal. - if (_impersonateHandler is not null && _server?.CurrentInstance?.SessionManager is { } sessionManager) - sessionManager.ImpersonateUser -= _impersonateHandler; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "OpcUaApplicationHost: detaching ImpersonateUser threw"); - } - _impersonateHandler = null; + ApplicationInstance? application; + StandardServer? server; + ImpersonateEventHandler? handler; + lock (_lifetimeLock) + { + application = _application; + server = _server; + handler = _impersonateHandler; + _application = null; + _server = null; + _impersonateHandler = null; + } - if (_application is not null) + try + { + try + { + // An explicit SDK stop may already have halted CurrentInstance; terminal disposal still follows. + if (handler is not null && server?.CurrentInstance?.SessionManager is { } sessionManager) + sessionManager.ImpersonateUser -= handler; + } + catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: detaching ImpersonateUser threw"); } + + if (application is not null) + { + try { await application.StopAsync().ConfigureAwait(false); } + catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: Stop threw on dispose"); } + } + } + finally + { + // SDK1 Stop does not release the server's request queue or terminal event registrations (#807). + try { server?.Dispose(); } + catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: server disposal threw"); } + } + } + catch (Exception error) { failure = error; } + finally { - // 1.5.378: ApplicationInstance.Stop() → StopAsync(). - try { await _application.StopAsync().ConfigureAwait(false); } - catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: Stop threw on dispose"); } + // All callers observe completion, including when a logger or cleanup callback throws. + if (failure is null) _disposeCompletion.TrySetResult(); + else _disposeCompletion.TrySetException(failure); } } } From efb9197a0f48802450474413c58b9d0c461b836d Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 10 Nov 2026 17:05:06 -0500 Subject: [PATCH 3/3] docs: record reviewed SDK1 terminal disposal repair --- .gitignore | 3 + .../2026-11-10-sdk1-terminal-disposal.json | 349 ++++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 docs/plans/evidence/2026-11-10-sdk1-terminal-disposal.json diff --git a/.gitignore b/.gitignore index 0a7b712b..0075c1c1 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,6 @@ lib/ # Subagent-driven-development git worktrees .worktrees/ + +# Local OPC UA validation runs; selected receipts belong under docs/plans/evidence. +/artifacts/opcua2/ diff --git a/docs/plans/evidence/2026-11-10-sdk1-terminal-disposal.json b/docs/plans/evidence/2026-11-10-sdk1-terminal-disposal.json new file mode 100644 index 00000000..07716962 --- /dev/null +++ b/docs/plans/evidence/2026-11-10-sdk1-terminal-disposal.json @@ -0,0 +1,349 @@ +{ + "issue": 807, + "scope": "SDK1 host terminal-disposal repair; independent of SDK2 migration release acceptance.", + "originalMaster": "52d1373e8129b7f4f0ea2e35f86a8a5d0ed1de36", + "redSource": "84bfe6cd20285ccf136998998ef5c051917d8201", + "greenSource": "10edc44f94afc42300518f1504091c58217a89a9", + "redProductionEqualsOriginalMaster": true, + "redOriginalProductionSha256": "346362c8c18b5bcdb90757f654c02c68bad44fc5875abc8b4672f4c7ac49b381", + "greenProductionSha256": "c4934b15e695a4110e8cea8fbdef8faabad80aadeca4ccfb007b11400057ff9b", + "resolvedSdkPackages": [ + "OPCFoundation.NetStandard.Opc.Ua.Configuration/1.5.378.182", + "OPCFoundation.NetStandard.Opc.Ua.Core/1.5.378.182", + "OPCFoundation.NetStandard.Opc.Ua.Security.Certificates/1.5.378.182", + "OPCFoundation.NetStandard.Opc.Ua.Server/1.5.378.182", + "OPCFoundation.NetStandard.Opc.Ua.Types/1.5.378.182" + ], + "builds": [ + { + "rawLogPath": "/tmp/opcua-sdk1-terminal-red-build.log", + "sha256": "38f5561a0db2a2d26f8462f36e114ced057c189effcd6e87dc5a62a42c06d52e", + "warnings": 0, + "errors": 0 + }, + { + "rawLogPath": "/tmp/opcua-sdk1-terminal-green-build.log", + "sha256": "5fae318d45d9b5b8b4ba2812ef87869edf6d851e09848154d5f4a51f47ed68d2", + "warnings": 0, + "errors": 0 + }, + { + "rawLogPath": "/tmp/opcua-sdk1-terminal-solution-build.log", + "sha256": "13317ee89238730216874531fad82e3591a6d0d1b5503c8bd896dcc95c885daf", + "warnings": 0, + "errors": 0 + } + ], + "runs": [ + { + "source": "84bfe6cd20285ccf136998998ef5c051917d8201", + "rawDirectory": "artifacts/opcua2/84bfe6cd/sdk1-terminal-red", + "command": [ + "dotnet", + "test", + "tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.csproj", + "-c", + "Release", + "--no-build", + "--no-restore", + "--filter", + "FullyQualifiedName~OpcUaApplicationHostDisposalTests", + "--logger", + "trx;LogFileName=close.trx", + "--results-directory", + "artifacts/opcua2/84bfe6cd/sdk1-terminal-red" + ], + "testExit": 1, + "guardPassed": true, + "runnerSeconds": 3.8918902919394895, + "counters": { + "total": "5", + "executed": "5", + "passed": "0", + "failed": "5", + "error": "0", + "timeout": "0", + "aborted": "0", + "inconclusive": "0", + "passedButRunAborted": "0", + "notRunnable": "0", + "notExecuted": "0", + "disconnected": "0", + "warning": "0", + "completed": "0", + "inProgress": "0", + "pending": "0" + }, + "selectedResults": [ + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Host_terminally_disposes_its_booted_server_once", + "outcome": "Failed", + "error": "Shouldly.ShouldAssertException : server.TerminalDisposals\r\n should be\r\n1\r\n but was\r\n0" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Concurrent_disposers_share_completion_and_one_cleanup_owner", + "outcome": "Failed", + "error": "Shouldly.ShouldAssertException : server.StopCalls\r\n should be\r\n1\r\n but was\r\n4" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Disposed_host_rejects_start_without_taking_server_ownership", + "outcome": "Failed", + "error": "Shouldly.ShouldAssertException : Task `host.StartAsync(server, Ct)`\r\n should throw\r\nSystem.ObjectDisposedException\r\n but did not" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Stop_failure_still_terminally_disposes_the_server", + "outcome": "Failed", + "error": "Shouldly.ShouldAssertException : server.TerminalDisposals\r\n should be\r\n1\r\n but was\r\n0" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Partial_startup_failure_does_not_strand_the_server", + "outcome": "Failed", + "error": "Shouldly.ShouldAssertException : server.TerminalDisposals\r\n should be\r\n1\r\n but was\r\n0" + } + ], + "rawHashes": { + "close.trx": "ec832633adf184a07f31075ff342bb2c1b9d3f3893c518f5737dc02526f886bb", + "run.log": "cdb7c4982d45807de53f70dfc22fad69ccda1db740db05407cef8df8b6b6d904", + "receipt.json": "4469c071c0d178446ccfdbdca4bf11ebad43c33feb91d8a8c5f2fa891579f2dc" + } + }, + { + "source": "10edc44f94afc42300518f1504091c58217a89a9", + "rawDirectory": "artifacts/opcua2/10edc44f/sdk1-terminal-green", + "command": [ + "dotnet", + "test", + "tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.csproj", + "-c", + "Release", + "--no-build", + "--no-restore", + "--filter", + "FullyQualifiedName~OpcUaApplicationHostDisposalTests", + "--logger", + "trx;LogFileName=close.trx", + "--results-directory", + "artifacts/opcua2/10edc44f/sdk1-terminal-green" + ], + "testExit": 0, + "guardPassed": true, + "runnerSeconds": 3.336757709039375, + "counters": { + "total": "5", + "executed": "5", + "passed": "5", + "failed": "0", + "error": "0", + "timeout": "0", + "aborted": "0", + "inconclusive": "0", + "passedButRunAborted": "0", + "notRunnable": "0", + "notExecuted": "0", + "disconnected": "0", + "warning": "0", + "completed": "0", + "inProgress": "0", + "pending": "0" + }, + "selectedResults": [ + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Partial_startup_failure_does_not_strand_the_server", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Concurrent_disposers_share_completion_and_one_cleanup_owner", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Host_terminally_disposes_its_booted_server_once", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Stop_failure_still_terminally_disposes_the_server", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Disposed_host_rejects_start_without_taking_server_ownership", + "outcome": "Passed" + } + ], + "rawHashes": { + "close.trx": "c44ba287659af567fcba1209c147aa5bfb6f2fc27753c0923d08b92372c007b4", + "run.log": "7f68723461a5ac4154c247b8bdfcc6d3770a5c1c1a88016051ef492f1684c88e", + "receipt.json": "bea02609e11dcc4569a26add9ae5836b101bf107d0346dd34514fc1d114ba525" + } + }, + { + "source": "10edc44f94afc42300518f1504091c58217a89a9", + "rawDirectory": "artifacts/opcua2/10edc44f/sdk1-server-suite", + "command": [ + "dotnet", + "test", + "tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.csproj", + "-c", + "Release", + "--no-build", + "--no-restore", + "--logger", + "trx;LogFileName=close.trx", + "--results-directory", + "artifacts/opcua2/10edc44f/sdk1-server-suite" + ], + "testExit": 0, + "guardPassed": true, + "runnerSeconds": 119.36682745895814, + "counters": { + "total": "1033", + "executed": "1032", + "passed": "1032", + "failed": "0", + "error": "0", + "timeout": "0", + "aborted": "0", + "inconclusive": "0", + "passedButRunAborted": "0", + "notRunnable": "0", + "notExecuted": "0", + "disconnected": "0", + "warning": "0", + "completed": "0", + "inProgress": "0", + "pending": "0" + }, + "selectedResults": [ + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.AddressSpaceComposerDeviceHostTests.Compose_resolves_driver_device_and_device_host", + "outcome": "NotExecuted", + "error": "v3: equipment no longer binds a driver/device \u2014 Equipment lost DriverInstanceId/DeviceId and Compose no longer takes a devices list, so EquipmentNode's DriverInstanceId/DeviceId/DeviceHost projection is always null. Equipment references raw tags via UnsTagReference. The device-endpoint merge this suite characterized now lives in Runtime.Tests DeploymentArtifactRawPathTests; the host-extraction unit is Commons.Tests DeviceConfigIntentTests." + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Host_terminally_disposes_its_booted_server_once", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.BuildSecurityPolicies_empty_input_falls_back_to_none", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostTests.StartAsync_creates_application_certificate_in_pki_own", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostQuotaTests.Configured_transport_quotas_replace_only_what_is_set", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.BuildSecurityPolicies_default_set_emits_all_three_baseline_profiles", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Partial_startup_failure_does_not_strand_the_server", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.BuildUserTokenPolicies_withholds_anonymous_when_disallowed", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.NullOpcUaUserAuthenticator_always_denies", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostQuotaTests.Configured_request_quotas_are_applied", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_anonymous_token_is_rejected_when_disallowed", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_username_success_sets_identity_and_no_validation_error", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Disposed_host_rejects_start_without_taking_server_ownership", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostServerArrayTests.ServerArray_contains_local_uri_and_configured_peers_after_start", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostQuotaTests.Default_request_quotas_are_the_former_hard_coded_values", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.StartAsync_with_only_signandencrypt_omits_None_endpoint", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Concurrent_disposers_share_completion_and_one_cleanup_owner", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostQuotaTests.Unset_transport_quotas_keep_the_sdk_defaults", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Stop_failure_still_terminally_disposes_the_server", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_anonymous_token_is_admitted_with_a_role_carrying_identity", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.StartAsync_populates_ServerConfiguration_with_all_enabled_profiles", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostTests.StartAsync_reuses_existing_certificate_on_second_boot", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_username_denial_sets_validation_error_and_no_identity", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostQuotaTests.History_read_batch_cap_default_scales_with_the_machine", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.BuildSecurityPolicies_dedupes_repeated_profiles", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_authenticator_throwing_results_in_rejection", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_null_username_treated_as_empty_string", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_username_success_carries_roles_on_identity", + "outcome": "Passed" + }, + { + "name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.BuildUserTokenPolicies_emits_anonymous_and_username", + "outcome": "Passed" + } + ], + "rawHashes": { + "close.trx": "800ac4134f48fc901da2c3519573184337c3f9a22cc5e7a5f4859a6b586fbf70", + "run.log": "253457c47f8801d37ce5fafc5c95cb9f5f0a9bbcead363c4ad0f5256e18fc7df", + "receipt.json": "b0db91f846ca883c8b8d48a946021f5e99785a26c37ff1087187b83575f62c96" + } + } + ], + "existingSkip": "AddressSpaceComposerDeviceHostTests.Compose_resolves_driver_device_and_device_host", + "newSkips": 0, + "ownership": "One caller owns detach/Stop/terminal Dispose; all Dispose callers await shared completion; Start after disposal begins is rejected.", + "limits": [ + "Does not serialize an already-running asynchronous Start against Dispose.", + "Does not alter immutable83ed880e baseline publications or aggregate regression oracles.", + "Does not establish any relative RSS denominator, #799 acceptance or SDK2 release approval." + ], + "review": "Astra approved the isolated SDK1 source/result/package gate: byte-identical original production RED; five expected REDs and five GREENs; 1032 owning-suite passes with one existing skip; all build hashes and selected security/PKI results verified. No SDK2 release or performance-baseline approval." +}