using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E; [Trait("Category", "ParityE2E")] [Collection(nameof(ParityCollection))] public sealed class HierarchyParityTests { private readonly ParityFixture _fx; public HierarchyParityTests(ParityFixture fx) => _fx = fx; [Fact] public async Task Discover_returns_at_least_one_gobject_with_attributes() { _fx.SkipIfUnavailable(); var builder = new RecordingAddressSpaceBuilder(); await _fx.Driver!.DiscoverAsync(builder, CancellationToken.None); builder.Folders.Count.ShouldBeGreaterThan(0, "live Galaxy ZB has at least one deployed gobject"); builder.Variables.Count.ShouldBeGreaterThan(0, "at least one gobject in the dev Galaxy carries dynamic attributes"); } [Fact] public async Task Discover_emits_only_lowercase_browse_paths_for_each_attribute() { // OPC UA browse paths are case-sensitive; the v1 server emits Galaxy attribute // names verbatim (camelCase like "PV.Input.Value"). Parity invariant: every // emitted variable's full reference contains a '.' separating the gobject // tag-name from the attribute name (Galaxy reference grammar). _fx.SkipIfUnavailable(); var builder = new RecordingAddressSpaceBuilder(); await _fx.Driver!.DiscoverAsync(builder, CancellationToken.None); builder.Variables.ShouldAllBe(v => v.AttributeInfo.FullName.Contains('.'), "Galaxy MXAccess full references are 'tag.attribute'"); } [Fact] public async Task Discover_marks_at_least_one_attribute_as_historized_when_HistoryExtension_present() { _fx.SkipIfUnavailable(); var builder = new RecordingAddressSpaceBuilder(); await _fx.Driver!.DiscoverAsync(builder, CancellationToken.None); // Soft assertion — some Galaxies are configuration-only with no Historian extensions. // We only check the field flows through correctly when populated. var historized = builder.Variables.Count(v => v.AttributeInfo.IsHistorized); // Just assert the count is non-negative — the value itself is data-dependent. historized.ShouldBeGreaterThanOrEqualTo(0); } }