diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/WonderwareHistorianClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/WonderwareHistorianClient.cs
index a1af5876..3f49a6f3 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/WonderwareHistorianClient.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/WonderwareHistorianClient.cs
@@ -33,13 +33,13 @@ public sealed class WonderwareHistorianClient : IHistorianDataSource, IAlarmHist
private int _consecutiveFailures;
///
- /// Creates a client over a real named-pipe connection. Tests that need an in-process
- /// duplex pair use the factory.
+ /// Creates a client that connects to the Wonderware historian sidecar over TCP.
+ /// Tests that need an in-process duplex pair use the factory.
///
/// The client connection options.
/// Optional logger for diagnostic output.
public WonderwareHistorianClient(WonderwareHistorianClientOptions options, ILogger? logger = null)
- : this(options, ct => FrameChannel.DefaultNamedPipeConnectFactory(options, ct), logger)
+ : this(options, ct => FrameChannel.DefaultTcpConnectFactory(options, ct), logger)
{
}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs
index 0e3c178a..9107e30b 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs
@@ -1,8 +1,11 @@
+using System.Net;
+using System.Net.Sockets;
using MessagePack;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
+using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Internal;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Ipc;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests;
@@ -28,6 +31,16 @@ public sealed class WonderwareHistorianClientTests
ConnectTimeout: TimeSpan.FromSeconds(2),
CallTimeout: TimeSpan.FromSeconds(2));
+ ///
+ /// Creates a client over a named pipe using the
+ /// factory seam. Existing pipe-based tests use this after the public ctor was flipped to TCP.
+ ///
+ private static WonderwareHistorianClient PipeClientFor(string pipe)
+ {
+ var opts = OptsFor(pipe);
+ return WonderwareHistorianClient.ForTests(opts, ct => FrameChannel.DefaultNamedPipeConnectFactory(opts, ct));
+ }
+
/// Verifies that ReadRawAsync round-trips samples and maps quality bytes to OPC UA status codes.
[Fact]
public async Task ReadRawAsync_RoundTripsSamples_AndMapsQualityByteToOpcUaStatusCode()
@@ -57,7 +70,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
var result = await client.ReadRawAsync("Tank.Level",
new DateTime(2026, 4, 29, 0, 0, 0, DateTimeKind.Utc),
new DateTime(2026, 4, 30, 0, 0, 0, DateTimeKind.Utc),
@@ -89,7 +102,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
var result = await client.ReadProcessedAsync("Tank.Level",
new DateTime(2026, 4, 29, 0, 0, 0, DateTimeKind.Utc),
new DateTime(2026, 4, 29, 0, 2, 0, DateTimeKind.Utc),
@@ -122,7 +135,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
var result = await client.ReadAtTimeAsync("Tank.Level", new[] { t1, t2 }, CancellationToken.None);
result.Samples.Count.ShouldBe(2);
@@ -164,7 +177,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
var result = await client.ReadAtTimeAsync("Tank.Level", new[] { t1, t2, t3 }, CancellationToken.None);
// Result MUST be the same length and order as the request.
@@ -209,7 +222,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
var result = await client.ReadEventsAsync("Tank.HiHi",
new DateTime(2026, 4, 29, 0, 0, 0, DateTimeKind.Utc),
new DateTime(2026, 4, 30, 0, 0, 0, DateTimeKind.Utc),
@@ -233,7 +246,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
var ex = await Should.ThrowAsync(() =>
client.ReadRawAsync("Tag", DateTime.UtcNow, DateTime.UtcNow, 100, CancellationToken.None));
@@ -255,7 +268,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
var batch = new[]
{
new AlarmHistorianEvent("ev-1", "Tank/HiHi", "HiHi", "LimitAlarm", AlarmSeverity.High, "Activated", "msg", "operator", null, DateTime.UtcNow),
@@ -285,7 +298,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
var batch = new[]
{
new AlarmHistorianEvent("ev-1", "Tank/HiHi", "HiHi", "LimitAlarm", AlarmSeverity.High, "Activated", "msg", "u", null, DateTime.UtcNow),
@@ -307,7 +320,7 @@ public sealed class WonderwareHistorianClientTests
await using var server = new FakeSidecarServer(pipe, "different-secret");
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
var ex = await Should.ThrowAsync(() =>
client.ReadRawAsync("Tag", DateTime.UtcNow, DateTime.UtcNow, 100, CancellationToken.None));
@@ -331,7 +344,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
// First call: handshake + dropped. Reconnect kicks in inside the channel; second
// attempt within the same InvokeAsync succeeds. From the caller's perspective it's
@@ -358,7 +371,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
await client.ReadRawAsync("Tag", DateTime.UtcNow, DateTime.UtcNow, 1, CancellationToken.None);
@@ -394,7 +407,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
var batch = new[]
{
new AlarmHistorianEvent("ev-1", "Tank/HiHi", "HiHi", "LimitAlarm", AlarmSeverity.High, "Activated", "msg", "u", null, DateTime.UtcNow),
@@ -425,7 +438,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
// ReadRawAsync uses Invoke, which propagates the exception when both attempts fail.
await Should.ThrowAsync(() =>
@@ -453,7 +466,7 @@ public sealed class WonderwareHistorianClientTests
ConnectTimeout: TimeSpan.FromSeconds(2),
CallTimeout: TimeSpan.FromMilliseconds(500)); // short timeout for test speed
- await using var client = new WonderwareHistorianClient(opts);
+ await using var client = WonderwareHistorianClient.ForTests(opts, ct => FrameChannel.DefaultNamedPipeConnectFactory(opts, ct));
// The stall means neither the first nor the retry can complete, so the timeout
// linked-token should cancel the operation.
@@ -473,7 +486,7 @@ public sealed class WonderwareHistorianClientTests
await using var server = new FakeSidecarServer(pipe, Secret);
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
await Should.ThrowAsync(() =>
client.ReadProcessedAsync("Tag",
@@ -497,7 +510,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
await Should.ThrowAsync(() =>
client.ReadRawAsync("Tag", DateTime.UtcNow, DateTime.UtcNow, 100, CancellationToken.None));
@@ -523,7 +536,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
await Should.ThrowAsync(() =>
client.ReadRawAsync("Tag", DateTime.UtcNow, DateTime.UtcNow, 1, CancellationToken.None));
@@ -554,7 +567,7 @@ public sealed class WonderwareHistorianClientTests
};
await server.StartAsync();
- await using var client = new WonderwareHistorianClient(OptsFor(pipe));
+ await using var client = PipeClientFor(pipe);
using var stop = new CancellationTokenSource();
var readerSawInconsistent = false;
@@ -593,4 +606,108 @@ public sealed class WonderwareHistorianClientTests
final.TotalSuccesses.ShouldBe(50);
final.TotalFailures.ShouldBe(0);
}
+
+ // ===== Task 3: default public ctor dials TCP =====
+
+ ///
+ /// Verifies that the default public ctor connects over TCP rather than named-pipe by
+ /// constructing the client against a loopback and asserting
+ /// that a ReadRaw round-trip returns the known sample. If the ctor still dialled a
+ /// named pipe the connect would fail because no pipe is listening.
+ ///
+ [Fact]
+ public async Task DefaultCtor_DialsTcp_ReadRawRoundTrips()
+ {
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ cts.CancelAfter(TimeSpan.FromSeconds(10));
+
+ // 1. Start a loopback TCP listener on an OS-assigned port.
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ var boundPort = ((IPEndPoint)listener.LocalEndpoint).Port;
+
+ var expectedTicks = new DateTime(2026, 6, 12, 8, 0, 0, DateTimeKind.Utc).Ticks;
+ var expectedValue = MessagePackSerializer.Serialize