diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs
index 3fb5b1a..cb8d6a7 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs
@@ -4,6 +4,7 @@ using MxGateway.Client;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
+using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Health;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
@@ -23,7 +24,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
/// registers under driver-type name
/// "GalaxyMxGateway" so both paths can be live simultaneously during parity testing.
///
-public sealed class GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IDisposable
+public sealed class GalaxyDriver
+ : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IRediscoverable, IHostConnectivityProbe, IDisposable
{
private readonly string _driverInstanceId;
private readonly GalaxyDriverOptions _options;
@@ -41,13 +43,13 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable,
// pump; until then ReadAsync throws NotSupportedException when the reader is null
// (legacy-host backend handles reads in production via DriverNodeManager's
// capability-routing).
- private readonly IGalaxyDataReader? _dataReader;
+ private IGalaxyDataReader? _dataReader;
// PR 4.3 — IGalaxyDataWriter is the test seam for IWritable. Production wraps
// GalaxyMxSession via GatewayGalaxyDataWriter (Write / WriteSecured routing). The
// per-tag SecurityClassification map is populated during ITagDiscovery and consumed
// here at write time.
- private readonly IGalaxyDataWriter? _dataWriter;
+ private IGalaxyDataWriter? _dataWriter;
private readonly System.Collections.Concurrent.ConcurrentDictionary
_securityByFullRef = new(StringComparer.OrdinalIgnoreCase);
@@ -55,11 +57,31 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable,
// out OnDataChange events to every registered driver subscription via the registry's
// reverse map. The subscriber is the test seam — production uses
// GatewayGalaxySubscriber over a connected GalaxyMxSession.
- private readonly IGalaxySubscriber? _subscriber;
+ private IGalaxySubscriber? _subscriber;
private readonly SubscriptionRegistry _subscriptions = new();
private EventPump? _eventPump;
private readonly Lock _pumpLock = new();
+ // PR 4.W — production runtime owned by InitializeAsync. The driver builds these
+ // when it opens a real gw session; tests bypass them by injecting seams via the
+ // internal ctor.
+ private GalaxyMxSession? _ownedMxSession;
+ private MxGatewayClient? _ownedMxClient;
+
+ // PR 4.5 — reconnect supervisor. Reflects in DriverState.Degraded while not Healthy.
+ private ReconnectSupervisor? _supervisor;
+
+ // PR 4.6 — IRediscoverable plumbing.
+ private DeployWatcher? _deployWatcher;
+
+ // PR 4.7 — IHostConnectivityProbe plumbing. The aggregator owns the merged
+ // transport+per-platform view; the forwarder is fed from the supervisor on
+ // transport state transitions; the probe watcher subscribes ScanState attributes
+ // for every discovered platform and pushes value changes to the aggregator.
+ private readonly HostStatusAggregator _hostStatuses = new();
+ private HostConnectivityForwarder? _transportForwarder;
+ private PerPlatformProbeWatcher? _probeWatcher;
+
private DriverHealth _health = new(DriverState.Unknown, null, null);
private bool _disposed;
@@ -70,6 +92,12 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable,
///
public event EventHandler? OnDataChange;
+ /// Fires when the gateway signals a deploy-time change (PR 4.6 DeployWatcher).
+ public event EventHandler? OnRediscoveryNeeded;
+
+ /// Fires when a host transitions Running ↔ Stopped (PR 4.7 HostStatusAggregator).
+ public event EventHandler? OnHostStatusChanged;
+
public GalaxyDriver(
string driverInstanceId,
GalaxyDriverOptions options,
@@ -103,6 +131,9 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable,
_dataReader = dataReader;
_dataWriter = dataWriter;
_subscriber = subscriber;
+
+ // Forward the aggregator's transitions through IHostConnectivityProbe.
+ _hostStatuses.OnHostStatusChanged += (_, args) => OnHostStatusChanged?.Invoke(this, args);
}
///
@@ -115,18 +146,141 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable,
internal GalaxyDriverOptions Options => _options;
///
- public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
+ public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
ObjectDisposedException.ThrowIf(_disposed, this);
- // PR 4.0 skeleton — capability bodies (PRs 4.1-4.7) replace this stub with real
- // MxGatewayClient session opening. The skeleton keeps the IDriver shape buildable
- // so the Galaxy:Backend flag (PR 4.W) can register the driver factory now.
+ // Tests inject seams via the internal ctor; production InitializeAsync builds
+ // the gateway client + session + per-capability runtime components from
+ // GalaxyDriverOptions. When seams are pre-injected we leave them alone (the
+ // test exercises the wired surface without a real gw round-trip).
+ if (_subscriber is null && _dataWriter is null && _hierarchySource is null)
+ {
+ await BuildProductionRuntimeAsync(cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ _logger.LogDebug(
+ "GalaxyDriver {InstanceId} initializing with pre-injected seams — production runtime build skipped",
+ _driverInstanceId);
+ }
+
+ StartDeployWatcher();
_logger.LogInformation(
- "GalaxyDriver {InstanceId} initializing — endpoint={Endpoint} clientName={ClientName} (skeleton; real gateway connect in PR 4.1+)",
+ "GalaxyDriver {InstanceId} initialized — endpoint={Endpoint} clientName={ClientName}",
_driverInstanceId, _options.Gateway.Endpoint, _options.MxAccess.ClientName);
_health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
- return Task.CompletedTask;
+ }
+
+ ///
+ /// Build the production gw client + session + per-capability runtime components
+ /// from _options. Sets up the reconnect supervisor's reopen / replay
+ /// callbacks so a transport drop replays every active subscription on the
+ /// restored session.
+ ///
+ private async Task BuildProductionRuntimeAsync(CancellationToken cancellationToken)
+ {
+ var clientOptions = BuildClientOptions(_options.Gateway);
+ _ownedMxClient = MxGatewayClient.Create(clientOptions);
+ _ownedMxSession = new GalaxyMxSession(_options.MxAccess, _logger);
+ await _ownedMxSession.ConnectAsync(clientOptions, cancellationToken).ConfigureAwait(false);
+
+ _subscriber = new GatewayGalaxySubscriber(_ownedMxSession);
+ _dataWriter = new GatewayGalaxyDataWriter(_ownedMxSession, _options.MxAccess.WriteUserId, _logger);
+
+ _supervisor = new ReconnectSupervisor(
+ reopen: ReopenAsync,
+ replay: ReplayAsync,
+ options: new ReconnectOptions(
+ InitialBackoffOverride: TimeSpan.FromMilliseconds(_options.Reconnect.InitialBackoffMs),
+ MaxBackoffOverride: TimeSpan.FromMilliseconds(_options.Reconnect.MaxBackoffMs)),
+ logger: _logger);
+
+ _transportForwarder = new HostConnectivityForwarder(_options.MxAccess.ClientName, _hostStatuses, _logger);
+ _transportForwarder.SetTransport(HostState.Running); // initial state — we just connected
+
+ _supervisor.StateChanged += OnSupervisorStateChanged;
+
+ _probeWatcher = new PerPlatformProbeWatcher(_subscriber, _hostStatuses, _logger);
+ }
+
+ ///
+ /// Reopen callback for : re-Register the gw session.
+ /// If the session never connected, this is a fresh ConnectAsync; otherwise it's a
+ /// reconnect against the existing client.
+ ///
+ private async Task ReopenAsync(CancellationToken cancellationToken)
+ {
+ if (_ownedMxSession is null) return;
+ var clientOptions = BuildClientOptions(_options.Gateway);
+ await _ownedMxSession.ConnectAsync(clientOptions, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Replay callback. Walks every active subscription's bindings and re-issues
+ /// SubscribeBulk for the tag list. PR 6.x can swap this for the gw's batched
+ /// ReplaySubscriptionsCommand once it ships.
+ ///
+ private async Task ReplayAsync(CancellationToken cancellationToken)
+ {
+ if (_subscriber is null) return;
+ var bindings = _subscriptions.SnapshotAllBindings();
+ if (bindings.Count == 0) return;
+
+ var refs = bindings.Select(b => b.FullReference).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+ await _subscriber.SubscribeBulkAsync(
+ refs, _options.MxAccess.PublishingIntervalMs, cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation(
+ "GalaxyDriver {InstanceId} replay completed — {Count} tags re-subscribed",
+ _driverInstanceId, refs.Length);
+ }
+
+ private void OnSupervisorStateChanged(object? sender, StateTransition transition)
+ {
+ // Reflect supervisor state in DriverHealth + transport forwarder.
+ _health = transition.Next switch
+ {
+ ReconnectSupervisor.State.Healthy => new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null),
+ _ => new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, transition.Cause),
+ };
+ if (_transportForwarder is not null)
+ {
+ var hostState = transition.Next == ReconnectSupervisor.State.Healthy
+ ? HostState.Running
+ : HostState.Stopped;
+ _transportForwarder.SetTransport(hostState);
+ }
+ }
+
+ private static MxGatewayClientOptions BuildClientOptions(GalaxyGatewayOptions gw) => new()
+ {
+ Endpoint = new Uri(gw.Endpoint, UriKind.Absolute),
+ ApiKey = gw.ApiKeySecretRef,
+ UseTls = gw.UseTls,
+ CaCertificatePath = gw.CaCertificatePath,
+ ConnectTimeout = TimeSpan.FromSeconds(gw.ConnectTimeoutSeconds),
+ DefaultCallTimeout = TimeSpan.FromSeconds(gw.DefaultCallTimeoutSeconds),
+ StreamTimeout = gw.StreamTimeoutSeconds > 0 ? TimeSpan.FromSeconds(gw.StreamTimeoutSeconds) : null,
+ };
+
+ private void StartDeployWatcher()
+ {
+ if (!_options.Repository.WatchDeployEvents) return;
+ if (_ownedRepositoryClient is null && _hierarchySource is null) return;
+
+ // Reuse the lazily-built repository client (DiscoverAsync constructs it on demand).
+ // If discovery hasn't run yet, build the client here so the watcher has a target.
+ if (_ownedRepositoryClient is null)
+ {
+ _ownedRepositoryClient = MxGateway.Client.GalaxyRepositoryClient.Create(
+ BuildClientOptions(_options.Gateway));
+ }
+
+ var source = new GatewayGalaxyDeployWatchSource(_ownedRepositoryClient);
+ _deployWatcher = new DeployWatcher(source, _logger);
+ _deployWatcher.OnRediscoveryNeeded += (_, args) => OnRediscoveryNeeded?.Invoke(this, args);
+
+ _ = _deployWatcher.StartAsync(CancellationToken.None);
}
///
@@ -149,7 +303,21 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable,
}
///
- public DriverHealth GetHealth() => _health;
+ public DriverHealth GetHealth()
+ {
+ // Reconnect supervisor wins when degraded — the cached _health reflects the last
+ // successful operation, but ongoing recovery should surface as Degraded.
+ if (_supervisor?.IsDegraded == true)
+ {
+ return new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, _supervisor.LastError);
+ }
+ return _health;
+ }
+
+ // ===== IHostConnectivityProbe (PR 4.7 wire-up) =====
+
+ ///
+ public IReadOnlyList GetHostStatuses() => _hostStatuses.Snapshot();
///
public long GetMemoryFootprint() => 0; // PR 4.4 sets this from SubscriptionRegistry size.
@@ -165,13 +333,25 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable,
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentNullException.ThrowIfNull(builder);
- // PR 4.3 — wrap the supplied builder in a capturing proxy that records each
- // attribute's SecurityClassification into _securityByFullRef. The wrapper is
- // transparent to GalaxyDiscoverer; it forwards every call to the real builder.
+ // PR 4.3 — capture SecurityClassification per attribute. PR 4.W — also refresh
+ // the per-platform probe watcher's membership after discovery so newly-added
+ // $WinPlatform / $AppEngine objects start advising their ScanState attribute.
var capturingBuilder = new SecurityCapturingBuilder(builder, _securityByFullRef);
var source = _hierarchySource ??= BuildDefaultHierarchySource();
var discoverer = new GalaxyDiscoverer(source);
await discoverer.DiscoverAsync(capturingBuilder, cancellationToken).ConfigureAwait(false);
+
+ if (_probeWatcher is not null)
+ {
+ var hierarchy = await source.GetHierarchyAsync(cancellationToken).ConfigureAwait(false);
+ var platforms = hierarchy
+ .Where(o => o.TemplateChain.Any(t =>
+ string.Equals(t, "$WinPlatform", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(t, "$AppEngine", StringComparison.OrdinalIgnoreCase)))
+ .Select(o => o.TagName)
+ .Where(name => !string.IsNullOrEmpty(name));
+ await _probeWatcher.SyncPlatformsAsync(platforms, cancellationToken).ConfigureAwait(false);
+ }
}
private SecurityClassification ResolveSecurity(string fullReference) =>
@@ -324,12 +504,37 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable,
{
if (_eventPump is not null) return _eventPump;
_eventPump = new EventPump(_subscriber!, _subscriptions, _logger);
- _eventPump.OnDataChange += (_, args) => OnDataChange?.Invoke(this, args);
+ _eventPump.OnDataChange += OnPumpDataChange;
_eventPump.Start();
return _eventPump;
}
}
+ ///
+ /// Forwards every fan-out event to the public for
+ /// ISubscribable consumers, AND routes ScanState changes to the per-platform
+ /// probe watcher (PR 4.7) so platform health entries update without the watcher
+ /// consuming the event stream itself.
+ ///
+ private void OnPumpDataChange(object? sender, DataChangeEventArgs args)
+ {
+ OnDataChange?.Invoke(this, args);
+
+ if (_probeWatcher is not null
+ && args.FullReference.EndsWith(PerPlatformProbeWatcher.ProbeSuffix, StringComparison.OrdinalIgnoreCase))
+ {
+ // The probe decoder takes a raw quality byte; recover it from the StatusCode
+ // top byte (Good=0x00 → byte 192, Uncertain=0x40 → byte 64, Bad=0x80 → byte 0).
+ var qualityByte = (byte)((args.Snapshot.StatusCode >> 30) & 0x3) switch
+ {
+ 0 => 192,
+ 1 => 64,
+ _ => 0,
+ };
+ _probeWatcher.OnProbeValueChanged(args.FullReference, args.Snapshot.Value, (byte)qualityByte);
+ }
+ }
+
///
/// Lazily builds the default from
/// _options.Gateway. Owned is disposed in
@@ -362,10 +567,24 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IReadable, IWritable,
if (_disposed) return;
_disposed = true;
+ // Order: stop deploy watcher, supervisor, probe watcher, pump, then sessions and
+ // clients. Each step is best-effort — disposal during a faulted state shouldn't
+ // throw and prevent the rest of the cleanup.
+ try { _deployWatcher?.Dispose(); } catch (Exception ex) { _logger.LogWarning(ex, "DeployWatcher dispose failed"); }
+ try { _supervisor?.Dispose(); } catch (Exception ex) { _logger.LogWarning(ex, "ReconnectSupervisor dispose failed"); }
+ try { _probeWatcher?.Dispose(); } catch (Exception ex) { _logger.LogWarning(ex, "ProbeWatcher dispose failed"); }
+ try { _transportForwarder?.Dispose(); } catch (Exception ex) { _logger.LogWarning(ex, "Transport forwarder dispose failed"); }
+
EventPump? pump;
lock (_pumpLock) { pump = _eventPump; _eventPump = null; }
pump?.DisposeAsync().AsTask().GetAwaiter().GetResult();
+ _ownedMxSession?.DisposeAsync().AsTask().GetAwaiter().GetResult();
+ _ownedMxSession = null;
+
+ _ownedMxClient?.DisposeAsync().AsTask().GetAwaiter().GetResult();
+ _ownedMxClient = null;
+
_ownedRepositoryClient?.DisposeAsync().AsTask().GetAwaiter().GetResult();
_ownedRepositoryClient = null;
_hierarchySource = null;
diff --git a/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs b/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs
index 54d2f3f..ec8da46 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs
@@ -110,7 +110,13 @@ builder.Services.AddSingleton();
builder.Services.AddSingleton(_ =>
{
var registry = new DriverFactoryRegistry();
+ // Both Galaxy backends register side-by-side under distinct DriverType names
+ // ("Galaxy" → legacy GalaxyProxyDriver, "GalaxyMxGateway" → in-process GalaxyDriver
+ // over the gRPC mxaccessgw). The DriverInstance row's DriverType selects between
+ // them at bootstrap time — see lmx_mxgw.md / PR 4.W. Phase 7 retires the legacy
+ // factory once parity tests pin both.
GalaxyProxyDriverFactoryExtensions.Register(registry);
+ ZB.MOM.WW.OtOpcUa.Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry);
FocasDriverFactoryExtensions.Register(registry);
ModbusDriverFactoryExtensions.Register(registry);
AbCipDriverFactoryExtensions.Register(registry);
diff --git a/src/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj b/src/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj
index 5b3b87b..4555272 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj
+++ b/src/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj
@@ -35,6 +35,7 @@
+
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverFactoryTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverFactoryTests.cs
index e0ffc6d..b566bdc 100644
--- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverFactoryTests.cs
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverFactoryTests.cs
@@ -1,7 +1,10 @@
+using MxGateway.Contracts.Proto;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
+using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
+using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests;
@@ -118,7 +121,12 @@ public sealed class GalaxyDriverFactoryTests
[Fact]
public async Task DriverLifecycle_InitializeShutdown_ToggleHealth()
{
- var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
+ // Inject a no-op subscriber seam so InitializeAsync skips BuildProductionRuntimeAsync
+ // (no gw connect attempt). The factory tests only care about the lifecycle health
+ // toggle; real-runtime wire-up is exercised in PR 4.W's BuildProductionRuntime tests.
+ using var driver = new GalaxyDriver(
+ "galaxy-x", BuildOptions(), hierarchySource: null, dataReader: null,
+ dataWriter: null, subscriber: new NoopSubscriber());
driver.GetHealth().State.ShouldBe(DriverState.Unknown);
await driver.InitializeAsync(MinimalConfig, CancellationToken.None);
@@ -135,7 +143,9 @@ public sealed class GalaxyDriverFactoryTests
[Fact]
public async Task ReinitializeAsync_RefreshesHealth()
{
- var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
+ using var driver = new GalaxyDriver(
+ "galaxy-x", BuildOptions(), hierarchySource: null, dataReader: null,
+ dataWriter: null, subscriber: new NoopSubscriber());
await driver.InitializeAsync(MinimalConfig, CancellationToken.None);
var firstStamp = driver.GetHealth().LastSuccessfulRead!.Value;
@@ -147,6 +157,34 @@ public sealed class GalaxyDriverFactoryTests
driver.GetHealth().LastSuccessfulRead!.Value.ShouldBeGreaterThan(firstStamp);
}
+ private static GalaxyDriverOptions BuildOptions() => new(
+ new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
+ new GalaxyMxAccessOptions("OtOpcUa-A"),
+ new GalaxyRepositoryOptions(),
+ new GalaxyReconnectOptions());
+
+ ///
+ /// Minimum-surface seam — enough to satisfy
+ /// 's "skip production-runtime build" branch
+ /// without driving any actual subscribe/event-pump traffic.
+ ///
+ private sealed class NoopSubscriber : IGalaxySubscriber
+ {
+ public Task> SubscribeBulkAsync(
+ IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
+ => Task.FromResult>([]);
+
+ public Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken)
+ => Task.CompletedTask;
+
+ public async IAsyncEnumerable StreamEventsAsync(
+ [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ await Task.CompletedTask;
+ yield break;
+ }
+ }
+
[Fact]
public void Dispose_IsIdempotent_AndShutdownAfterDisposeIsHarmless()
{
@@ -163,4 +201,42 @@ public sealed class GalaxyDriverFactoryTests
await Should.ThrowAsync(() =>
driver.InitializeAsync(MinimalConfig, CancellationToken.None));
}
+
+ [Fact]
+ public void DriverImplementsAllPhase4Capabilities()
+ {
+ // PR 4.W contract pin — every capability the in-process Galaxy driver must
+ // expose for parity with GalaxyProxyDriver. If any of these regress, the
+ // Galaxy:Backend flag flip in PR 7.1 will silently lose surface.
+ var driver = GalaxyDriverFactoryExtensions.CreateInstance("g", MinimalConfig);
+ driver.ShouldBeAssignableTo();
+ driver.ShouldBeAssignableTo();
+ driver.ShouldBeAssignableTo();
+ driver.ShouldBeAssignableTo();
+ driver.ShouldBeAssignableTo();
+ driver.ShouldBeAssignableTo();
+ }
+
+ [Fact]
+ public async Task GetHostStatuses_AfterInitWithSeam_ReturnsEmptySnapshot()
+ {
+ // PR 4.W wire-up assertion: when InitializeAsync skips the production-runtime
+ // build (seam injected), no transport-forwarder or probe-watcher pushes a
+ // status, so the aggregator snapshot is empty. The forwarder + watcher have
+ // their own unit tests in PR 4.7.
+ using var driver = new GalaxyDriver(
+ "g", BuildOptions(), hierarchySource: null, dataReader: null,
+ dataWriter: null, subscriber: new NoopSubscriber());
+ await driver.InitializeAsync(MinimalConfig, CancellationToken.None);
+ driver.GetHostStatuses().ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void DriverType_IsGalaxyMxGateway_NotLegacyGalaxy()
+ {
+ // Must match GalaxyProxyDriver's DriverType ("Galaxy") side-by-side without
+ // collision so DriverInstanceBootstrapper can resolve both factories.
+ var driver = GalaxyDriverFactoryExtensions.CreateInstance("g", MinimalConfig);
+ driver.DriverType.ShouldBe("GalaxyMxGateway");
+ }
}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/ZB.MOM.WW.OtOpcUa.Server.Tests.csproj b/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/ZB.MOM.WW.OtOpcUa.Server.Tests.csproj
index ef2f2c5..28859f9 100644
--- a/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/ZB.MOM.WW.OtOpcUa.Server.Tests.csproj
+++ b/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/ZB.MOM.WW.OtOpcUa.Server.Tests.csproj
@@ -13,7 +13,7 @@
-
+