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);
}
}