fix(driver-historian-wonderware-client): resolve Medium code-review finding (Driver.Historian.Wonderware.Client-009)

Add six previously-missing edge-case tests to WonderwareHistorianClientTests:
(2) WriteBatchAsync transport-drop catch path returns RetryPlease for all events;
(3) InvokeAsync second-attempt-also-fails propagates the exception;
(4) stalled sidecar fires OperationCanceledException within CallTimeout;
(5) HistoryAggregateType.Total throws NotSupportedException via ReadProcessedAsync;
(6) sidecar wrong-MessageKind reply throws InvalidDataException.

Extend FakeSidecarServer with DisconnectBeforeReply, ReplyWithWrongKind, and
StallAfterRequest test knobs to support these scenarios.

Add ContractsWireParityTests.cs (11 tests) to pin the MessagePack byte layout,
round-trip correctness, MessageKind enum values, and Framing constants — catching
silent [Key] index drift between the client and sidecar mirror copies without
requiring a cross-TFM (net10 vs net48) project reference.

Test count grew from 11 to 27; all 27 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 09:26:56 -04:00
parent 03c2028669
commit 1c6db86631
4 changed files with 399 additions and 3 deletions

View File

@@ -26,6 +26,26 @@ internal sealed class FakeSidecarServer : IAsyncDisposable
/// <summary>Force-disconnect the next accepted client mid-call to exercise reconnect.</summary>
public bool DisconnectAfterHandshake { get; set; }
/// <summary>
/// Drop the connection after the handshake but before replying to any non-Hello request.
/// Armed for every connection until reset. Used to exercise the WriteBatchAsync catch
/// path and the second-attempt-also-fails propagation path.
/// </summary>
public bool DisconnectBeforeReply { get; set; }
/// <summary>
/// Reply to the first non-Hello request with this kind instead of the expected kind,
/// to exercise <see cref="System.IO.InvalidDataException"/> detection in ExchangeAsync.
/// Reset to null after the first mis-routed reply.
/// </summary>
public MessageKind? ReplyWithWrongKind { get; set; }
/// <summary>
/// Stall indefinitely after receiving a request before sending any reply, so the client's
/// call-timeout token fires. Used to test the CallTimeout path.
/// </summary>
public bool StallAfterRequest { get; set; }
public FakeSidecarServer(string pipeName, string expectedSecret)
{
_pipeName = pipeName;
@@ -83,6 +103,32 @@ internal sealed class FakeSidecarServer : IAsyncDisposable
var frame = await reader.ReadFrameAsync(ct).ConfigureAwait(false);
if (frame is null) break;
// Drop before sending any reply — lets the client fall into its catch /
// retry path or propagate on second failure.
if (DisconnectBeforeReply)
{
pipe.Disconnect();
break;
}
// Stall indefinitely to let the client's call-timeout token fire.
if (StallAfterRequest)
{
await Task.Delay(Timeout.Infinite, ct).ConfigureAwait(false);
break;
}
// Optionally send a deliberately wrong kind back to exercise
// InvalidDataException detection in the client's ExchangeAsync.
if (ReplyWithWrongKind.HasValue)
{
var wrongKind = ReplyWithWrongKind.Value;
ReplyWithWrongKind = null; // arm once
// Send an empty body with the wrong kind so the client can parse it.
await writer.WriteAsync(wrongKind, new ReadRawReply { Success = false }, ct);
continue;
}
switch (frame.Value.Kind)
{
case MessageKind.ReadRawRequest: