diff --git a/Directory.Build.rsp b/Directory.Build.rsp
new file mode 100644
index 00000000..7633ea7a
--- /dev/null
+++ b/Directory.Build.rsp
@@ -0,0 +1,12 @@
+# MSBuild auto-response file, applied to every build run from this repo root.
+#
+# Disable the MSBuild node pool. By default MSBuild leaves worker nodes resident
+# after a build so the next one starts warm. Across a session of repeated
+# solution-wide builds and test runs this repo accumulated 55 idle nodes holding
+# ~6 GB, which is not merely untidy: the integration suites assert on timeouts,
+# and memory pressure turns those into failures indistinguishable from real
+# regressions (see the Workstation-GC note in Host.IntegrationTests.csproj for
+# the same failure mode from a different cause).
+#
+# The cost is a small startup penalty per build. That is a good trade here.
+-nodeReuse:false
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/DriverTypeNamesGuardTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/DriverTypeNamesGuardTests.cs
index 64c86d23..8f8c13a6 100644
--- a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/DriverTypeNamesGuardTests.cs
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/DriverTypeNamesGuardTests.cs
@@ -3,6 +3,7 @@ using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
+using ZB.MOM.WW.Secrets.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
@@ -23,13 +24,40 @@ public sealed class DriverTypeNamesGuardTests
private static readonly string[] NonFactoryDriverAssemblySuffixes =
{ ".Contracts", ".Addressing", ".Cli" };
+ ///
+ /// A stand-in for factories that require one.
+ ///
+ ///
+ /// Every secret resolves to absent. That is deliberate and safe here: the test only needs the
+ /// type-name key each Register call installs, and never invokes the registered factory
+ /// func, so no secret is ever requested. Resolving to absent rather than to a fake value means
+ /// that if this assumption is ever broken, the driver fails closed.
+ ///
+ private sealed class AbsentSecretResolver : ISecretResolver
+ {
+ public static readonly AbsentSecretResolver Instance = new();
+
+ public Task GetAsync(SecretName name, CancellationToken ct) =>
+ Task.FromResult(null);
+ }
+
///
/// Build the authoritative registered-factory set by discovering every
/// *DriverFactoryExtensions.Register in the deployed driver assemblies (by convention) and
- /// invoking it against a fresh registry. The factory func is never invoked — we only need the
- /// type-name key each Register call installs — so is passed for the
- /// optional loggerFactory/other parameters.
+ /// invoking it against a fresh registry. The factory func is never invoked — only the type-name
+ /// key each Register call installs is needed.
///
+ ///
+ /// Argument construction is by parameter type, not by position. It used to pass
+ /// for everything after the registry on the assumption that the
+ /// remaining parameters were all optional. They are not: GalaxyDriverFactoryExtensions.Register
+ /// takes a required and guards it with
+ /// ArgumentNullException.ThrowIfNull, so the reflective call threw and all three parity
+ /// facts failed — for a reason that had nothing to do with the drift they exist to catch.
+ /// (OpcUaClient's equivalent parameter is optional, which is why only Galaxy tripped it.)
+ /// A parameter this method cannot satisfy now fails with a message naming the factory and the
+ /// parameter, rather than surfacing as an opaque .
+ ///
private static IReadOnlyCollection RegisteredDriverTypeNames()
{
var registry = new DriverFactoryRegistry();
@@ -62,8 +90,7 @@ public sealed class DriverTypeNamesGuardTests
|| ps[0].ParameterType != typeof(DriverFactoryRegistry))
continue;
- var args = new object?[ps.Length];
- args[0] = registry; // remaining params (optional loggerFactory, etc.) default to null
+ var args = ps.Select(p => ArgumentFor(p, registry, type.Name)).ToArray();
register.Invoke(null, args);
invoked++;
}
@@ -78,6 +105,29 @@ public sealed class DriverTypeNamesGuardTests
return registry.RegisteredTypes;
}
+ ///
+ /// Supplies one argument for a discovered Register parameter, by type.
+ ///
+ /// The parameter to supply.
+ /// The registry the factories register into.
+ /// The declaring factory type, for the failure message.
+ /// The argument value to pass.
+ private static object? ArgumentFor(ParameterInfo parameter, DriverFactoryRegistry registry, string factoryTypeName)
+ {
+ if (parameter.ParameterType == typeof(DriverFactoryRegistry)) return registry;
+ if (parameter.ParameterType == typeof(ISecretResolver)) return AbsentSecretResolver.Instance;
+
+ // Optional or nullable-reference parameters are safe to omit; the factory is documented to
+ // cope. Anything else is a required dependency this method does not know how to build, and
+ // guessing null would resurface as an opaque ArgumentNullException from inside the factory.
+ if (parameter.IsOptional || !parameter.ParameterType.IsValueType) return null;
+
+ throw new InvalidOperationException(
+ $"{factoryTypeName}.Register requires parameter '{parameter.Name}' of type "
+ + $"{parameter.ParameterType.Name}, which the DriverTypeNames guard does not know how to "
+ + "supply. Add a case to ArgumentFor, or give the parameter a default on the factory.");
+ }
+
/// Reflect every public const string declared on .
private static IReadOnlyCollection DeclaredConstantValues() =>
typeof(DriverTypeNames)