feat(mesh-phase5): central telemetry dialer client + proto->domain converters
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
+125
@@ -0,0 +1,125 @@
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using Shouldly;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;
|
||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Telemetry;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="TelemetryStreamClient"/> driving a fake generated client (the ctor
|
||||
/// seam), so the pump/normal-shutdown/error-routing behaviour is provable without a live gRPC
|
||||
/// server. The wire itself is covered by the Phase 5 live gate.
|
||||
/// </summary>
|
||||
public sealed class TelemetryStreamClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunAsync_pumps_every_event_to_onEvent()
|
||||
{
|
||||
var events = new[]
|
||||
{
|
||||
new TelemetryEvent { ScriptLog = Script("one") },
|
||||
new TelemetryEvent { ScriptLog = Script("two") },
|
||||
};
|
||||
var fake = new FakeClient(new FakeStreamReader(events));
|
||||
using var sut = new TelemetryStreamClient(fake, "key");
|
||||
|
||||
var received = new List<TelemetryEvent>();
|
||||
Exception? errored = null;
|
||||
|
||||
await sut.RunAsync("corr-1", received.Add, ex => errored = ex, CancellationToken.None);
|
||||
|
||||
received.Count.ShouldBe(2);
|
||||
received[0].ScriptLog.Message.ShouldBe("one");
|
||||
received[1].ScriptLog.Message.ShouldBe("two");
|
||||
errored.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_swallows_rpc_cancelled_without_calling_onError()
|
||||
{
|
||||
var fake = new FakeClient(new FakeStreamReader(
|
||||
[new TelemetryEvent { ScriptLog = Script("one") }],
|
||||
throwAtEnd: new RpcException(new Status(StatusCode.Cancelled, "cancelled"))));
|
||||
using var sut = new TelemetryStreamClient(fake, "key");
|
||||
|
||||
var received = new List<TelemetryEvent>();
|
||||
Exception? errored = null;
|
||||
|
||||
await sut.RunAsync("corr-1", received.Add, ex => errored = ex, CancellationToken.None);
|
||||
|
||||
received.Count.ShouldBe(1);
|
||||
errored.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_routes_real_rpc_failure_to_onError()
|
||||
{
|
||||
var boom = new RpcException(new Status(StatusCode.Unavailable, "node down"));
|
||||
var fake = new FakeClient(new FakeStreamReader([], throwAtEnd: boom));
|
||||
using var sut = new TelemetryStreamClient(fake, "key");
|
||||
|
||||
Exception? errored = null;
|
||||
|
||||
await sut.RunAsync("corr-1", _ => { }, ex => errored = ex, CancellationToken.None);
|
||||
|
||||
errored.ShouldBeSameAs(boom);
|
||||
}
|
||||
|
||||
private static ScriptLog Script(string message) => new()
|
||||
{
|
||||
ScriptId = "s",
|
||||
Level = "Information",
|
||||
Message = message,
|
||||
TimestampUtc = Timestamp.FromDateTime(new DateTime(2026, 7, 23, 0, 0, 0, DateTimeKind.Utc)),
|
||||
};
|
||||
|
||||
/// <summary>Fake generated client whose Subscribe returns a canned server-streaming call.</summary>
|
||||
private sealed class FakeClient : TelemetryStreamService.TelemetryStreamServiceClient
|
||||
{
|
||||
private readonly IAsyncStreamReader<TelemetryEvent> _reader;
|
||||
|
||||
public FakeClient(IAsyncStreamReader<TelemetryEvent> reader) => _reader = reader;
|
||||
|
||||
public override AsyncServerStreamingCall<TelemetryEvent> Subscribe(
|
||||
TelemetryStreamRequest request,
|
||||
Metadata? headers = null,
|
||||
DateTime? deadline = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
new(
|
||||
_reader,
|
||||
Task.FromResult(new Metadata()),
|
||||
() => Status.DefaultSuccess,
|
||||
() => [],
|
||||
() => { });
|
||||
}
|
||||
|
||||
/// <summary>Replays a fixed sequence of events, optionally throwing at the end.</summary>
|
||||
private sealed class FakeStreamReader : IAsyncStreamReader<TelemetryEvent>
|
||||
{
|
||||
private readonly IReadOnlyList<TelemetryEvent> _items;
|
||||
private readonly Exception? _throwAtEnd;
|
||||
private int _index = -1;
|
||||
|
||||
public FakeStreamReader(IReadOnlyList<TelemetryEvent> items, Exception? throwAtEnd = null)
|
||||
{
|
||||
_items = items;
|
||||
_throwAtEnd = throwAtEnd;
|
||||
}
|
||||
|
||||
public TelemetryEvent Current => _items[_index];
|
||||
|
||||
public Task<bool> MoveNext(CancellationToken cancellationToken)
|
||||
{
|
||||
_index++;
|
||||
if (_index < _items.Count)
|
||||
return Task.FromResult(true);
|
||||
|
||||
if (_throwAtEnd is not null)
|
||||
throw _throwAtEnd;
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user