Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/WorkerExecutableHealthCheckTests.cs
T
Joseph Doherty 61c00d59a9
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m27s
ci / java (push) Successful in 2m13s
ci / portable (push) Failing after 4m25s
feat(gateway): readiness check for the worker executable (#206)
Readiness covered only the auth store, so a gateway whose worker exe was
missing or of the wrong architecture reported ready while every OpenSession
failed. The new ready-tier check worker-executable applies the launcher's own
rule (path resolution, .exe, presence, PE architecture), now shared as
WorkerExecutableValidator.ResolveAndValidate, and reports only the launch
error code (the endpoint is anonymous). The session check stays on the
active tier, so zero sessions keeps readiness green.

The bounded Galaxy SQL check is not in this change: its tier, failure status
and probe cadence need a decision (see the issue).

Tests: WorkerExecutableHealthCheckTests (healthy, missing, wrong architecture,
and the ready-tier registration, which fails without the wiring).
2026-11-06 16:13:09 -05:00

107 lines
4.4 KiB
C#

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Health;
using ZB.MOM.WW.MxGateway.Server;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Diagnostics;
namespace ZB.MOM.WW.MxGateway.Tests.Diagnostics;
/// <summary>Readiness coverage for the worker executable (#206).</summary>
public sealed class WorkerExecutableHealthCheckTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), $"mxgw-workercheck-{Guid.NewGuid():N}");
/// <summary>Initializes a new instance of the <see cref="WorkerExecutableHealthCheckTests"/> class.</summary>
public WorkerExecutableHealthCheckTests() => Directory.CreateDirectory(_directory);
/// <inheritdoc />
public void Dispose() => Directory.Delete(_directory, recursive: true);
/// <summary>A present x86 worker image is healthy.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Healthy_WhenWorkerExecutableIsLaunchable()
{
string path = WritePeStub(machine: 0x014c);
HealthCheckResult result = await CheckAsync(path, WorkerArchitecture.X86);
Assert.Equal(HealthStatus.Healthy, result.Status);
}
/// <summary>A missing worker executable is unhealthy and reports only the error code, never the path.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Unhealthy_WhenWorkerExecutableIsMissing()
{
string path = Path.Combine(_directory, "missing", "ZB.MOM.WW.MxGateway.Worker.exe");
HealthCheckResult result = await CheckAsync(path, WorkerArchitecture.X86);
Assert.Equal(HealthStatus.Unhealthy, result.Status);
Assert.Equal("ExecutableNotFound", result.Data["error_code"]);
Assert.DoesNotContain(_directory, result.Description, StringComparison.OrdinalIgnoreCase);
}
/// <summary>A worker image of the wrong architecture is unhealthy, as the launcher would reject it.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Unhealthy_WhenWorkerExecutableHasWrongArchitecture()
{
string path = WritePeStub(machine: 0x8664);
HealthCheckResult result = await CheckAsync(path, WorkerArchitecture.X86);
Assert.Equal(HealthStatus.Unhealthy, result.Status);
Assert.Equal("InvalidExecutable", result.Data["error_code"]);
}
/// <summary>
/// The gateway registers the check on the ready tier, and the session check stays on the active
/// tier so zero sessions keeps readiness green.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Build_RegistersWorkerExecutableCheckOnReadyTier()
{
await using WebApplication app = GatewayApplication.Build([]);
HealthCheckServiceOptions options =
app.Services.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value;
HealthCheckRegistration worker = Assert.Single(options.Registrations, r => r.Name == "worker-executable");
Assert.Contains(ZbHealthTags.Ready, worker.Tags);
HealthCheckRegistration sessions = Assert.Single(options.Registrations, r => r.Name == "mxaccess-sessions");
Assert.DoesNotContain(ZbHealthTags.Ready, sessions.Tags);
}
private static Task<HealthCheckResult> CheckAsync(string path, WorkerArchitecture architecture)
{
WorkerExecutableHealthCheck check = new(Options.Create(new GatewayOptions
{
Worker = new WorkerOptions
{
ExecutablePath = path,
RequiredArchitecture = architecture,
},
}));
return check.CheckHealthAsync(new HealthCheckContext());
}
private string WritePeStub(ushort machine)
{
string path = Path.Combine(_directory, "ZB.MOM.WW.MxGateway.Worker.exe");
byte[] bytes = new byte[0x100];
bytes[0] = (byte)'M';
bytes[1] = (byte)'Z';
BitConverter.GetBytes(0x80).CopyTo(bytes, 0x3c);
bytes[0x80] = (byte)'P';
bytes[0x81] = (byte)'E';
BitConverter.GetBytes(machine).CopyTo(bytes, 0x84);
File.WriteAllBytes(path, bytes);
return path;
}
}