diff --git a/Services/SclSafeTrialRunner.cs b/Services/SclSafeTrialRunner.cs index 4b8490600..686e811c8 100644 --- a/Services/SclSafeTrialRunner.cs +++ b/Services/SclSafeTrialRunner.cs @@ -127,7 +127,8 @@ public static async Task RunAsync( var document = XDocument.Load(command.SclPath, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); var sclXml = document.ToString(SaveOptions.DisableFormatting); - var client = new NativeIec61850Client(); + // Own the entire read-only trial association, including failure and cancellation. + await using var client = new NativeIec61850Client(); result = await client.ConnectUsingSclAsync( sclXml, command.IedName, diff --git a/tests/ARSAS.Tests/SclSafeTrialRunnerTests.cs b/tests/ARSAS.Tests/SclSafeTrialRunnerTests.cs index 5e23a1e9a..f53a6afa5 100644 --- a/tests/ARSAS.Tests/SclSafeTrialRunnerTests.cs +++ b/tests/ARSAS.Tests/SclSafeTrialRunnerTests.cs @@ -67,6 +67,19 @@ public void CommandParser_RejectsInvalidMmsPort(string port) Assert.Contains("port", error, StringComparison.OrdinalIgnoreCase); } + [Fact] + public void TrialRunner_SourceContract_DisposesTrialClientOnEveryExit() + { + var source = File.ReadAllText(FindRepoFile("Services/SclSafeTrialRunner.cs")); + + // An awaited using declaration includes normal return, cancellation and + // exception paths, and keeps the association alive for snapshot capture. + Assert.Contains( + "await using var client = new NativeIec61850Client();", + source, + StringComparison.Ordinal); + } + [Fact] public void TrialRunner_SourceContract_HasNoDiscoveryWriteControlOrReportingEntryPoint() {