Add Galaxy repository API and clients

This commit is contained in:
Joseph Doherty
2026-04-29 07:27:00 -04:00
parent 047d875fe6
commit 133c83029b
103 changed files with 22788 additions and 39 deletions
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Options;
using MxGateway.Contracts.Proto;
using MxGateway.Server.Configuration;
using MxGateway.Server.Dashboard;
using MxGateway.Server.Galaxy;
using MxGateway.Server.Metrics;
using MxGateway.Server.Sessions;
using MxGateway.Server.Workers;
@@ -171,6 +172,51 @@ public sealed class DashboardSnapshotServiceTests
Assert.Equal("session-newer", Assert.Single(snapshot.Faults).SessionId);
}
[Fact]
public void GetSnapshot_ProjectsGalaxySummaryFromHierarchyCache()
{
GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with
{
Status = GalaxyCacheStatus.Healthy,
Sequence = 7,
LastQueriedAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z"),
LastSuccessAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z"),
LastDeployTime = DateTimeOffset.Parse("2026-04-28T09:00:00Z"),
Hierarchy =
[
new GalaxyHierarchyRow { GobjectId = 1, TagName = "Pump_001", BrowseName = "Pump_001", CategoryId = 10, IsArea = false, TemplateChain = ["$Pump"] },
new GalaxyHierarchyRow { GobjectId = 2, TagName = "Pump_002", BrowseName = "Pump_002", CategoryId = 10, IsArea = false, TemplateChain = ["$Pump"] },
new GalaxyHierarchyRow { GobjectId = 3, TagName = "Area_A", BrowseName = "Area_A", CategoryId = 13, IsArea = true, TemplateChain = ["$Area"] },
],
Attributes =
[
new GalaxyAttributeRow { GobjectId = 1, AttributeName = "Speed", IsHistorized = true },
new GalaxyAttributeRow { GobjectId = 1, AttributeName = "Status", IsAlarm = true },
],
ObjectCount = 3,
AreaCount = 1,
AttributeCount = 2,
HistorizedAttributeCount = 1,
AlarmAttributeCount = 1,
};
using GatewayMetrics metrics = new();
DashboardSnapshotService service = CreateService(
new SessionRegistry(),
metrics,
galaxyHierarchyCache: new StubGalaxyHierarchyCache(entry));
DashboardSnapshot snapshot = service.GetSnapshot();
Assert.Equal(DashboardGalaxyStatus.Healthy, snapshot.Galaxy.Status);
Assert.Equal(3, snapshot.Galaxy.ObjectCount);
Assert.Equal(1, snapshot.Galaxy.AreaCount);
Assert.Equal(2, snapshot.Galaxy.AttributeCount);
Assert.Equal("$Pump", Assert.Single(snapshot.Galaxy.TopTemplates, t => t.TemplateName == "$Pump").TemplateName);
Assert.Equal(2, snapshot.Galaxy.TopTemplates.First(t => t.TemplateName == "$Pump").InstanceCount);
Assert.Contains(snapshot.Galaxy.ObjectCategories, c => c.CategoryName == "UserDefined" && c.ObjectCount == 2);
Assert.Contains(snapshot.Galaxy.ObjectCategories, c => c.CategoryName == "Area" && c.ObjectCount == 1);
}
[Fact]
public async Task WatchSnapshotsAsync_WhenSubscriberCancels_DisposesCleanly()
{
@@ -200,7 +246,8 @@ public sealed class DashboardSnapshotServiceTests
private static DashboardSnapshotService CreateService(
SessionRegistry registry,
GatewayMetrics metrics,
GatewayOptions? options = null)
GatewayOptions? options = null,
IGalaxyHierarchyCache? galaxyHierarchyCache = null)
{
GatewayOptions resolvedOptions = options ?? new GatewayOptions
{
@@ -215,9 +262,19 @@ public sealed class DashboardSnapshotServiceTests
registry,
metrics,
configurationProvider,
galaxyHierarchyCache ?? new StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry.Empty),
Options.Create(resolvedOptions));
}
private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache
{
public GalaxyHierarchyCacheEntry Current { get; } = current;
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
private static GatewaySession CreateSession(
string sessionId,
string? clientIdentity,
@@ -174,6 +174,7 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests
new MxAccessGrpcRequestValidator(),
mapper,
eventStreamService,
_metrics,
NullLogger<MxAccessGatewayService>.Instance);
}
@@ -1,3 +1,4 @@
using System.Diagnostics.Metrics;
using System.Runtime.CompilerServices;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
@@ -5,6 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using MxGateway.Contracts;
using MxGateway.Contracts.Proto;
using MxGateway.Server.Grpc;
using MxGateway.Server.Metrics;
using MxGateway.Server.Security.Authentication;
using MxGateway.Server.Security.Authorization;
using MxGateway.Server.Sessions;
@@ -163,6 +165,50 @@ public sealed class MxAccessGatewayServiceTests
Assert.Equal("session-1", sessionManager.LastReadEventsSessionId);
}
[Fact]
public async Task StreamEvents_WhenEventIsWritten_RecordsSendDuration()
{
using GatewayMetrics metrics = new();
using MeterListener listener = new();
List<string> families = [];
listener.InstrumentPublished = (instrument, meterListener) =>
{
if (instrument.Meter.Name == GatewayMetrics.MeterName
&& instrument.Name == "mxgateway.events.stream_send.duration")
{
meterListener.EnableMeasurementEvents(instrument);
}
};
listener.SetMeasurementEventCallback<double>(
(instrument, measurement, tags, _) =>
{
if (instrument.Name != "mxgateway.events.stream_send.duration")
{
return;
}
foreach (KeyValuePair<string, object?> tag in tags)
{
if (tag.Key == "family" && tag.Value is string family)
{
families.Add(family);
}
}
});
listener.Start();
FakeSessionManager sessionManager = new();
sessionManager.Events.Add(CreateWorkerEvent("session-1", workerSequence: 2));
MxAccessGatewayService service = CreateService(sessionManager, metrics: metrics);
TestServerStreamWriter<MxEvent> writer = new();
await service.StreamEvents(
new StreamEventsRequest { SessionId = "session-1" },
writer,
new TestServerCallContext());
Assert.Equal([MxEventFamily.OnDataChange.ToString()], families);
}
[Fact]
public async Task CloseSession_WithBlankSessionId_ThrowsInvalidArgument()
{
@@ -178,7 +224,8 @@ public sealed class MxAccessGatewayServiceTests
private static MxAccessGatewayService CreateService(
FakeSessionManager sessionManager,
IGatewayRequestIdentityAccessor? identityAccessor = null)
IGatewayRequestIdentityAccessor? identityAccessor = null,
GatewayMetrics? metrics = null)
{
return new MxAccessGatewayService(
sessionManager,
@@ -186,6 +233,7 @@ public sealed class MxAccessGatewayServiceTests
new MxAccessGrpcRequestValidator(),
new MxAccessGrpcMapper(),
new FakeEventStreamService(sessionManager),
metrics ?? new GatewayMetrics(),
NullLogger<MxAccessGatewayService>.Instance);
}