using Google.Protobuf.WellKnownTypes; using MxGateway.Client.Cli; using MxGateway.Contracts.Proto; using MxGateway.Contracts.Proto.Galaxy; namespace MxGateway.Client.Tests; /// Tests for the CLI command interface. public sealed class MxGatewayClientCliTests { /// Verifies that the version command prints compiled protocol versions. [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=3", output.ToString()); Assert.Contains("worker-protocol=1", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that the version command with --json flag prints JSON protocol versions. [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\":3", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that the write command builds a write request and prints JSON reply. [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()); } /// Verifies that error output redacts sensitive API key values. [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()); } /// /// Verifies that error output redacts the API key even when it was sourced from /// the --api-key-env environment variable rather than passed via /// --api-key — the documented default credential path. /// [Fact] public async Task RunAsync_ErrorOutput_RedactsApiKey_WhenSourcedFromEnvironmentVariable() { const string environmentVariableName = "MXGATEWAY_TEST_API_KEY_REDACT"; using var output = new StringWriter(); using var error = new StringWriter(); Environment.SetEnvironmentVariable(environmentVariableName, "env-secret-api-key"); try { int exitCode = await MxGatewayClientCli.RunAsync( [ "open-session", "--endpoint", "http://localhost:5000", "--api-key-env", environmentVariableName, ], output, error, _ => throw new InvalidOperationException("boom env-secret-api-key")); Assert.Equal(1, exitCode); Assert.DoesNotContain("env-secret-api-key", error.ToString()); Assert.Contains("[redacted]", error.ToString()); } finally { Environment.SetEnvironmentVariable(environmentVariableName, null); } } /// Verifies that stream-events with max-events limit stops output in non-JSON format. [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()); } /// /// Client.Dotnet-017 regression: a finite-window event collector /// (stream-events --timeout) must exit 0 and emit the events /// that arrived before the timeout fired, instead of propagating the /// timeout-driven as an /// unhandled exception (exit code -532462766). The fix wraps the /// await foreach in a token-aware catch so the cancellation /// ends the foreach gracefully; the aggregated JSON output still runs. /// [Fact] public async Task RunAsync_StreamEvents_WhenTimeoutFiresAfterEvents_EmitsCollectedEventsAndExitsZero() { 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.OnDataChange, WorkerSequence = 2, }); // Park forever after yielding the configured events so the CLI's // --timeout drives the cancellation path. fakeClient.StreamHangAfterEvents = async token => { await Task.Delay(Timeout.InfiniteTimeSpan, token).ConfigureAwait(false); }; int exitCode = await MxGatewayClientCli.RunAsync( [ "stream-events", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--session-id", "session-fixture", "--json", "--max-events", "200", "--timeout", "1s", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); string json = output.ToString(); // Aggregate JSON output must run even though the foreach exited via // cancellation, and it must contain both events that arrived first. Assert.Contains("\"events\"", json); Assert.Contains("\"workerSequence\":\"1\"", json); Assert.Contains("\"workerSequence\":\"2\"", json); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that stream-alarms with --max-events stops output and distinguishes payload cases. [Fact] public async Task RunAsync_StreamAlarms_WithMaxEventsStopsAndDistinguishesPayloadCases() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.AlarmFeedMessages.Add(new AlarmFeedMessage { ActiveAlarm = new ActiveAlarmSnapshot { AlarmFullReference = "Tank01.Level.HiHi" }, }); fakeClient.AlarmFeedMessages.Add(new AlarmFeedMessage { SnapshotComplete = true }); int exitCode = await MxGatewayClientCli.RunAsync( [ "stream-alarms", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--filter-prefix", "Tank01", "--max-events", "1", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); StreamAlarmsRequest request = Assert.Single(fakeClient.StreamAlarmsRequests); Assert.Equal("Tank01", request.AlarmFilterPrefix); string text = output.ToString(); Assert.Contains("active-alarm", text); Assert.Contains("Tank01.Level.HiHi", text); Assert.DoesNotContain("snapshot-complete", text); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that acknowledge-alarm builds a request and prints the JSON reply. [Fact] public async Task RunAsync_AcknowledgeAlarm_BuildsRequestAndPrintsJsonReply() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.AcknowledgeAlarmReplies.Enqueue(new AcknowledgeAlarmReply { CorrelationId = "ack-fixture", ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, Hresult = 0, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "acknowledge-alarm", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--reference", "Tank01.Level.HiHi", "--comment", "ack from cli", "--operator", "operator1", "--json", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); AcknowledgeAlarmRequest request = Assert.Single(fakeClient.AcknowledgeAlarmRequests); Assert.Equal("Tank01.Level.HiHi", request.AlarmFullReference); Assert.Equal("ack from cli", request.Comment); Assert.Equal("operator1", request.OperatorUser); Assert.Contains("ack-fixture", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that smoke command closes opened session when a command fails. [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); } /// Verifies that galaxy-test-connection command prints JSON reply. [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()); } /// Verifies that galaxy-discover command prints hierarchy summary. [Fact] public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply { NextPageToken = "7:1", TotalObjectCount = 2, Objects = { new GalaxyObject { GobjectId = 7, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver", ParentGobjectId = 1, Attributes = { new GalaxyAttribute { AttributeName = "DownloadPath", FullTagReference = "DelmiaReceiver_001.DownloadPath", }, }, }, }, }); fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply { TotalObjectCount = 2, Objects = { new GalaxyObject { GobjectId = 8, TagName = "DelmiaReceiver_002", ContainedName = "DelmiaReceiver", ParentGobjectId = 1, }, }, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "galaxy-discover", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); Assert.Equal(2, fakeClient.GalaxyDiscoverHierarchyRequests.Count); Assert.Equal(5000, fakeClient.GalaxyDiscoverHierarchyRequests[0].PageSize); Assert.Equal("", fakeClient.GalaxyDiscoverHierarchyRequests[0].PageToken); Assert.Equal("7:1", fakeClient.GalaxyDiscoverHierarchyRequests[1].PageToken); string text = output.ToString(); Assert.Contains("objects=2", text); Assert.Contains("DelmiaReceiver_001", text); Assert.Contains("DelmiaReceiver_002", text); Assert.Contains("attributes=1", text); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that galaxy-watch command prints text output for deploy events. [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()); } /// Verifies that galaxy-watch with --json emits one JSON object per event. [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); } /// Verifies that batch mode executes a single no-gateway command and writes the EOR sentinel. [Fact] public async Task RunAsync_Batch_SingleVersionCommand_WritesOutputAndEorSentinel() { using var output = new StringWriter(); using var error = new StringWriter(); using var stdin = new StringReader("version --json\n"); int exitCode = await MxGatewayClientCli.RunAsync( ["batch"], output, error, clientFactory: null, standardInput: stdin); Assert.Equal(0, exitCode); string text = output.ToString(); Assert.Contains("\"gatewayProtocolVersion\"", text); Assert.Contains("__MXGW_BATCH_EOR__", text); // Sentinel must appear after the output, not before. int outputIdx = text.IndexOf("gatewayProtocolVersion", StringComparison.Ordinal); int eorIdx = text.IndexOf("__MXGW_BATCH_EOR__", StringComparison.Ordinal); Assert.True(outputIdx < eorIdx, "EOR sentinel must follow command output."); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that batch mode processes two commands sequentially and writes two EOR sentinels. [Fact] public async Task RunAsync_Batch_TwoVersionCommands_WritesTwoEorSentinels() { using var output = new StringWriter(); using var error = new StringWriter(); // Two commands followed by EOF (end of string). using var stdin = new StringReader("version\nversion --json\n"); int exitCode = await MxGatewayClientCli.RunAsync( ["batch"], output, error, clientFactory: null, standardInput: stdin); Assert.Equal(0, exitCode); string text = output.ToString(); int firstEor = text.IndexOf("__MXGW_BATCH_EOR__", StringComparison.Ordinal); int secondEor = text.IndexOf("__MXGW_BATCH_EOR__", firstEor + 1, StringComparison.Ordinal); Assert.True(firstEor >= 0, "First EOR sentinel must be present."); Assert.True(secondEor > firstEor, "Second EOR sentinel must follow first."); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that batch mode on EOF (empty stdin) exits 0 immediately without writing any sentinel. [Fact] public async Task RunAsync_Batch_EmptyStdin_ExitsZeroWithNoOutput() { using var output = new StringWriter(); using var error = new StringWriter(); using var stdin = new StringReader(string.Empty); int exitCode = await MxGatewayClientCli.RunAsync( ["batch"], output, error, clientFactory: null, standardInput: stdin); Assert.Equal(0, exitCode); Assert.Equal(string.Empty, output.ToString()); Assert.Equal(string.Empty, error.ToString()); } /// /// Verifies that batch mode continues after a command failure and writes the error JSON /// to stdout (not stderr), followed by the EOR sentinel. /// [Fact] public async Task RunAsync_Batch_CommandFailure_WritesErrorJsonToStdoutAndContinues() { using var output = new StringWriter(); using var error = new StringWriter(); // First line: a gateway command with no API key (will fail). // Second line: version (will succeed). using var stdin = new StringReader("open-session --endpoint http://localhost:5000\nversion --json\n"); int exitCode = await MxGatewayClientCli.RunAsync( ["batch"], output, error, clientFactory: _ => throw new InvalidOperationException("injected failure"), standardInput: stdin); Assert.Equal(0, exitCode); string text = output.ToString(); // Error record: the error JSON must be on stdout, not stderr. Assert.Contains("\"error\"", text); Assert.Equal(string.Empty, error.ToString()); // Both records must be present. int firstEor = text.IndexOf("__MXGW_BATCH_EOR__", StringComparison.Ordinal); int secondEor = text.IndexOf("__MXGW_BATCH_EOR__", firstEor + 1, StringComparison.Ordinal); Assert.True(firstEor >= 0, "EOR after failed command must be present."); Assert.True(secondEor > firstEor, "EOR after successful command must follow first EOR."); // Second record must contain the version output. string afterFirstEor = text[(firstEor + "__MXGW_BATCH_EOR__".Length)..]; Assert.Contains("\"gatewayProtocolVersion\"", afterFirstEor); } /// Verifies that batch mode treats an empty (blank) line as EOF and exits 0. [Fact] public async Task RunAsync_Batch_EmptyLine_ExitsZeroAfterPreviousCommands() { using var output = new StringWriter(); using var error = new StringWriter(); // One command, then an empty line (stop signal), then another command that must NOT run. using var stdin = new StringReader("version --json\n\nversion --json\n"); int exitCode = await MxGatewayClientCli.RunAsync( ["batch"], output, error, clientFactory: null, standardInput: stdin); Assert.Equal(0, exitCode); string text = output.ToString(); // Only one EOR sentinel — the second command after the empty line must not execute. int firstEor = text.IndexOf("__MXGW_BATCH_EOR__", StringComparison.Ordinal); int secondEor = text.IndexOf("__MXGW_BATCH_EOR__", firstEor + 1, StringComparison.Ordinal); Assert.True(firstEor >= 0, "One EOR sentinel must be present."); Assert.Equal(-1, secondEor); Assert.Equal(string.Empty, error.ToString()); } /// Fake CLI client for testing. private sealed class FakeCliClient : IMxGatewayCliClient { /// Queue of invoke replies to return. public Queue InvokeReplies { get; } = new(); /// List of received invoke requests. public List InvokeRequests { get; } = []; /// List of received close session requests. public List CloseSessionRequests { get; } = []; /// List of events to yield when streaming. public List Events { get; } = []; /// Exception to throw on invoke, if any. public Exception? InvokeFailure { get; init; } /// /// When set, after yielding all the stream /// awaits the provided handle and then throws /// — used to simulate the /// CLI timeout / Ctrl+C cancellation path (Client.Dotnet-017). /// public Func? StreamHangAfterEvents { get; set; } /// 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; } if (StreamHangAfterEvents is not null) { await StreamHangAfterEvents(cancellationToken).ConfigureAwait(false); } } /// Queue of acknowledge-alarm replies to return. public Queue AcknowledgeAlarmReplies { get; } = new(); /// List of received acknowledge-alarm requests. public List AcknowledgeAlarmRequests { get; } = []; /// List of received stream-alarms requests. public List StreamAlarmsRequests { get; } = []; /// List of alarm feed messages to yield when streaming alarms. public List AlarmFeedMessages { get; } = []; /// public Task AcknowledgeAlarmAsync( AcknowledgeAlarmRequest request, CancellationToken cancellationToken) { AcknowledgeAlarmRequests.Add(request); return Task.FromResult(AcknowledgeAlarmReplies.Dequeue()); } /// public async IAsyncEnumerable StreamAlarmsAsync( StreamAlarmsRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { StreamAlarmsRequests.Add(request); foreach (AlarmFeedMessage feedMessage in AlarmFeedMessages) { cancellationToken.ThrowIfCancellationRequested(); await Task.Yield(); yield return feedMessage; } } /// Galaxy test connection reply to return. public TestConnectionReply GalaxyTestConnectionReply { get; set; } = new() { Ok = true }; /// Galaxy get last deploy time reply to return. public GetLastDeployTimeReply GalaxyGetLastDeployTimeReply { get; set; } = new() { Present = false }; /// Galaxy discover hierarchy reply to return. public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new(); public Queue GalaxyDiscoverHierarchyReplies { get; } = new(); /// List of received galaxy test connection requests. public List GalaxyTestConnectionRequests { get; } = []; /// List of received galaxy get last deploy time requests. public List GalaxyGetLastDeployTimeRequests { get; } = []; /// List of received galaxy discover hierarchy requests. 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( GalaxyDiscoverHierarchyReplies.TryDequeue(out DiscoverHierarchyReply? reply) ? reply : GalaxyDiscoverHierarchyReply); } /// List of received galaxy watch deploy events requests. public List GalaxyWatchDeployEventsRequests { get; } = []; /// List of deploy events to yield when watching. 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; } } } }