using System.Diagnostics; using System.Reflection; using System.Security.Principal; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Ipc; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests; /// /// The honest cross-FX parity test — spawns the actual OtOpcUa.Driver.Galaxy.Host.exe /// subprocess (net48 x86), the Proxy connects via real named pipe, exercises Discover /// against the live Galaxy ZB DB, and asserts gobjects come back. This is the production /// deployment shape (Tier C: separate process, IPC over named pipe, Proxy in the .NET 10 /// server process). Skipped when the Host EXE isn't built or Galaxy is unreachable. /// [Trait("Category", "ProcessSpawnParity")] public sealed class HostSubprocessParityTests : IDisposable { private Process? _hostProcess; public void Dispose() { if (_hostProcess is not null && !_hostProcess.HasExited) { try { _hostProcess.Kill(entireProcessTree: true); } catch { /* ignore */ } try { _hostProcess.WaitForExit(5_000); } catch { /* ignore */ } } _hostProcess?.Dispose(); } private static string? FindHostExe() { // The test assembly lives at tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/bin/Debug/net10.0/. // The Host EXE lives at src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/bin/Debug/net48/. var asmDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; var solutionRoot = asmDir; for (var i = 0; i < 8 && solutionRoot is not null; i++) { if (File.Exists(Path.Combine(solutionRoot, "ZB.MOM.WW.OtOpcUa.slnx"))) break; solutionRoot = Path.GetDirectoryName(solutionRoot); } if (solutionRoot is null) return null; var candidate = Path.Combine(solutionRoot, "src", "ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host", "bin", "Debug", "net48", "OtOpcUa.Driver.Galaxy.Host.exe"); return File.Exists(candidate) ? candidate : null; } private static bool IsAdministrator() { if (!OperatingSystem.IsWindows()) return false; using var identity = WindowsIdentity.GetCurrent(); return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); } private static async Task ZbReachableAsync() { try { using var client = new System.Net.Sockets.TcpClient(); var task = client.ConnectAsync("localhost", 1433); return await Task.WhenAny(task, Task.Delay(1_500)) == task && client.Connected; } catch { return false; } } [Fact] public async Task Spawned_Host_in_db_mode_lets_Proxy_Discover_real_Galaxy_gobjects() { if (!OperatingSystem.IsWindows() || IsAdministrator()) return; if (!await ZbReachableAsync()) return; var hostExe = FindHostExe(); if (hostExe is null) return; // skip when the Host hasn't been built using var identity = WindowsIdentity.GetCurrent(); var sid = identity.User!; var pipeName = $"OtOpcUaGalaxyParity-{Guid.NewGuid():N}"; const string secret = "parity-secret"; var psi = new ProcessStartInfo(hostExe) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, EnvironmentVariables = { ["OTOPCUA_GALAXY_PIPE"] = pipeName, ["OTOPCUA_ALLOWED_SID"] = sid.Value, ["OTOPCUA_GALAXY_SECRET"] = secret, ["OTOPCUA_GALAXY_BACKEND"] = "db", // SQL-only — doesn't need MXAccess ["OTOPCUA_GALAXY_ZB_CONN"] = "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;", }, }; _hostProcess = Process.Start(psi) ?? throw new InvalidOperationException("Failed to spawn Galaxy.Host"); // Wait for the pipe to come up — the Host's PipeServer takes ~100ms to bind. await Task.Delay(2_000); await using var client = await GalaxyIpcClient.ConnectAsync( pipeName, secret, TimeSpan.FromSeconds(5), CancellationToken.None); var sessionResp = await client.CallAsync( MessageKind.OpenSessionRequest, new OpenSessionRequest { DriverInstanceId = "parity", DriverConfigJson = "{}" }, MessageKind.OpenSessionResponse, CancellationToken.None); sessionResp.Success.ShouldBeTrue(sessionResp.Error); var discoverResp = await client.CallAsync( MessageKind.DiscoverHierarchyRequest, new DiscoverHierarchyRequest { SessionId = sessionResp.SessionId }, MessageKind.DiscoverHierarchyResponse, CancellationToken.None); discoverResp.Success.ShouldBeTrue(discoverResp.Error); discoverResp.Objects.Length.ShouldBeGreaterThan(0, "live Galaxy ZB has at least one deployed gobject"); await client.SendOneWayAsync(MessageKind.CloseSessionRequest, new CloseSessionRequest { SessionId = sessionResp.SessionId }, CancellationToken.None); } }