diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATRemediationGateTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATRemediationGateTests.cs
new file mode 100644
index 00000000..c592495b
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATRemediationGateTests.cs
@@ -0,0 +1,146 @@
+using System.Collections.Concurrent;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests;
+
+///
+/// Wire-level cases for the #739 High/Medium findings, against the live TwinCAT 3 XAR fixture
+/// (skipped without it, like ). Every case that writes restores what it
+/// changed: GVL_Primitives.vWord and vString are seeds the primitives theory reads.
+///
+[Collection("TwinCATXar")]
+[Trait("Category", "Integration")]
+[Trait("Simulator", "TwinCAT-XAR")]
+public sealed class TwinCATRemediationGateTests(TwinCATXarFixture sim)
+{
+ private string Host => $"ads://{sim.TargetNetId}:{sim.AmsPort}";
+
+ private TwinCATDriver NewDriver(string id, bool native, params TwinCATTagDefinition[] tags) => new(new TwinCATDriverOptions
+ {
+ Devices = [new TwinCATDeviceOptions(Host, "XAR-VM")],
+ RawTags = TwinCATRawTags.Entries(tags),
+ UseNativeNotifications = native,
+ Timeout = TimeSpan.FromSeconds(5),
+ Probe = new TwinCATProbeOptions { Enabled = false },
+ }, id);
+
+ /// H11: a typo'd symbol in a native subscribe is isolated; the real counter keeps delivering.
+ [TwinCATFact]
+ public async Task Native_subscribe_isolates_an_unknown_symbol()
+ {
+ if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
+ await using var drv = NewDriver("tc3-gate-h11", native: true,
+ new TwinCATTagDefinition("Counter", Host, "GVL_Fixture.nCounter", TwinCATDataType.DInt),
+ new TwinCATTagDefinition("Typo", Host, "GVL_Fixture.nCounterTypo", TwinCATDataType.DInt));
+ await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
+ var events = new ConcurrentQueue();
+ drv.OnDataChange += (_, e) => events.Enqueue(e);
+
+ var handle = await drv.SubscribeAsync(["Counter", "Typo"], TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken);
+ var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
+ while (events.Count(e => e.FullReference == "Counter") < 2 && DateTime.UtcNow < deadline)
+ await Task.Delay(50, TestContext.Current.CancellationToken);
+ await drv.UnsubscribeAsync(handle, TestContext.Current.CancellationToken);
+
+ events.Count(e => e.FullReference == "Counter" && e.Snapshot.StatusCode == 0u).ShouldBeGreaterThanOrEqualTo(2);
+ events.First(e => e.FullReference == "Typo").Snapshot.StatusCode.ShouldBe(TwinCATStatusMapper.BadNodeIdUnknown);
+ drv.GetHealth().State.ShouldBe(DriverState.Healthy, "a refused symbol is not a comms fault");
+ }
+
+ /// H12: native pushes alone advance LastSuccessfulRead.
+ [TwinCATFact]
+ public async Task Native_pushes_advance_LastSuccessfulRead()
+ {
+ if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
+ await using var drv = NewDriver("tc3-gate-h12", native: true,
+ new TwinCATTagDefinition("Counter", Host, "GVL_Fixture.nCounter", TwinCATDataType.DInt));
+ await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
+ var initStamp = drv.GetHealth().LastSuccessfulRead!.Value;
+
+ var handle = await drv.SubscribeAsync(["Counter"], TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken);
+ await Task.Delay(1500, TestContext.Current.CancellationToken);
+ await drv.UnsubscribeAsync(handle, TestContext.Current.CancellationToken);
+
+ drv.GetHealth().LastSuccessfulRead!.Value.ShouldBeGreaterThan(initStamp.AddMilliseconds(500));
+ }
+
+ ///
+ /// H18: a bit write on the 16-bit vWord (0xBEEF) flips exactly that bit at the WORD's own width
+ /// (the old 32-bit UDInt RMW could not even address a WORD), and a bit beyond it is refused.
+ ///
+ [TwinCATFact]
+ public async Task Bit_write_on_a_WORD_flips_one_bit_and_refuses_bit_16()
+ {
+ if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
+ await using var drv = NewDriver("tc3-gate-h18", native: false,
+ new TwinCATTagDefinition("Word", Host, "GVL_Primitives.vWord", TwinCATDataType.UInt, Writable: false),
+ new TwinCATTagDefinition("Bit4", Host, "GVL_Primitives.vWord.4", TwinCATDataType.Bool),
+ new TwinCATTagDefinition("Bit16", Host, "GVL_Primitives.vWord.16", TwinCATDataType.Bool));
+ await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
+ var before = Convert.ToUInt16((await drv.ReadAsync(["Word"], TestContext.Current.CancellationToken))[0].Value);
+ var bit4WasSet = (before & 0x10) != 0;
+ try
+ {
+ (await drv.WriteAsync([new WriteRequest("Bit4", !bit4WasSet)], TestContext.Current.CancellationToken))[0]
+ .StatusCode.ShouldBe(0u);
+ var after = Convert.ToUInt16((await drv.ReadAsync(["Word"], TestContext.Current.CancellationToken))[0].Value);
+ after.ShouldBe((ushort)(before ^ 0x10), "only bit 4 may change");
+
+ (await drv.WriteAsync([new WriteRequest("Bit16", true)], TestContext.Current.CancellationToken))[0]
+ .StatusCode.ShouldBe(TwinCATStatusMapper.BadOutOfRange);
+ }
+ finally
+ {
+ await drv.WriteAsync([new WriteRequest("Bit4", bit4WasSet)], CancellationToken.None);
+ }
+ Convert.ToUInt16((await drv.ReadAsync(["Word"], TestContext.Current.CancellationToken))[0].Value).ShouldBe(before);
+ }
+
+ /// U9: a whole-array write round-trips through the codec (CLR double[] converts to REAL elements).
+ [TwinCATFact]
+ public async Task Whole_array_write_round_trips()
+ {
+ if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
+ await using var drv = NewDriver("tc3-gate-u9-array", native: false,
+ new TwinCATTagDefinition("Arr", Host, "GVL_Arrays.aReal1D", TwinCATDataType.Real, ArrayLength: 32));
+ await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
+ var values = Enumerable.Range(0, 32).Select(i => i * 1.5 + 0.25).ToArray();
+
+ (await drv.WriteAsync([new WriteRequest("Arr", values)], TestContext.Current.CancellationToken))[0]
+ .StatusCode.ShouldBe(0u);
+ var read = (await drv.ReadAsync(["Arr"], TestContext.Current.CancellationToken))[0];
+ read.StatusCode.ShouldBe(0u);
+ ((float[])read.Value!).ShouldBe(values.Select(v => (float)v).ToArray());
+
+ (await drv.WriteAsync([new WriteRequest("Arr", values[..31])], TestContext.Current.CancellationToken))[0]
+ .StatusCode.ShouldBe(TwinCATStatusMapper.BadOutOfRange);
+ }
+
+ /// U9: a STRING write is NUL-padded at the PLC width (no stale tail) and an over-long one is refused.
+ [TwinCATFact]
+ public async Task String_write_round_trips_and_refuses_overflow()
+ {
+ if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
+ await using var drv = NewDriver("tc3-gate-u9-string", native: false,
+ new TwinCATTagDefinition("S", Host, "GVL_Primitives.vString", TwinCATDataType.String));
+ await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
+ var original = (string)(await drv.ReadAsync(["S"], TestContext.Current.CancellationToken))[0].Value!;
+ try
+ {
+ (await drv.WriteAsync([new WriteRequest("S", "Grüße 40°")], TestContext.Current.CancellationToken))[0]
+ .StatusCode.ShouldBe(0u);
+ (await drv.ReadAsync(["S"], TestContext.Current.CancellationToken))[0].Value.ShouldBe("Grüße 40°");
+
+ (await drv.WriteAsync([new WriteRequest("S", new string('x', 81))], TestContext.Current.CancellationToken))[0]
+ .StatusCode.ShouldBe(TwinCATStatusMapper.BadOutOfRange, "vString is STRING(80)");
+ }
+ finally
+ {
+ await drv.WriteAsync([new WriteRequest("S", original)], CancellationToken.None);
+ }
+ (await drv.ReadAsync(["S"], TestContext.Current.CancellationToken))[0].Value.ShouldBe(original);
+ }
+}