using System.Text.Json; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Runtime.Drivers; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers; /// /// v3 (B1-WP4) — the deploy artifact must compute each raw tag's RawPath by walking /// RawFolder → Driver → Device → TagGroup ancestry, inject the per-driver RawTags into the driver's /// merged config, and fold the sole device's DeviceConfig endpoint up to the top level. A strong signal /// for the docker-dev live gate (a driver that gets the wrong RawPath or a missing endpoint won't bind). /// public sealed class DeploymentArtifactRawPathTests { private static byte[] BlobOf(object snapshot) => JsonSerializer.SerializeToUtf8Bytes(snapshot); private static object NestedSingleDeviceSnapshot() => new { Clusters = new[] { new { ClusterId = "MAIN" } }, Nodes = Array.Empty(), RawFolders = new[] { new { RawFolderId = "f1", ParentRawFolderId = (string?)null, Name = "Plant", ClusterId = "MAIN" }, new { RawFolderId = "f2", ParentRawFolderId = (string?)"f1", Name = "Line1", ClusterId = "MAIN" }, }, DriverInstances = new[] { new { DriverInstanceRowId = Guid.NewGuid(), DriverInstanceId = "drv1", Name = "Modbus1", DriverType = "Modbus", Enabled = true, DriverConfig = "{\"UnitId\":3}", ClusterId = "MAIN", RawFolderId = "f2" }, }, Devices = new[] { new { DeviceId = "dev1", DriverInstanceId = "drv1", Name = "PLC-A", DeviceConfig = "{\"Host\":\"10.0.0.5\",\"Port\":502}" }, }, TagGroups = new[] { new { TagGroupId = "g1", DeviceId = "dev1", ParentTagGroupId = (string?)null, Name = "Motors" }, new { TagGroupId = "g2", DeviceId = "dev1", ParentTagGroupId = (string?)"g1", Name = "M1" }, }, Tags = new[] { new { TagId = "t1", DeviceId = "dev1", TagGroupId = (string?)"g2", Name = "Speed", DataType = "Float", TagConfig = "{\"address\":\"40001\"}", WriteIdempotent = true }, new { TagId = "t2", DeviceId = "dev1", TagGroupId = (string?)null, Name = "Status", DataType = "Boolean", TagConfig = "{\"address\":\"1\"}", WriteIdempotent = false }, }, }; /// The nested folder + nested group tag resolves to /// Plant/Line1/Modbus1/PLC-A/Motors/M1/Speed; a device-root tag drops the group segments. [Fact] public void ParseDriverInstances_computes_nested_RawPath_and_merges_device_endpoint() { var specs = DeploymentArtifact.ParseDriverInstances(BlobOf(NestedSingleDeviceSnapshot())); var spec = specs.ShouldHaveSingleItem(); spec.DriverInstanceId.ShouldBe("drv1"); using var doc = JsonDocument.Parse(spec.DriverConfig); var root = doc.RootElement; // Endpoint from the sole device's DeviceConfig merged up to the top level (single-endpoint driver). root.GetProperty("Host").GetString().ShouldBe("10.0.0.5"); root.GetProperty("Port").GetInt32().ShouldBe(502); // Protocol/channel setting from the driver-level DriverConfig survives. root.GetProperty("UnitId").GetInt32().ShouldBe(3); // RawTags injected, ordinal-ordered by RawPath, each carrying its computed RawPath + device name. var rawTags = root.GetProperty("RawTags").EnumerateArray().ToList(); rawTags.Count.ShouldBe(2); var paths = rawTags.Select(e => e.GetProperty("RawPath").GetString()).ToList(); paths.ShouldContain("Plant/Line1/Modbus1/PLC-A/Motors/M1/Speed"); paths.ShouldContain("Plant/Line1/Modbus1/PLC-A/Status"); var speed = rawTags.Single(e => e.GetProperty("RawPath").GetString() == "Plant/Line1/Modbus1/PLC-A/Motors/M1/Speed"); speed.GetProperty("WriteIdempotent").GetBoolean().ShouldBeTrue(); speed.GetProperty("DeviceName").GetString().ShouldBe("PLC-A"); // Reconstructed Devices array carries the device name (routing key) + endpoint. var devices = root.GetProperty("Devices").EnumerateArray().ToList(); devices.ShouldHaveSingleItem(); devices[0].GetProperty("DeviceName").GetString().ShouldBe("PLC-A"); } /// The DARK address space this batch: no equipment-tag variable plans materialize. [Fact] public void ParseComposition_emits_dark_address_space_no_equipment_tags() { var composition = DeploymentArtifact.ParseComposition(BlobOf(NestedSingleDeviceSnapshot())); composition.EquipmentTags.ShouldBeEmpty(); } }