diff --git a/docs/v2/lmx-followups.md b/docs/v2/lmx-followups.md
index a6f5f90..19d9443 100644
--- a/docs/v2/lmx-followups.md
+++ b/docs/v2/lmx-followups.md
@@ -77,18 +77,36 @@ drive a full OPC UA session with username/password, then read an
`IHostConnectivityProbe`-style "whoami" node to verify the role surfaced).
That needs a test-only address-space node and is a separate PR.
-## 5. Full Galaxy live-service smoke test against the merged v2 stack
+## 5. Full Galaxy live-service smoke test against the merged v2 stack — **IN PROGRESS (PRs 36 + 37)**
-**Status**: Individual pieces have live smoke tests (PR 5 MXAccess, PR 13
-probe manager, PR 14 alarm tracker), but the full loop — OPC UA client →
-`OtOpcUaServer` → `GalaxyProxyDriver` (in-process) → named-pipe to
-Galaxy.Host subprocess → live MXAccess runtime → real Galaxy objects — has
-no single end-to-end smoke test.
+PR 36 shipped the prerequisites helper (`AvevaPrerequisites`) that probes
+every dependency a live smoke test needs and produces actionable skip
+messages.
-**To do**:
-- Test that spawns the full topology, discovers a deployed Galaxy object,
- subscribes to one of its attributes, writes a value back, and asserts the
- write round-tripped through MXAccess. Skip when ArchestrA isn't running.
+PR 37 shipped the live-stack smoke test project structure:
+`tests/Driver.Galaxy.Proxy.Tests/LiveStack/` with `LiveStackFixture` (connects
+to the *already-running* `OtOpcUaGalaxyHost` Windows service via named pipe;
+never spawns the Host process) and `LiveStackSmokeTests` covering:
+
+- Fixture initializes successfully (IPC handshake succeeds end-to-end).
+- Driver reports `DriverState.Healthy` post-handshake.
+- `DiscoverAsync` returns at least one variable from the live Galaxy.
+- `GetHostStatuses` reports at least one Platform/AppEngine host.
+- `ReadAsync` on a discovered variable round-trips through
+ Proxy → Host pipe → MXAccess → back without a BadInternalError.
+
+Shared secret + pipe name resolve from `OTOPCUA_GALAXY_SECRET` /
+`OTOPCUA_GALAXY_PIPE` env vars, falling back to reading the service's
+registry-stored Environment values (requires elevated test host).
+
+**Remaining**:
+- Install + run the `OtOpcUaGalaxyHost` + `OtOpcUa` services on the dev box
+ (`scripts/install/Install-Services.ps1`) so the skip-on-unready tests
+ actually execute and the smoke PR lands green.
+- Subscribe-and-receive-data-change fact (needs a known tag that actually
+ ticks; deferred until operators confirm a scratch tag exists).
+- Write-and-roundtrip fact (needs a test-only UDA or agreed scratch tag
+ so we can't accidentally mutate a process-critical value).
## 6. Second driver instance on the same server — **DONE (PR 32)**
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackConfig.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackConfig.cs
new file mode 100644
index 0000000..9941aaf
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackConfig.cs
@@ -0,0 +1,75 @@
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using Microsoft.Win32;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.LiveStack;
+
+///
+/// Resolves the pipe name + shared secret the live needs
+/// to connect to a running OtOpcUaGalaxyHost Windows service. Two sources are
+/// consulted, first match wins:
+///
+/// - Explicit env vars (OTOPCUA_GALAXY_PIPE, OTOPCUA_GALAXY_SECRET) — lets CI / benchwork override.
+/// - The service's per-process Environment registry values under
+/// HKLM\SYSTEM\CurrentControlSet\Services\OtOpcUaGalaxyHost — what
+/// Install-Services.ps1 writes at install time. Requires the test to run as a
+/// principal with read access to that registry key (typically Administrators).
+///
+///
+///
+/// Explicitly NOT baked-in-to-source: the shared secret is rotated per install (the
+/// installer generates 32 random bytes and stores the base64 string). A hard-coded secret
+/// in tests would diverge from production the moment someone re-installed the service.
+///
+public sealed record LiveStackConfig(string PipeName, string SharedSecret, string? Source)
+{
+ public const string EnvPipeName = "OTOPCUA_GALAXY_PIPE";
+ public const string EnvSharedSecret = "OTOPCUA_GALAXY_SECRET";
+ public const string ServiceRegistryKey =
+ @"SYSTEM\CurrentControlSet\Services\OtOpcUaGalaxyHost";
+ public const string DefaultPipeName = "OtOpcUaGalaxy";
+
+ public static LiveStackConfig? Resolve()
+ {
+ var envPipe = Environment.GetEnvironmentVariable(EnvPipeName);
+ var envSecret = Environment.GetEnvironmentVariable(EnvSharedSecret);
+ if (!string.IsNullOrWhiteSpace(envPipe) && !string.IsNullOrWhiteSpace(envSecret))
+ return new LiveStackConfig(envPipe, envSecret, "env vars");
+
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return null;
+
+ return FromServiceRegistry();
+ }
+
+ [SupportedOSPlatform("windows")]
+ private static LiveStackConfig? FromServiceRegistry()
+ {
+ try
+ {
+ using var key = Registry.LocalMachine.OpenSubKey(ServiceRegistryKey);
+ if (key is null) return null;
+ var env = key.GetValue("Environment") as string[];
+ if (env is null || env.Length == 0) return null;
+
+ string? pipe = null, secret = null;
+ foreach (var line in env)
+ {
+ var eq = line.IndexOf('=');
+ if (eq <= 0) continue;
+ var name = line[..eq];
+ var value = line[(eq + 1)..];
+ if (name.Equals(EnvPipeName, StringComparison.OrdinalIgnoreCase)) pipe = value;
+ else if (name.Equals(EnvSharedSecret, StringComparison.OrdinalIgnoreCase)) secret = value;
+ }
+
+ if (string.IsNullOrWhiteSpace(secret)) return null;
+ return new LiveStackConfig(pipe ?? DefaultPipeName, secret, "service registry");
+ }
+ catch
+ {
+ // Access denied / key missing / malformed — caller gets null and surfaces a Skip.
+ return null;
+ }
+ }
+}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackFixture.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackFixture.cs
new file mode 100644
index 0000000..7199c22
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackFixture.cs
@@ -0,0 +1,120 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.LiveStack;
+
+///
+/// Connects a single to the already-running
+/// OtOpcUaGalaxyHost Windows service for the lifetime of a test class. Uses
+/// to decide whether to proceed; on failure,
+/// is populated and each test calls
+/// to translate that into Assert.Skip.
+///
+///
+///
+/// Does NOT spawn the Host process. Production deploys OtOpcUaGalaxyHost
+/// as a standalone Windows service — spawning a second instance from a test would
+/// bypass the COM-apartment + service-account setup and fail differently than
+/// production (see project_galaxy_host_service.md memory).
+///
+///
+/// Shared-secret handling: read from — env vars
+/// first, then the service's registry-stored Environment values. Requires
+/// the test process to have read access to
+/// HKLM\SYSTEM\CurrentControlSet\Services\OtOpcUaGalaxyHost; on a dev box
+/// that typically means running the test host elevated, or exporting
+/// OTOPCUA_GALAXY_SECRET out-of-band.
+///
+///
+public sealed class LiveStackFixture : IAsyncLifetime
+{
+ public GalaxyProxyDriver? Driver { get; private set; }
+
+ public string? SkipReason { get; private set; }
+
+ public PrerequisiteReport? PrerequisiteReport { get; private set; }
+
+ public LiveStackConfig? Config { get; private set; }
+
+ public async ValueTask InitializeAsync()
+ {
+ // 1. AVEVA + OtOpcUa service state — actionable diagnostic if anything is missing.
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+ PrerequisiteReport = await AvevaPrerequisites.CheckAllAsync(
+ new AvevaPrerequisites.Options { CheckGalaxyHostPipe = true, CheckHistorian = false },
+ cts.Token);
+
+ if (!PrerequisiteReport.IsLivetestReady)
+ {
+ SkipReason = PrerequisiteReport.SkipReason;
+ return;
+ }
+
+ // 2. Secret / pipe-name resolution. If the service is running but we can't discover its
+ // env vars from registry (non-elevated test host), a clear message beats a silent
+ // connect-rejected failure 10 seconds later.
+ Config = LiveStackConfig.Resolve();
+ if (Config is null)
+ {
+ SkipReason =
+ $"Cannot resolve shared secret. Set {LiveStackConfig.EnvSharedSecret} (and optionally " +
+ $"{LiveStackConfig.EnvPipeName}) in the environment, or run the test host elevated so it " +
+ $"can read HKLM\\{LiveStackConfig.ServiceRegistryKey}\\Environment.";
+ return;
+ }
+
+ // 3. Connect. InitializeAsync does the pipe connect + handshake; a 5-second
+ // ConnectTimeout gives enough headroom for a service that just started.
+ Driver = new GalaxyProxyDriver(new GalaxyProxyOptions
+ {
+ DriverInstanceId = "live-stack-smoke",
+ PipeName = Config.PipeName,
+ SharedSecret = Config.SharedSecret,
+ ConnectTimeout = TimeSpan.FromSeconds(5),
+ });
+
+ try
+ {
+ await Driver.InitializeAsync(driverConfigJson: "{}", CancellationToken.None);
+ }
+ catch (Exception ex)
+ {
+ SkipReason =
+ $"Connected to named pipe '{Config.PipeName}' but GalaxyProxyDriver.InitializeAsync failed: " +
+ $"{ex.GetType().Name}: {ex.Message}. Common causes: shared secret mismatch (rotated after last install), " +
+ $"service account SID not in pipe ACL (installer sets OTOPCUA_ALLOWED_SID to the service account — " +
+ $"test must run as that user), or Host's backend couldn't connect to ZB.";
+ Driver.Dispose();
+ Driver = null;
+ return;
+ }
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (Driver is not null)
+ {
+ try { await Driver.ShutdownAsync(CancellationToken.None); } catch { /* best-effort */ }
+ Driver.Dispose();
+ }
+ }
+
+ ///
+ /// Translate into Assert.Skip. Tests call this at the
+ /// top of every fact so a fixture init failure shows up as a cleanly-skipped test with
+ /// the full prerequisites report, not a cascading NullReferenceException on
+ /// .
+ ///
+ public void SkipIfUnavailable()
+ {
+ if (SkipReason is not null) Assert.Skip(SkipReason);
+ }
+}
+
+[CollectionDefinition(Name)]
+public sealed class LiveStackCollection : ICollectionFixture
+{
+ public const string Name = "LiveStack";
+}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackSmokeTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackSmokeTests.cs
new file mode 100644
index 0000000..18fd429
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackSmokeTests.cs
@@ -0,0 +1,147 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.LiveStack;
+
+///
+/// End-to-end smoke against the installed OtOpcUaGalaxyHost Windows service.
+/// Closes LMX follow-up #5 — exercises the full topology:
+/// in-process → named-pipe IPC → OtOpcUaGalaxyHost service → MxAccessGalaxyBackend →
+/// live MXAccess runtime → real Galaxy objects + attributes.
+///
+///
+///
+/// Preconditions (all checked by , surfaced via
+/// Assert.Skip when missing):
+///
+///
+/// - AVEVA System Platform installed + Platform deployed.
+/// - aaBootstrap / aaGR / NmxSvc / MSSQLSERVER running.
+/// - MXAccess COM server registered.
+/// - ZB database exists with at least one deployed gobject.
+/// - OtOpcUaGalaxyHost service installed + running (named pipe accepting connections).
+/// - Shared secret discoverable via OTOPCUA_GALAXY_SECRET env var or the
+/// service's registry Environment values (test host typically needs to be elevated
+/// to read the latter).
+/// - Test process runs as the account listed in the service's pipe ACL
+/// (OTOPCUA_ALLOWED_SID, typically the service account per decision #76).
+///
+///
+/// Tests here are deliberately read-only. Writes against live Galaxy attributes are a
+/// separate concern — they need a test-only UDA or an agreed scratch tag so they can't
+/// accidentally mutate a process-critical value. Adding a write test is a follow-up
+/// PR that reuses this fixture.
+///
+///
+[Trait("Category", "LiveGalaxy")]
+[Collection(LiveStackCollection.Name)]
+public sealed class LiveStackSmokeTests(LiveStackFixture fixture)
+{
+ [Fact]
+ public void Fixture_initialized_successfully()
+ {
+ fixture.SkipIfUnavailable();
+ // If the fixture init succeeded, Driver is non-null and InitializeAsync completed.
+ // This is the cheapest possible assertion that the IPC handshake worked end-to-end;
+ // every other test in this class depends on it.
+ fixture.Driver.ShouldNotBeNull();
+ fixture.Config.ShouldNotBeNull();
+ fixture.PrerequisiteReport.ShouldNotBeNull();
+ fixture.PrerequisiteReport!.IsLivetestReady.ShouldBeTrue(fixture.PrerequisiteReport.SkipReason);
+ }
+
+ [Fact]
+ public void Driver_reports_Healthy_after_IPC_handshake()
+ {
+ fixture.SkipIfUnavailable();
+ var health = fixture.Driver!.GetHealth();
+ health.State.ShouldBe(DriverState.Healthy,
+ $"Expected Healthy after successful IPC connect; Reason={health.LastError}");
+ }
+
+ [Fact]
+ public async Task DiscoverAsync_returns_at_least_one_variable_from_live_galaxy()
+ {
+ fixture.SkipIfUnavailable();
+ var builder = new CapturingAddressSpaceBuilder();
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ await fixture.Driver!.DiscoverAsync(builder, cts.Token);
+
+ builder.Variables.Count.ShouldBeGreaterThan(0,
+ "Live Galaxy has > 0 deployed objects per the prereq check — at least one variable must be discovered. " +
+ "Zero usually means the Host couldn't read ZB (check OTOPCUA_GALAXY_ZB_CONN in the service Environment).");
+
+ // Every discovered attribute must carry a non-empty FullName so the OPC UA server can
+ // route reads/writes back. Regression guard — PR 19 normalized this across drivers.
+ builder.Variables.ShouldAllBe(v => !string.IsNullOrEmpty(v.AttributeInfo.FullName));
+ }
+
+ [Fact]
+ public void GetHostStatuses_reports_at_least_one_platform()
+ {
+ fixture.SkipIfUnavailable();
+ var statuses = fixture.Driver!.GetHostStatuses();
+ statuses.Count.ShouldBeGreaterThan(0,
+ "Live Galaxy must report at least one Platform/AppEngine host via IHostConnectivityProbe. " +
+ "Zero means the Host's probe loop hasn't completed its first tick or the Platform isn't deployed locally.");
+
+ // Host names are driver-opaque to the Core but non-empty by contract.
+ statuses.ShouldAllBe(h => !string.IsNullOrEmpty(h.HostName));
+ }
+
+ [Fact]
+ public async Task Can_read_a_discovered_variable_from_live_galaxy()
+ {
+ fixture.SkipIfUnavailable();
+ var builder = new CapturingAddressSpaceBuilder();
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ await fixture.Driver!.DiscoverAsync(builder, cts.Token);
+ builder.Variables.Count.ShouldBeGreaterThan(0);
+
+ // Pick the first discovered variable. Read-only smoke — we don't assert on Value,
+ // only that a ReadAsync round-trip through Proxy → Host pipe → MXAccess → back
+ // returns a snapshot with a non-BadInternalError status. Galaxy attributes default to
+ // Uncertain quality until the Engine's first scan publishes them, which is fine here.
+ var full = builder.Variables[0].AttributeInfo.FullName;
+ var snapshots = await fixture.Driver!.ReadAsync([full], cts.Token);
+
+ snapshots.Count.ShouldBe(1);
+ var snap = snapshots[0];
+ snap.StatusCode.ShouldNotBe(0x80020000u,
+ $"Read returned BadInternalError for {full} — the Host couldn't fulfil the request. " +
+ $"Investigate: the Host service's logs at {System.Environment.GetFolderPath(System.Environment.SpecialFolder.CommonApplicationData)}\\OtOpcUa\\Galaxy\\logs.");
+ }
+
+ ///
+ /// Minimal implementation that captures every
+ /// Variable() call into a flat list so tests can inspect what discovery produced
+ /// without running the full OPC UA node-manager stack.
+ ///
+ private sealed class CapturingAddressSpaceBuilder : IAddressSpaceBuilder
+ {
+ public List<(string BrowseName, DriverAttributeInfo AttributeInfo)> Variables { get; } = [];
+
+ public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;
+ public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
+ {
+ Variables.Add((browseName, attributeInfo));
+ return new NoopHandle(attributeInfo.FullName);
+ }
+ public void AddProperty(string browseName, DriverDataType dataType, object? value) { }
+
+ private sealed class NoopHandle(string fullReference) : IVariableHandle
+ {
+ public string FullReference { get; } = fullReference;
+ public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NoopSink();
+ private sealed class NoopSink : IAlarmConditionSink
+ {
+ public void OnTransition(AlarmEventArgs args) { }
+ }
+ }
+ }
+}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.csproj b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.csproj
index f90149b..25fac36 100644
--- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.csproj
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.csproj
@@ -22,6 +22,7 @@
+