diff --git a/docs/v2/lmx-followups.md b/docs/v2/lmx-followups.md
index 6d6aa82..11ca514 100644
--- a/docs/v2/lmx-followups.md
+++ b/docs/v2/lmx-followups.md
@@ -91,18 +91,22 @@ no single end-to-end smoke test.
subscribes to one of its attributes, writes a value back, and asserts the
write round-tripped through MXAccess. Skip when ArchestrA isn't running.
-## 6. Second driver instance on the same server
+## 6. Second driver instance on the same server — **DONE (PR 32)**
-**Status**: `DriverHost.RegisterAsync` supports multiple drivers; the OPC UA
-server creates one `DriverNodeManager` per driver and isolates their
-subtrees under distinct namespace URIs. Not proven with two active
-`GalaxyProxyDriver` instances pointing at different Galaxies.
+`Server.Tests/MultipleDriverInstancesIntegrationTests.cs` registers two
+drivers with distinct `DriverInstanceId`s on one `DriverHost`, spins up the
+full OPC UA server, and asserts three behaviors: (1) each driver's namespace
+URI (`urn:OtOpcUa:{id}`) resolves to a distinct index in the client's
+NamespaceUris, (2) browsing one subtree returns that driver's folder and
+does NOT leak the other driver's folder, (3) reads route to the correct
+driver — the alpha instance returns 42 while beta returns 99, so a misroute
+would surface at the assertion layer.
-**To do**:
-- Integration test that registers two driver instances, each with a distinct
- `DriverInstanceId` + endpoint in its own session, asserts nodes from both
- appear under the correct subtrees, alarm events land on the correct
- instance's condition nodes.
+Deferred: the alarm-event multi-driver parity case (two drivers each raising
+a `GalaxyAlarmEvent`, assert each condition lands on its owning instance's
+condition node). Alarm tracking already has its own integration test
+(`AlarmSubscription*`); the multi-driver alarm case would need a stub
+`IAlarmSource` that's worth its own focused PR.
## 7. Host-status per-AppEngine granularity → Admin UI dashboard
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/MultipleDriverInstancesIntegrationTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/MultipleDriverInstancesIntegrationTests.cs
new file mode 100644
index 0000000..cd93e14
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/MultipleDriverInstancesIntegrationTests.cs
@@ -0,0 +1,191 @@
+using Microsoft.Extensions.Logging.Abstractions;
+using Opc.Ua;
+using Opc.Ua.Client;
+using Opc.Ua.Configuration;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Core.Hosting;
+using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
+using ZB.MOM.WW.OtOpcUa.Server.Security;
+
+namespace ZB.MOM.WW.OtOpcUa.Server.Tests;
+
+///
+/// Closes LMX follow-up #6 — proves that two instances registered
+/// on the same land in isolated namespaces and their reads
+/// route to the correct driver. The existing
+/// only exercises a single-driver topology; this sibling fixture registers two.
+///
+///
+/// Each driver gets its own namespace URI of the form urn:OtOpcUa:{DriverInstanceId}
+/// (per DriverNodeManager's base-class namespaceUris argument). A client
+/// that browses one namespace must see only that driver's subtree, and a read against a
+/// variable in one namespace must return that driver's value, not the other's — this is
+/// what stops a cross-driver routing regression from going unnoticed when the v1
+/// single-driver code path gets new knobs.
+///
+[Trait("Category", "Integration")]
+public sealed class MultipleDriverInstancesIntegrationTests : IAsyncLifetime
+{
+ private static readonly int Port = 48500 + Random.Shared.Next(0, 99);
+ private readonly string _endpoint = $"opc.tcp://localhost:{Port}/OtOpcUaMultiDriverTest";
+ private readonly string _pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-multi-{Guid.NewGuid():N}");
+
+ private DriverHost _driverHost = null!;
+ private OpcUaApplicationHost _server = null!;
+
+ public async ValueTask InitializeAsync()
+ {
+ _driverHost = new DriverHost();
+ await _driverHost.RegisterAsync(new StubDriver("alpha", folderName: "AlphaFolder", readValue: 42),
+ "{}", CancellationToken.None);
+ await _driverHost.RegisterAsync(new StubDriver("beta", folderName: "BetaFolder", readValue: 99),
+ "{}", CancellationToken.None);
+
+ var options = new OpcUaServerOptions
+ {
+ EndpointUrl = _endpoint,
+ ApplicationName = "OtOpcUaMultiDriverTest",
+ ApplicationUri = "urn:OtOpcUa:Server:MultiDriverTest",
+ PkiStoreRoot = _pkiRoot,
+ AutoAcceptUntrustedClientCertificates = true,
+ };
+
+ _server = new OpcUaApplicationHost(options, _driverHost, new DenyAllUserAuthenticator(),
+ NullLoggerFactory.Instance, NullLogger.Instance);
+ await _server.StartAsync(CancellationToken.None);
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await _server.DisposeAsync();
+ await _driverHost.DisposeAsync();
+ try { Directory.Delete(_pkiRoot, recursive: true); } catch { /* best-effort */ }
+ }
+
+ [Fact]
+ public async Task Both_drivers_register_under_their_own_urn_namespace()
+ {
+ using var session = await OpenSessionAsync();
+
+ var alphaNs = session.NamespaceUris.GetIndex("urn:OtOpcUa:alpha");
+ var betaNs = session.NamespaceUris.GetIndex("urn:OtOpcUa:beta");
+
+ alphaNs.ShouldBeGreaterThanOrEqualTo(0, "DriverNodeManager for 'alpha' must register its namespace URI");
+ betaNs.ShouldBeGreaterThanOrEqualTo(0, "DriverNodeManager for 'beta' must register its namespace URI");
+ alphaNs.ShouldNotBe(betaNs, "each driver owns its own namespace");
+ }
+
+ [Fact]
+ public async Task Each_driver_subtree_exposes_only_its_own_folder()
+ {
+ using var session = await OpenSessionAsync();
+
+ var alphaNs = (ushort)session.NamespaceUris.GetIndex("urn:OtOpcUa:alpha");
+ var betaNs = (ushort)session.NamespaceUris.GetIndex("urn:OtOpcUa:beta");
+
+ var alphaRoot = new NodeId("alpha", alphaNs);
+ session.Browse(null, null, alphaRoot, 0, BrowseDirection.Forward, ReferenceTypeIds.HierarchicalReferences,
+ true, (uint)NodeClass.Object | (uint)NodeClass.Variable, out _, out var alphaRefs);
+ alphaRefs.ShouldContain(r => r.BrowseName.Name == "AlphaFolder",
+ "alpha's subtree must contain alpha's folder");
+ alphaRefs.ShouldNotContain(r => r.BrowseName.Name == "BetaFolder",
+ "alpha's subtree must NOT see beta's folder — cross-driver leak would hide subscription-routing bugs");
+
+ var betaRoot = new NodeId("beta", betaNs);
+ session.Browse(null, null, betaRoot, 0, BrowseDirection.Forward, ReferenceTypeIds.HierarchicalReferences,
+ true, (uint)NodeClass.Object | (uint)NodeClass.Variable, out _, out var betaRefs);
+ betaRefs.ShouldContain(r => r.BrowseName.Name == "BetaFolder");
+ betaRefs.ShouldNotContain(r => r.BrowseName.Name == "AlphaFolder");
+ }
+
+ [Fact]
+ public async Task Reads_route_to_the_correct_driver_by_namespace()
+ {
+ using var session = await OpenSessionAsync();
+
+ var alphaNs = (ushort)session.NamespaceUris.GetIndex("urn:OtOpcUa:alpha");
+ var betaNs = (ushort)session.NamespaceUris.GetIndex("urn:OtOpcUa:beta");
+
+ var alphaValue = session.ReadValue(new NodeId("AlphaFolder.Var1", alphaNs));
+ var betaValue = session.ReadValue(new NodeId("BetaFolder.Var1", betaNs));
+
+ alphaValue.Value.ShouldBe(42, "alpha driver's ReadAsync returns 42 — a misroute would surface as 99");
+ betaValue.Value.ShouldBe(99, "beta driver's ReadAsync returns 99 — a misroute would surface as 42");
+ }
+
+ private async Task OpenSessionAsync()
+ {
+ var cfg = new ApplicationConfiguration
+ {
+ ApplicationName = "OtOpcUaMultiDriverTestClient",
+ ApplicationUri = "urn:OtOpcUa:MultiDriverTestClient",
+ ApplicationType = ApplicationType.Client,
+ SecurityConfiguration = new SecurityConfiguration
+ {
+ ApplicationCertificate = new CertificateIdentifier
+ {
+ StoreType = CertificateStoreType.Directory,
+ StorePath = Path.Combine(_pkiRoot, "client-own"),
+ SubjectName = "CN=OtOpcUaMultiDriverTestClient",
+ },
+ TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-issuers") },
+ TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-trusted") },
+ RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-rejected") },
+ AutoAcceptUntrustedCertificates = true,
+ AddAppCertToTrustedStore = true,
+ },
+ TransportConfigurations = new TransportConfigurationCollection(),
+ TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
+ ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 },
+ };
+ await cfg.Validate(ApplicationType.Client);
+ cfg.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
+
+ var instance = new ApplicationInstance { ApplicationConfiguration = cfg, ApplicationType = ApplicationType.Client };
+ await instance.CheckApplicationInstanceCertificate(true, CertificateFactory.DefaultKeySize);
+
+ var selected = CoreClientUtils.SelectEndpoint(cfg, _endpoint, useSecurity: false);
+ var endpointConfig = EndpointConfiguration.Create(cfg);
+ var configuredEndpoint = new ConfiguredEndpoint(null, selected, endpointConfig);
+
+ return await Session.Create(cfg, configuredEndpoint, false, "OtOpcUaMultiDriverTestClientSession", 60000,
+ new UserIdentity(new AnonymousIdentityToken()), null);
+ }
+
+ ///
+ /// Driver stub that returns a caller-specified folder + variable + read value so two
+ /// instances in the same server can be told apart at the assertion layer.
+ ///
+ private sealed class StubDriver(string driverInstanceId, string folderName, int readValue)
+ : IDriver, ITagDiscovery, IReadable
+ {
+ public string DriverInstanceId => driverInstanceId;
+ public string DriverType => "Stub";
+
+ public Task InitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
+ public Task ReinitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
+ public Task ShutdownAsync(CancellationToken ct) => Task.CompletedTask;
+ public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
+ public long GetMemoryFootprint() => 0;
+ public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask;
+
+ public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken ct)
+ {
+ var folder = builder.Folder(folderName, folderName);
+ folder.Variable("Var1", "Var1", new DriverAttributeInfo(
+ $"{folderName}.Var1", DriverDataType.Int32, false, null, SecurityClassification.FreeAccess, false, IsAlarm: false));
+ return Task.CompletedTask;
+ }
+
+ public Task> ReadAsync(
+ IReadOnlyList fullReferences, CancellationToken cancellationToken)
+ {
+ var now = DateTime.UtcNow;
+ IReadOnlyList result =
+ fullReferences.Select(_ => new DataValueSnapshot(readValue, 0u, now, now)).ToArray();
+ return Task.FromResult(result);
+ }
+ }
+}