using Google.Protobuf.WellKnownTypes; using MxGateway.Client.Cli; using MxGateway.Contracts.Proto; using MxGateway.Contracts.Proto.Galaxy; namespace MxGateway.Client.Tests; public sealed class MxGatewayClientCliTests { [Fact] public void Run_Version_PrintsCompiledProtocolVersions() { using var output = new StringWriter(); using var error = new StringWriter(); var exitCode = MxGatewayClientCli.Run(["version"], output, error); Assert.Equal(0, exitCode); Assert.Contains("gateway-protocol=1", output.ToString()); Assert.Contains("worker-protocol=1", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } [Fact] public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions() { using var output = new StringWriter(); using var error = new StringWriter(); int exitCode = await MxGatewayClientCli.RunAsync(["version", "--json"], output, error); Assert.Equal(0, exitCode); Assert.Contains("\"gatewayProtocolVersion\":1", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } [Fact] public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.InvokeReplies.Enqueue(new MxCommandReply { SessionId = "session-fixture", Kind = MxCommandKind.Write, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "write", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--session-id", "session-fixture", "--server-handle", "12", "--item-handle", "34", "--type", "int32", "--value", "123", "--json", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); MxCommandRequest request = Assert.Single(fakeClient.InvokeRequests); Assert.Equal(MxCommandKind.Write, request.Command.Kind); Assert.Equal(123, request.Command.Write.Value.Int32Value); Assert.Contains("MX_COMMAND_KIND_WRITE", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } [Fact] public async Task RunAsync_ErrorOutput_RedactsApiKey() { using var output = new StringWriter(); using var error = new StringWriter(); int exitCode = await MxGatewayClientCli.RunAsync( [ "open-session", "--endpoint", "http://localhost:5000", "--api-key", "secret-api-key", ], output, error, _ => throw new InvalidOperationException("boom secret-api-key")); Assert.Equal(1, exitCode); Assert.DoesNotContain("secret-api-key", error.ToString()); Assert.Contains("[redacted]", error.ToString()); } [Fact] public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.Events.Add(new MxEvent { SessionId = "session-fixture", Family = MxEventFamily.OnDataChange, WorkerSequence = 1, }); fakeClient.Events.Add(new MxEvent { SessionId = "session-fixture", Family = MxEventFamily.OnWriteComplete, WorkerSequence = 2, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "stream-events", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--session-id", "session-fixture", "--max-events", "1", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); Assert.Contains("workerSequence", output.ToString()); Assert.DoesNotContain("ON_WRITE_COMPLETE", output.ToString()); } [Fact] public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new() { InvokeFailure = new InvalidOperationException("register failed"), }; int exitCode = await MxGatewayClientCli.RunAsync( [ "smoke", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--item", "Area001.Pump001.Speed", "--json", ], output, error, _ => fakeClient); Assert.Equal(1, exitCode); CloseSessionRequest closeRequest = Assert.Single(fakeClient.CloseSessionRequests); Assert.Equal("session-fixture", closeRequest.SessionId); } [Fact] public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new() { GalaxyTestConnectionReply = new TestConnectionReply { Ok = true }, }; int exitCode = await MxGatewayClientCli.RunAsync( [ "galaxy-test-connection", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--json", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); Assert.Single(fakeClient.GalaxyTestConnectionRequests); Assert.Contains("\"ok\": true", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } [Fact] public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.GalaxyDiscoverHierarchyReply = new DiscoverHierarchyReply { Objects = { new GalaxyObject { GobjectId = 7, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver", ParentGobjectId = 1, Attributes = { new GalaxyAttribute { AttributeName = "DownloadPath", FullTagReference = "DelmiaReceiver_001.DownloadPath", }, }, }, }, }; int exitCode = await MxGatewayClientCli.RunAsync( [ "galaxy-discover", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); Assert.Single(fakeClient.GalaxyDiscoverHierarchyRequests); string text = output.ToString(); Assert.Contains("objects=1", text); Assert.Contains("DelmiaReceiver_001", text); Assert.Contains("attributes=1", text); Assert.Equal(string.Empty, error.ToString()); } [Fact] public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); DateTime deploy = new(2026, 4, 28, 14, 30, 0, DateTimeKind.Utc); fakeClient.GalaxyDeployEvents.Add(new DeployEvent { Sequence = 1, ObservedAt = Timestamp.FromDateTime(deploy), TimeOfLastDeploy = Timestamp.FromDateTime(deploy), TimeOfLastDeployPresent = true, ObjectCount = 5, AttributeCount = 17, }); fakeClient.GalaxyDeployEvents.Add(new DeployEvent { Sequence = 2, ObservedAt = Timestamp.FromDateTime(deploy.AddSeconds(30)), TimeOfLastDeploy = Timestamp.FromDateTime(deploy.AddSeconds(30)), TimeOfLastDeployPresent = true, ObjectCount = 6, AttributeCount = 18, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "galaxy-watch", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--last-seen-deploy-time", "2026-04-28T14:00:00Z", "--max-events", "2", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); WatchDeployEventsRequest request = Assert.Single(fakeClient.GalaxyWatchDeployEventsRequests); Assert.NotNull(request.LastSeenDeployTime); string text = output.ToString(); Assert.Contains("sequence=1", text); Assert.Contains("sequence=2", text); Assert.Contains("objects=5", text); Assert.Contains("attributes=18", text); Assert.Equal(string.Empty, error.ToString()); } [Fact] public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.GalaxyDeployEvents.Add(new DeployEvent { Sequence = 42, ObjectCount = 99, AttributeCount = 1024, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "galaxy-watch", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--max-events", "1", "--json", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); string text = output.ToString(); Assert.Contains("\"sequence\": \"42\"", text); Assert.Contains("\"objectCount\": 99", text); } private sealed class FakeCliClient : IMxGatewayCliClient { public Queue InvokeReplies { get; } = new(); public List InvokeRequests { get; } = []; public List CloseSessionRequests { get; } = []; public List Events { get; } = []; public Exception? InvokeFailure { get; init; } public ValueTask DisposeAsync() { return ValueTask.CompletedTask; } public Task OpenSessionAsync( OpenSessionRequest request, CancellationToken cancellationToken) { return Task.FromResult(new OpenSessionReply { SessionId = "session-fixture", ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, GatewayProtocolVersion = 1, WorkerProtocolVersion = 1, }); } public Task CloseSessionAsync( CloseSessionRequest request, CancellationToken cancellationToken) { CloseSessionRequests.Add(request); return Task.FromResult(new CloseSessionReply { SessionId = request.SessionId, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, FinalState = SessionState.Closed, }); } public Task InvokeAsync( MxCommandRequest request, CancellationToken cancellationToken) { InvokeRequests.Add(request); if (InvokeFailure is not null) { throw InvokeFailure; } return Task.FromResult(InvokeReplies.Dequeue()); } public async IAsyncEnumerable StreamEventsAsync( StreamEventsRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { foreach (MxEvent gatewayEvent in Events) { cancellationToken.ThrowIfCancellationRequested(); await Task.Yield(); yield return gatewayEvent; } } public TestConnectionReply GalaxyTestConnectionReply { get; set; } = new() { Ok = true }; public GetLastDeployTimeReply GalaxyGetLastDeployTimeReply { get; set; } = new() { Present = false }; public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new(); public List GalaxyTestConnectionRequests { get; } = []; public List GalaxyGetLastDeployTimeRequests { get; } = []; public List GalaxyDiscoverHierarchyRequests { get; } = []; public Task GalaxyTestConnectionAsync( TestConnectionRequest request, CancellationToken cancellationToken) { GalaxyTestConnectionRequests.Add(request); return Task.FromResult(GalaxyTestConnectionReply); } public Task GalaxyGetLastDeployTimeAsync( GetLastDeployTimeRequest request, CancellationToken cancellationToken) { GalaxyGetLastDeployTimeRequests.Add(request); return Task.FromResult(GalaxyGetLastDeployTimeReply); } public Task GalaxyDiscoverHierarchyAsync( DiscoverHierarchyRequest request, CancellationToken cancellationToken) { GalaxyDiscoverHierarchyRequests.Add(request); return Task.FromResult(GalaxyDiscoverHierarchyReply); } public List GalaxyWatchDeployEventsRequests { get; } = []; public List GalaxyDeployEvents { get; } = []; public async IAsyncEnumerable GalaxyWatchDeployEventsAsync( WatchDeployEventsRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { GalaxyWatchDeployEventsRequests.Add(request); foreach (DeployEvent deployEvent in GalaxyDeployEvents) { cancellationToken.ThrowIfCancellationRequested(); await Task.Yield(); yield return deployEvent; } } } }