From 56334183c95a521bdd0bb332b85533bc56b0513e Mon Sep 17 00:00:00 2001 From: Neil Colvin Date: Wed, 21 Oct 2026 12:39:38 +0100 Subject: [PATCH 1/3] Expose the agent provider contract for engine extensions Co-authored-by: OpenAI Codex --- src/NUnit.Engine.Api/ITestAgentProvider.cs | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/NUnit.Engine.Api/ITestAgentProvider.cs diff --git a/src/NUnit.Engine.Api/ITestAgentProvider.cs b/src/NUnit.Engine.Api/ITestAgentProvider.cs new file mode 100644 index 0000000..490d3f6 --- /dev/null +++ b/src/NUnit.Engine.Api/ITestAgentProvider.cs @@ -0,0 +1,53 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; + +namespace NUnit.Engine.Services +{ + /// + /// An object implementing ITestAgentProvider is able to provide + /// test agents, which satisfy the criteria specified in a TestPackage. + /// The engine loads implementations through /NUnit/Engine/AgentProviders. + /// Capability queries do not acquire execution ownership. An acquired agent + /// remains associated with its provider until ReleaseAgent succeeds. + /// + public interface ITestAgentProvider : ITestAgentInfo + { + /// + /// Returns true if an agent can be found, which is suitable + /// for running the provided test package. + /// + /// A TestPackage + /// True if the provider can supply a compatible agent. + /// Capability queries must not acquire execution ownership. + bool IsAgentAvailable(TestPackage package); + + /// + /// Return an agent, which best matches the criteria defined + /// in a TestPackage. + /// + /// The test package to be run + /// An ITestAgent + /// If the package is invalid. + /// If no compatible agent is available. + ITestAgent GetAgent(TestPackage package); + + /// + /// Releases the test agent back to the supplier, which provided it. + /// + /// An agent previously provided by a call to GetAgent. + /// + /// If agent was never provided by the factory or was previously released. + /// + /// + /// Return the agent through the same provider which acquired it so that + /// ownership bookkeeping remains consistent. A stop request, a closed + /// connection or disposal of a runner is not confirmation of release. + /// If release fails, retain ownership until the provider can reconcile it. + /// The provider defines how it stops its agent. Successful release alone + /// does not prove that external deployment files can be deleted safely; + /// remote providers must confirm execution has stopped before allowing cleanup. + /// + void ReleaseAgent(ITestAgent agent); + } +} From 549f2315210dd0af6ade3f9320852a22cafa5e07 Mon Sep 17 00:00:00 2001 From: Neil Colvin Date: Wed, 28 Oct 2026 05:25:52 +0000 Subject: [PATCH 2/3] docs: clarify agent provider lifecycle and ownership Qualify discovery and capability queries, document acquisition and failed release responsibility, and apply resource cleanup guidance to all providers. Remove the obsolete provider-loading path claim without changing signatures. Prepared by Neil Colvin with assistance from OpenAI Codex. --- src/NUnit.Engine.Api/ITestAgentProvider.cs | 92 +++++++++++++++++----- 1 file changed, 71 insertions(+), 21 deletions(-) diff --git a/src/NUnit.Engine.Api/ITestAgentProvider.cs b/src/NUnit.Engine.Api/ITestAgentProvider.cs index 490d3f6..aef9013 100644 --- a/src/NUnit.Engine.Api/ITestAgentProvider.cs +++ b/src/NUnit.Engine.Api/ITestAgentProvider.cs @@ -5,48 +5,98 @@ namespace NUnit.Engine.Services { /// - /// An object implementing ITestAgentProvider is able to provide - /// test agents, which satisfy the criteria specified in a TestPackage. - /// The engine loads implementations through /NUnit/Engine/AgentProviders. - /// Capability queries do not acquire execution ownership. An acquired agent - /// remains associated with its provider until ReleaseAgent succeeds. + /// Provides test agents suitable for a and accepts + /// their return when the caller no longer requires them. /// + /// + /// + /// This contract applies to in-process, local-process and remote agents. It does + /// not require the caller to own a process or perform a particular registration + /// handshake. The provider is responsible for the implementation-specific setup + /// and lifetime of the agents it supplies. + /// + /// + /// The caller is responsible for returning each acquired agent through the same + /// provider. Capability queries do not transfer that responsibility to the caller; + /// see for their initialization and discovery limits. + /// + /// public interface ITestAgentProvider : ITestAgentInfo { /// - /// Returns true if an agent can be found, which is suitable + /// Determines whether the provider can currently supply an agent suitable /// for running the provided test package. /// /// A TestPackage - /// True if the provider can supply a compatible agent. - /// Capability queries must not acquire execution ownership. + /// True if the provider can currently supply a compatible agent. + /// + /// + /// This query does not return an agent or make the caller responsible for + /// releasing one. It does not guarantee that no extension or agent implementation + /// is loaded or instantiated internally. Depending on the information supplied + /// by an extension, the provider may use declared metadata or instantiate the + /// extension to determine its capabilities. Other implementations may need to + /// initialize provider resources or contact another host. + /// + /// + /// A successful query does not reserve an agent. Availability may change before + /// is called, so the caller must still handle acquisition failure. + /// The provider remains responsible for resources created while evaluating the query. + /// + /// bool IsAgentAvailable(TestPackage package); /// - /// Return an agent, which best matches the criteria defined - /// in a TestPackage. + /// Acquires an agent suitable for the criteria specified in a . /// /// The test package to be run - /// An ITestAgent + /// The acquired agent, which must be returned through this provider. /// If the package is invalid. - /// If no compatible agent is available. + /// + /// No compatible agent is available. Call first + /// to check suitability, while allowing for availability to change before acquisition. + /// + /// + /// + /// Successful acquisition makes the caller responsible for returning this exact + /// agent reference using . The provider retains responsibility + /// for its underlying implementation and resources; acquiring an agent does not + /// imply that the caller owns its process, transport or deployment. + /// + /// + /// If acquisition throws, the caller has no returned agent to release. The provider + /// remains responsible for resources created during the failed attempt and for + /// reconciling an outcome that cannot yet be confirmed. + /// + /// ITestAgent GetAgent(TestPackage package); /// - /// Releases the test agent back to the supplier, which provided it. + /// Returns a previously acquired test agent to the provider that supplied it. /// /// An agent previously provided by a call to GetAgent. /// - /// If agent was never provided by the factory or was previously released. + /// The reference was not acquired from this provider or was previously released. /// /// - /// Return the agent through the same provider which acquired it so that - /// ownership bookkeeping remains consistent. A stop request, a closed - /// connection or disposal of a runner is not confirmation of release. - /// If release fails, retain ownership until the provider can reconcile it. - /// The provider defines how it stops its agent. Successful release alone - /// does not prove that external deployment files can be deleted safely; - /// remote providers must confirm execution has stopped before allowing cleanup. + /// + /// Return the exact reference through the same provider that supplied it so that + /// ownership bookkeeping remains consistent. The provider decides whether to stop + /// the agent, retain it for reuse or return it to another source. After successful + /// release, the caller must not continue using the reference. + /// + /// + /// A stop request, a closed connection or disposal of a runner is not confirmation + /// of release. If release throws, preserve the association with the originating + /// provider until it can reconcile the outcome. This does not guarantee that an + /// unconfirmed operation can be repeated without reconciliation. + /// + /// + /// Successful release does not by itself prove that the agent terminated or that + /// external resources, including deployed files, are no longer in use. Before + /// removing resources used by an agent, the responsible provider must establish + /// that the agent no longer needs them. This applies to all provider types. + /// /// void ReleaseAgent(ITestAgent agent); } From f356819687142f031f8b1fcce4130d395aa2d0e1 Mon Sep 17 00:00:00 2001 From: Neil Colvin Date: Wed, 28 Oct 2026 14:59:43 +0000 Subject: [PATCH 3/3] api: use GetAgent and ReturnAgent for provider contract Adopt the reviewed ReturnAgent name and keep provider implementation and reuse policy opaque to callers. Simplify failed-acquisition responsibility and consistently describe returning acquired references. Prepared by Neil Colvin with assistance from OpenAI Codex. --- src/NUnit.Engine.Api/ITestAgentProvider.cs | 35 +++++++++++----------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/NUnit.Engine.Api/ITestAgentProvider.cs b/src/NUnit.Engine.Api/ITestAgentProvider.cs index aef9013..9d69dcd 100644 --- a/src/NUnit.Engine.Api/ITestAgentProvider.cs +++ b/src/NUnit.Engine.Api/ITestAgentProvider.cs @@ -12,8 +12,9 @@ namespace NUnit.Engine.Services /// /// This contract applies to in-process, local-process and remote agents. It does /// not require the caller to own a process or perform a particular registration - /// handshake. The provider is responsible for the implementation-specific setup - /// and lifetime of the agents it supplies. + /// handshake. The caller does not need to know how a provider obtains its agents + /// or what resources it manages. The provider is responsible for its agents + /// and their resources. /// /// /// The caller is responsible for returning each acquired agent through the same @@ -32,7 +33,7 @@ public interface ITestAgentProvider : ITestAgentInfo /// /// /// This query does not return an agent or make the caller responsible for - /// releasing one. It does not guarantee that no extension or agent implementation + /// returning one. It does not guarantee that no extension or agent implementation /// is loaded or instantiated internally. Depending on the information supplied /// by an extension, the provider may use declared metadata or instantiate the /// extension to determine its capabilities. Other implementations may need to @@ -59,14 +60,13 @@ public interface ITestAgentProvider : ITestAgentInfo /// /// /// Successful acquisition makes the caller responsible for returning this exact - /// agent reference using . The provider retains responsibility + /// agent reference using . The provider retains responsibility /// for its underlying implementation and resources; acquiring an agent does not /// imply that the caller owns its process, transport or deployment. /// /// - /// If acquisition throws, the caller has no returned agent to release. The provider - /// remains responsible for resources created during the failed attempt and for - /// reconciling an outcome that cannot yet be confirmed. + /// If acquisition throws, the provider remains responsible for resources created + /// during the failed attempt. /// /// ITestAgent GetAgent(TestPackage package); @@ -74,30 +74,31 @@ public interface ITestAgentProvider : ITestAgentInfo /// /// Returns a previously acquired test agent to the provider that supplied it. /// - /// An agent previously provided by a call to GetAgent. + /// An agent previously acquired by a call to . /// - /// The reference was not acquired from this provider or was previously released. + /// The reference was not acquired from this provider or was previously returned. /// /// /// /// Return the exact reference through the same provider that supplied it so that - /// ownership bookkeeping remains consistent. The provider decides whether to stop - /// the agent, retain it for reuse or return it to another source. After successful - /// release, the caller must not continue using the reference. + /// ownership bookkeeping remains consistent. After successfully returning the + /// agent, the caller must not continue using the reference. The provider remains + /// responsible for its agents and their resources. /// /// /// A stop request, a closed connection or disposal of a runner is not confirmation - /// of release. If release throws, preserve the association with the originating - /// provider until it can reconcile the outcome. This does not guarantee that an - /// unconfirmed operation can be repeated without reconciliation. + /// that the agent was returned. If returning an agent fails, preserve its association + /// with the originating provider. Further attempts to return it must use that provider. + /// A failed call does not guarantee that no work was performed, so the caller must + /// not assume that repeating it is harmless. /// /// - /// Successful release does not by itself prove that the agent terminated or that + /// Successfully returning an agent does not by itself prove that it terminated or that /// external resources, including deployed files, are no longer in use. Before /// removing resources used by an agent, the responsible provider must establish /// that the agent no longer needs them. This applies to all provider types. /// /// - void ReleaseAgent(ITestAgent agent); + void ReturnAgent(ITestAgent agent); } }