diff --git a/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json b/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json index d952ba71..19647b38 100644 --- a/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json +++ b/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json @@ -3,7 +3,7 @@ "lastUpdated": "2026-07-12", "tasks": [ { "id": "T1", "subject": "Golden TagConfig corpus + compose→encode→decode parity characterization test (Runtime.Tests, green on unmodified tree)", "status": "completed", "blockedBy": [] }, - { "id": "T2", "subject": "FullName-semantics characterization for EquipmentNodeWalker (Core.Tests) + DraftValidator Galaxy rule (Configuration.Tests)", "status": "pending", "blockedBy": [] }, + { "id": "T2", "subject": "FullName-semantics characterization for EquipmentNodeWalker (Core.Tests) + DraftValidator Galaxy rule (Configuration.Tests)", "status": "completed", "blockedBy": [] }, { "id": "T3", "subject": "TagConfigIntent + TagAlarmIntent in Commons/Types (TDD; port composer semantics verbatim + OpcUaServer ExtractTag* test tables)", "status": "pending", "blockedBy": [] }, { "id": "T4", "subject": "DeviceConfigIntent (TryExtractHost/NormalizeHost) in Commons/Types (TDD)", "status": "pending", "blockedBy": [] }, { "id": "T5", "subject": "AddressSpaceComposer swap: single TagConfigIntent.Parse per tag, delete 4 Extract* statics (P-1 parse-once)", "status": "pending", "blockedBy": ["T1", "T3"] }, diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftValidatorGalaxyFullNameCorpusTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftValidatorGalaxyFullNameCorpusTests.cs new file mode 100644 index 00000000..e4af659e --- /dev/null +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftValidatorGalaxyFullNameCorpusTests.cs @@ -0,0 +1,60 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Configuration.Entities; +using ZB.MOM.WW.OtOpcUa.Configuration.Enums; +using ZB.MOM.WW.OtOpcUa.Configuration.Validation; + +namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests; + +/// +/// Characterization net (R2-11) pinning the DraftValidator Galaxy rule's explicit-FullName +/// semantics across the golden TagConfig corpus BEFORE the private FullName reader is delegated to +/// TagConfigIntent.Parse(...).ExplicitFullName. Distinct from the walker/composer raw-blob +/// fallback: GalaxyTagMissingReference fires whenever the TagConfig has no usable +/// explicit string FullName — i.e. absent / non-string / whitespace / non-object / +/// malformed — and does NOT fire only when a non-blank string FullName is present. Must be green on +/// the unmodified tree and stay green through the swap. +/// +public sealed class DraftValidatorGalaxyFullNameCorpusTests +{ + [Theory] + // Present, non-blank string FullName → rule does NOT fire + [InlineData("{\"FullName\":\"X.Y\"}", false)] + [InlineData("{\"FullName\":\"A.B\",\"region\":\"Coils\"}", false)] + // Absent / non-string / whitespace / non-object / malformed → rule FIRES + [InlineData("{}", true)] + [InlineData("{\"region\":\"HoldingRegisters\"}", true)] + [InlineData("{\"FullName\":123}", true)] + [InlineData("{\"FullName\":\" \"}", true)] + [InlineData("[1,2]", true)] + [InlineData("not json {", true)] + public void Galaxy_missing_reference_fires_iff_no_explicit_string_FullName(string tagConfig, bool shouldFire) + { + var draft = new DraftSnapshot + { + GenerationId = 1, + ClusterId = "c", + DriverInstances = + [ + new DriverInstance + { + DriverInstanceId = "d-galaxy", ClusterId = "c", NamespaceId = "ns-1", + Name = "Galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}", + }, + ], + Tags = + [ + new Tag + { + TagId = "tag-galaxytag", DriverInstanceId = "d-galaxy", EquipmentId = "eq-1", + Name = "galaxytag", FolderPath = null, DataType = "Float", + AccessLevel = TagAccessLevel.Read, TagConfig = tagConfig, + }, + ], + }; + + var fired = DraftValidator.Validate(draft) + .Any(e => e.Code == "GalaxyTagMissingReference" && e.Context == "tag-galaxytag"); + fired.ShouldBe(shouldFire); + } +} diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/OpcUa/EquipmentNodeWalkerFullNameCorpusTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/OpcUa/EquipmentNodeWalkerFullNameCorpusTests.cs new file mode 100644 index 00000000..ae74919d --- /dev/null +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/OpcUa/EquipmentNodeWalkerFullNameCorpusTests.cs @@ -0,0 +1,36 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.OpcUa; + +namespace ZB.MOM.WW.OtOpcUa.Core.Tests.OpcUa; + +/// +/// Characterization net (R2-11) pinning 's +/// raw-blob-fallback semantics across the golden TagConfig corpus BEFORE the parser is delegated to +/// TagConfigIntent.Parse. The walker's FullName copy falls back to the raw blob whenever the +/// input is not a JSON object carrying a string FullName (blank / non-object / malformed / +/// absent / non-string). Whitespace + non-string-property values are returned verbatim (not trimmed). +/// Must be green on the unmodified tree and stay green through the swap. +/// +public sealed class EquipmentNodeWalkerFullNameCorpusTests +{ + [Theory] + // object with a string FullName → the FullName string (verbatim, incl. whitespace) + [InlineData("{\"FullName\":\"X.Y\"}", "X.Y")] + [InlineData("{\"FullName\":\" \"}", " ")] + [InlineData("{\"FullName\":\"A.B\",\"region\":\"Coils\",\"address\":5}", "A.B")] + // blank → raw blob (IsNullOrWhiteSpace short-circuit) + [InlineData(" ", " ")] + // non-object root → raw blob + [InlineData("[1,2]", "[1,2]")] + // malformed JSON → raw blob + [InlineData("not json {", "not json {")] + // object, FullName absent → raw blob + [InlineData("{\"region\":\"HoldingRegisters\",\"address\":5}", "{\"region\":\"HoldingRegisters\",\"address\":5}")] + // object, FullName present but non-string → raw blob + [InlineData("{\"FullName\":123}", "{\"FullName\":123}")] + public void ExtractFullName_falls_back_to_raw_blob_except_for_string_FullName(string tagConfig, string expected) + { + EquipmentNodeWalker.ExtractFullName(tagConfig).ShouldBe(expected); + } +}