diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/HistoryReadParityTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/HistoryReadParityTests.cs new file mode 100644 index 0000000..29f68eb --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests/HistoryReadParityTests.cs @@ -0,0 +1,66 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests; + +/// +/// PR 5.6 — History-read parity. Phase-1 routing lifted history off the +/// per-driver path onto the server-owned +/// HistoryRouter + WonderwareHistorianBootstrap; neither +/// Galaxy backend implements directly. So +/// the parity surface here is the *routing decision*: both backends must +/// identify the same set of historized attributes and produce the same +/// full-reference for each, so HistoryRouter routes reads identically. +/// +[Trait("Category", "ParityE2E")] +[Collection(nameof(ParityCollection))] +public sealed class HistoryReadParityTests +{ + private readonly ParityHarness _h; + public HistoryReadParityTests(ParityHarness h) => _h = h; + + [Fact] + public async Task Discover_emits_same_historized_attribute_set_for_both_backends() + { + _h.RequireBoth(); + + var snapshots = await _h.RunOnAvailableAsync(async (driver, ct) => + { + var b = new RecordingAddressSpaceBuilder(); + await ((ITagDiscovery)driver).DiscoverAsync(b, ct); + return b.Variables + .Where(v => v.AttributeInfo.IsHistorized) + .Select(v => v.AttributeInfo.FullName) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + }, CancellationToken.None); + + var legacy = snapshots[ParityHarness.Backend.LegacyHost]; + var mxgw = snapshots[ParityHarness.Backend.MxGateway]; + + if (legacy.Count == 0) + { + Assert.Skip("dev Galaxy has no historized attributes — history routing parity unverified for this rig"); + } + + legacy.Except(mxgw, StringComparer.OrdinalIgnoreCase).ShouldBeEmpty( + "every historized attribute discovered by the legacy backend must appear in the mxgw backend"); + mxgw.Except(legacy, StringComparer.OrdinalIgnoreCase).ShouldBeEmpty( + "every historized attribute discovered by the mxgw backend must appear in the legacy backend"); + } + + [Fact] + public async Task Neither_Galaxy_backend_implements_IHistoryProvider_directly() + { + // Pinning the architectural decision from Phase 1 (PR 1.3): per-driver + // IHistoryProvider was retired in favor of the server-owned HistoryRouter. + // If a regression brings IHistoryProvider back on either Galaxy driver, + // this test fires. + _h.RequireBoth(); + + (_h.LegacyDriver as IHistoryProvider).ShouldBeNull( + "legacy GalaxyProxyDriver must not surface IHistoryProvider — history routes through HistoryRouter"); + (_h.MxGatewayDriver as IHistoryProvider).ShouldBeNull( + "in-process GalaxyDriver must not surface IHistoryProvider — history routes through HistoryRouter"); + } +}