diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/DriverReconnectE2eTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/DriverReconnectE2eTests.cs
index 645fbb5e..72c1ea61 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/DriverReconnectE2eTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/DriverReconnectE2eTests.cs
@@ -225,14 +225,7 @@ public sealed class DriverReconnectE2eTests
CreatedBy = "test",
});
- db.Namespaces.Add(new Namespace
- {
- NamespaceId = "RECONNECT-E2E-equipment",
- ClusterId = ClusterId,
- Kind = NamespaceKind.Equipment,
- NamespaceUri = "urn:zb:reconnect-e2e:equipment",
- });
-
+ // v3: the Namespace entity is deleted (DriverInstance no longer binds a Namespace).
db.ClusterNodes.Add(new ClusterNode
{
NodeId = harness.NodeANodeId,
@@ -258,7 +251,6 @@ public sealed class DriverReconnectE2eTests
{
DriverInstanceId = DriverId,
ClusterId = ClusterId,
- NamespaceId = "RECONNECT-E2E-equipment",
Name = DriverId,
DriverType = "Modbus",
Enabled = true,
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/EquipmentNamespaceMaterializationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/EquipmentNamespaceMaterializationTests.cs
index e95e2e1e..03fd19ca 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/EquipmentNamespaceMaterializationTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/EquipmentNamespaceMaterializationTests.cs
@@ -13,20 +13,32 @@ using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
///
-/// End-to-end deploy of an Equipment-kind namespace through the real
-/// ConfigComposer: seed a 1-area / 1-line / 1-equipment / 1-tag Equipment namespace,
-/// StartDeployment, then assert the deployment's persisted artifact decodes (via
-/// ) to the equipment signal + the friendly
-/// UNS folder name. This covers the ConfigComposer → ArtifactBlob → ParseComposition.EquipmentTags
-/// seam that the OpcUaServer unit tests only approximate with hand-built JSON.
+/// End-to-end deploy of a UNS folder hierarchy through the real ConfigComposer: seed a
+/// 1-area / 1-line / 1-equipment UNS plus a v3 raw-tag chain (RawFolder → DriverInstance → Device →
+/// Tag), StartDeployment, then assert the deployment's persisted artifact decodes (via
+/// ).
+///
+/// v3 DARK address space (Batch 1): the composer + artifact deliberately emit an EMPTY
+/// equipment-tag plan set — raw-tag variable nodes are lit up in Batch 4's dual-namespace UNS↔Raw
+/// fan-out. So the live assertion here is the DARK contract: the Area/Line/Equipment folder nodes
+/// decode with their friendly UNS names, and EquipmentTags is empty EVEN THOUGH a raw tag was
+/// seeded. The full equipment-signal materialization (raw tag → EquipmentTag with FullName) is
+/// preserved as a Batch-4-pending Skipped test below (the plan explicitly says keep it, don't delete).
+///
///
/// The OPC UA address-space browse is exercised separately against a real SDK node manager in
-/// AddressSpaceApplierHierarchyTests.Equipment_namespace_structure_materialises_end_to_end_against_real_SDK,
-/// because the in-process binds the no-op address-space sink.
+/// AddressSpaceApplierHierarchyTests, because the in-process
+/// binds the no-op address-space sink.
///
///
public sealed class EquipmentNamespaceMaterializationTests
{
+ private const string Batch4Pending =
+ "v3 dark address space: equipment-tag variable plans (raw tag → EquipmentTag with FullName) do " +
+ "not materialize until Batch 4 (dual-namespace UNS↔Raw fan-out). The composer + artifact emit an " +
+ "empty equipment-tag plan set this batch; re-enable this full-materialization assertion (re-authored " +
+ "against the UnsTagReference fan-out) when Batch 4 lights the raw/UNS variable nodes.";
+
private static CancellationToken Ct => TestContext.Current.CancellationToken;
// Equipment.EquipmentId must equal DraftValidator.DeriveEquipmentId(EquipmentUuid) — the
@@ -35,16 +47,18 @@ public sealed class EquipmentNamespaceMaterializationTests
private static readonly Guid EquipmentUuid = Guid.Parse("11111111-1111-1111-1111-111111111111");
private static readonly string EquipmentId = DraftValidator.DeriveEquipmentId(EquipmentUuid); // EQ-111111111111
- /// Verifies a deployed Equipment namespace carries its signal into the composed artifact.
+ /// Verifies a deployed UNS hierarchy carries its Area/Line/Equipment folder nodes into the
+ /// composed artifact with their friendly UNS names, and — the v3 Batch-1 DARK contract — that the
+ /// equipment-tag plan set is EMPTY even though a raw tag was seeded (raw-tag variables are Batch 4).
[Fact]
- public async Task Deploying_an_equipment_namespace_carries_the_signal_into_the_artifact()
+ public async Task Deploying_a_uns_hierarchy_carries_folder_nodes_and_leaves_equipment_tags_dark()
{
await using var harness = await TwoNodeClusterHarness.StartAsync();
// Seed the parent ServerCluster + ClusterNode rows the SQL FKs require (the c1 config below
// FKs to ServerCluster; on the in-memory provider this is unnecessary — FKs aren't enforced).
await harness.SeedDefaultClusterAsync("c1");
- await SeedEquipmentNamespaceAsync(harness);
+ await SeedUnsHierarchyAsync(harness);
await using var scope = harness.NodeA.Services.CreateAsyncScope();
var client = scope.ServiceProvider.GetRequiredService();
@@ -71,47 +85,88 @@ public sealed class EquipmentNamespaceMaterializationTests
var composition = DeploymentArtifact.ParseComposition(artifact);
- // The real ConfigComposer serialised the equipment Tag (incl. its TagConfig), and
- // ParseComposition extracted it as an EquipmentTag with FullName pulled from TagConfig.
+ // The UNS folder hierarchy decodes with its friendly UNS names (browse names), not MachineCodes.
+ composition.EquipmentNodes.ShouldContain(e => e.EquipmentId == EquipmentId && e.DisplayName == "station-1");
+ composition.UnsAreas.ShouldContain(a => a.UnsAreaId == "nw-area-filling" && a.DisplayName == "filling");
+ composition.UnsLines.ShouldContain(l => l.UnsLineId == "nw-line-1" && l.DisplayName == "line-1");
+
+ // v3 DARK contract: no equipment-tag variable plans materialize this batch — even though a raw
+ // tag ("Speed" / FullName 40001) was seeded under the device. Batch 4 lights these via the UNS↔Raw fan-out.
+ composition.EquipmentTags.ShouldBeEmpty();
+ }
+
+ /// BATCH-4-PENDING: the full equipment-signal materialization — the seeded raw tag decodes
+ /// (via the real ConfigComposer → ParseComposition) into an EquipmentTag carrying its FullName. Dark
+ /// until Batch 4's UNS↔Raw fan-out; preserved (not deleted) so re-enabling is a one-line change once
+ /// the equipment-tag plan set is lit up.
+ [Fact(Skip = Batch4Pending)]
+ public async Task Deploying_an_equipment_namespace_carries_the_signal_into_the_artifact()
+ {
+ await using var harness = await TwoNodeClusterHarness.StartAsync();
+ await harness.SeedDefaultClusterAsync("c1");
+ await SeedUnsHierarchyAsync(harness);
+
+ await using var scope = harness.NodeA.Services.CreateAsyncScope();
+ var client = scope.ServiceProvider.GetRequiredService();
+
+ var result = await client.StartDeploymentAsync(createdBy: "alice@test", Ct);
+ result.Outcome.ShouldBe(StartDeploymentOutcome.Accepted, $"Deploy not accepted: {result.Message}");
+ var deploymentId = result.DeploymentId!.Value.Value;
+
+ var artifact = Array.Empty();
+ await WaitForAsync(async () =>
+ {
+ await using var db = await CreateDbAsync(harness);
+ var d = await db.Deployments.AsNoTracking()
+ .FirstOrDefaultAsync(x => x.DeploymentId == deploymentId, Ct);
+ if (d is { ArtifactBlob.Length: > 0 })
+ {
+ artifact = d.ArtifactBlob;
+ return true;
+ }
+ return false;
+ }, TimeSpan.FromSeconds(15));
+
+ var composition = DeploymentArtifact.ParseComposition(artifact);
+
+ // Batch 4: the raw tag surfaces as an EquipmentTag with FullName pulled from TagConfig.
var tag = composition.EquipmentTags.ShouldHaveSingleItem();
tag.TagId.ShouldBe("tag-speed");
tag.EquipmentId.ShouldBe(EquipmentId);
tag.Name.ShouldBe("Speed");
tag.DataType.ShouldBe("Float");
tag.FullName.ShouldBe("40001");
-
- // The equipment folder browses by its friendly UNS Name, not the MachineCode.
- composition.EquipmentNodes.ShouldContain(e => e.EquipmentId == EquipmentId && e.DisplayName == "station-1");
- composition.UnsAreas.ShouldContain(a => a.UnsAreaId == "nw-area-filling" && a.DisplayName == "filling");
- composition.UnsLines.ShouldContain(l => l.UnsLineId == "nw-line-1" && l.DisplayName == "line-1");
}
- private static async Task SeedEquipmentNamespaceAsync(TwoNodeClusterHarness harness)
+ private static async Task SeedUnsHierarchyAsync(TwoNodeClusterHarness harness)
{
await using var db = await CreateDbAsync(harness);
- db.Namespaces.Add(new Namespace
- {
- NamespaceId = "ns-eq", ClusterId = "c1", Kind = NamespaceKind.Equipment, NamespaceUri = "urn:eq",
- });
+ // v3 raw-tag chain: RawFolder → DriverInstance(RawFolderId) → Device(DriverInstanceId) →
+ // Tag(DeviceId). The Namespace entity is deleted; equipment no longer binds a driver/device.
+ db.RawFolders.Add(new RawFolder { RawFolderId = "raw-plant", ClusterId = "c1", Name = "Plant" });
// Disabled so the driver-role nodes don't spawn a live Modbus driver (no endpoint to reach);
// the composition still carries the instance (ParseComposition does not filter on Enabled).
db.DriverInstances.Add(new DriverInstance
{
- DriverInstanceId = "drv-modbus", ClusterId = "c1", NamespaceId = "ns-eq",
+ DriverInstanceId = "drv-modbus", ClusterId = "c1", RawFolderId = "raw-plant",
Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}", Enabled = false,
});
+ db.Devices.Add(new Device
+ {
+ DeviceId = "dev-1", DriverInstanceId = "drv-modbus",
+ Name = "dev-1", DeviceConfig = "{}",
+ });
db.UnsAreas.Add(new UnsArea { UnsAreaId = "nw-area-filling", ClusterId = "c1", Name = "filling" });
db.UnsLines.Add(new UnsLine { UnsLineId = "nw-line-1", UnsAreaId = "nw-area-filling", Name = "line-1" });
db.Equipment.Add(new Equipment
{
EquipmentId = EquipmentId, EquipmentUuid = EquipmentUuid,
- DriverInstanceId = "drv-modbus", UnsLineId = "nw-line-1",
- Name = "station-1", MachineCode = "STATION_001",
+ UnsLineId = "nw-line-1", Name = "station-1", MachineCode = "STATION_001",
});
db.Tags.Add(new Tag
{
- TagId = "tag-speed", DriverInstanceId = "drv-modbus", EquipmentId = EquipmentId,
+ TagId = "tag-speed", DeviceId = "dev-1",
Name = "Speed", DataType = "Float", AccessLevel = TagAccessLevel.Read,
TagConfig = "{\"FullName\":\"40001\"}",
});
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MultiClusterScopingTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MultiClusterScopingTests.cs
index a554b8d2..c7d75e5e 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MultiClusterScopingTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MultiClusterScopingTests.cs
@@ -120,17 +120,15 @@ public sealed class MultiClusterScopingTests
NewCluster(MainCluster, "Main Cluster", "central"),
NewCluster(SiteACluster, "Site A Cluster", "site-a"));
- db.Namespaces.AddRange(
- NewNamespace(MainCluster, "MAIN-equipment", "urn:zb:central:equipment"),
- NewNamespace(SiteACluster, "SITE-A-equipment", "urn:zb:site-a:equipment"));
-
+ // v3: the Namespace entity is deleted (DriverInstance no longer binds a Namespace). Cluster
+ // scoping filters by NodeId/ClusterId, so the namespace seed is dropped here.
db.ClusterNodes.AddRange(
NewNode(mainNodeId, MainCluster, "urn:zb:central:node-a"),
NewNode(siteANodeId, SiteACluster, "urn:zb:site-a:node-b"));
db.DriverInstances.AddRange(
- NewDriver(MainDriverId, MainCluster, "MAIN-equipment"),
- NewDriver(SiteADriverId, SiteACluster, "SITE-A-equipment"));
+ NewDriver(MainDriverId, MainCluster),
+ NewDriver(SiteADriverId, SiteACluster));
await db.SaveChangesAsync(Ct);
}
@@ -146,14 +144,6 @@ public sealed class MultiClusterScopingTests
CreatedBy = "test",
};
- private static Namespace NewNamespace(string clusterId, string namespaceId, string uri) => new()
- {
- NamespaceId = namespaceId,
- ClusterId = clusterId,
- Kind = NamespaceKind.Equipment,
- NamespaceUri = uri,
- };
-
private static ClusterNode NewNode(string nodeId, string clusterId, string applicationUri) => new()
{
NodeId = nodeId,
@@ -163,11 +153,10 @@ public sealed class MultiClusterScopingTests
CreatedBy = "test",
};
- private static DriverInstance NewDriver(string driverInstanceId, string clusterId, string namespaceId) => new()
+ private static DriverInstance NewDriver(string driverInstanceId, string clusterId) => new()
{
DriverInstanceId = driverInstanceId,
ClusterId = clusterId,
- NamespaceId = namespaceId,
Name = driverInstanceId,
DriverType = "Modbus",
Enabled = true,
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs
index 6215eea9..14cc8f87 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs
@@ -165,7 +165,7 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
/// network-browse E2E (Host.IntegrationTests) needs the docker-dev fixture and is tracked
/// as a follow-up.
///
- [Fact]
+ [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
public async Task Equipment_namespace_structure_materialises_end_to_end_against_real_SDK()
{
await using var host = new OpcUaApplicationHost(
@@ -183,20 +183,23 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable
await host.StartAsync(sdkServer, Ct);
sdkServer.NodeManager.ShouldNotBeNull();
- // One area / line / equipment + a Modbus FK driver in an Equipment-kind namespace, with a
- // single equipment-bound Tag (the signal). Equipment.Name is the UNS browse segment.
- var ns = new Namespace { NamespaceId = "ns-eq", ClusterId = "c1", Kind = NamespaceKind.Equipment, NamespaceUri = "urn:eq" };
- var driver = new DriverInstance { DriverInstanceId = "drv-modbus", ClusterId = "c1", NamespaceId = "ns-eq", Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}" };
+ // One area / line / equipment + a Modbus driver. v3: no Namespace entity, DriverInstance no
+ // longer binds a Namespace, and Equipment no longer binds a driver. The equipment-bound Tag
+ // (the Speed signal) that this end-to-end test materialised as a Variable node is DARK until
+ // Batch 4 — the composer emits an empty EquipmentTags set, so it is no longer seeded/passed
+ // here. Equipment.Name is the UNS browse segment. (Folder-only materialisation is covered live
+ // by the sibling MaterialiseHierarchy_against_real_SDK test; this suite's equipment-tag
+ // Variable end-to-end leg re-enables in Batch 4 via the UnsTagReference fan-out.)
+ var driver = new DriverInstance { DriverInstanceId = "drv-modbus", ClusterId = "c1", Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}" };
var area = new UnsArea { UnsAreaId = "nw-area-filling", ClusterId = "c1", Name = "filling" };
var line = new UnsLine { UnsLineId = "nw-line-1", UnsAreaId = "nw-area-filling", Name = "line-1" };
- var equipment = new Equipment { EquipmentId = "eq-1", DriverInstanceId = "drv-modbus", UnsLineId = "nw-line-1", Name = "station-1", MachineCode = "STATION_001" };
- var tag = new Tag { TagId = "tag-speed", DriverInstanceId = "drv-modbus", EquipmentId = "eq-1", Name = "Speed", DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{\"FullName\":\"40001\"}" };
+ var equipment = new Equipment { EquipmentId = "eq-1", UnsLineId = "nw-line-1", Name = "station-1", MachineCode = "STATION_001" };
var composition = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equipment }, new[] { driver },
- Array.Empty(), new[] { tag }, new[] { ns });
+ Array.Empty());
- // Compose-side EquipmentTags extraction.
+ // Compose-side EquipmentTags extraction (Batch-4 subject — empty this batch).
var planned = composition.EquipmentTags.ShouldHaveSingleItem();
planned.EquipmentId.ShouldBe("eq-1");
planned.FullName.ShouldBe("40001");
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerAliasTagTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerAliasTagTests.cs
index c01d71c6..70138b07 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerAliasTagTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerAliasTagTests.cs
@@ -1,81 +1,30 @@
-using Shouldly;
using Xunit;
-using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
-using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
///
-/// Verifies the live-edit compose seam () treats a Galaxy
-/// point as an ordinary equipment tag: GalaxyMxGateway is a standard Equipment-kind driver.
-/// An equipment-scoped (non-null ) bound to a
-/// GalaxyMxGateway driver living in an Equipment-kind namespace must surface under
-/// (carrying its driver-side FullName).
+/// Characterized the live-edit compose seam admitting a Galaxy point as an ordinary equipment tag:
+/// an equipment-scoped Tag bound to a GalaxyMxGateway driver surfaced under
+/// carrying its driver-side FullName (coalescing a
+/// null FolderPath to string.Empty).
+/// v3 Batch-1 DARK + reshaped seed: equipment-tag variable plans do not materialize until
+/// Batch 4 (the composer emits an empty EquipmentTags set — the raw + UNS variable nodes light
+/// up in Batch 4's dual-namespace UNS↔Raw fan-out). The seed mechanism this test used is also gone:
+/// Tag is now raw-only (no EquipmentId/DriverInstanceId/FolderPath), the
+/// Compose overload no longer takes tags/namespaces, and equipment references raw tags via
+/// UnsTagReference rather than a Galaxy point carrying an EquipmentId. So there is no
+/// Batch-1 analog to assert. Re-author against the UnsTagReference → EquipmentTags fan-out when Batch 4
+/// lights the equipment-tag variable nodes.
///
public sealed class AddressSpaceComposerAliasTagTests
{
- /// A GalaxyMxGateway driver in an Equipment-kind namespace carries an
- /// equipment-scoped Galaxy tag (EquipmentId set, FolderPath null, TagConfig FullName = the Galaxy
- /// ref). Compose must put it in EquipmentTags with its FullName, coalescing the null FolderPath to
- /// string.Empty.
- [Fact]
+ /// A GalaxyMxGateway equipment tag surfaces under EquipmentTags with its FullName —
+ /// dark until Batch 4 (empty equipment-tag plan set) and re-authored via UnsTagReference.
+ [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
public void Compose_admits_galaxy_equipment_tag_in_equipment_tags()
{
- var ns = new Namespace
- {
- NamespaceId = "ns-eq",
- ClusterId = "c1",
- Kind = NamespaceKind.Equipment,
- NamespaceUri = "urn:eq",
- };
- var driver = new DriverInstance
- {
- DriverInstanceId = "drv-galaxy",
- ClusterId = "c1",
- NamespaceId = "ns-eq",
- Name = "Galaxy1",
- DriverType = "GalaxyMxGateway",
- DriverConfig = "{}",
- };
- var area = new UnsArea { UnsAreaId = "area-1", ClusterId = "c1", Name = "filling" };
- var line = new UnsLine { UnsLineId = "line-1", UnsAreaId = "area-1", Name = "line-1" };
- var equip = new Equipment
- {
- EquipmentId = "eq-1",
- DriverInstanceId = "drv-galaxy",
- UnsLineId = "line-1",
- Name = "TestMachine_020",
- MachineCode = "TESTMACHINE_020",
- };
-
- // Equipment-scoped Galaxy tag: EquipmentId set, FolderPath null, FullName = Galaxy ref.
- var galaxyTag = new Tag
- {
- TagId = "tag-galaxy",
- DriverInstanceId = "drv-galaxy",
- EquipmentId = "eq-1",
- FolderPath = null,
- Name = "TestChangingInt",
- DataType = "Int32",
- AccessLevel = TagAccessLevel.Read,
- TagConfig = "{\"FullName\":\"TestMachine_020.TestChangingInt\"}",
- };
-
- var result = AddressSpaceComposer.Compose(
- new[] { area }, new[] { line }, new[] { equip },
- new[] { driver }, Array.Empty(),
- new[] { galaxyTag }, new[] { ns });
-
- // The Galaxy point survives composition as an equipment tag, carrying its Galaxy FullName.
- var tag = result.EquipmentTags.ShouldHaveSingleItem();
- tag.TagId.ShouldBe("tag-galaxy");
- tag.EquipmentId.ShouldBe("eq-1");
- tag.DriverInstanceId.ShouldBe("drv-galaxy");
- tag.Name.ShouldBe("TestChangingInt");
- tag.DataType.ShouldBe("Int32");
- tag.FullName.ShouldBe("TestMachine_020.TestChangingInt");
- // The input Tag.FolderPath is null; the composer coalesces it to string.Empty (the explicit
- // byte-parity null-coalesce the artifact-decode side mirrors).
- tag.FolderPath.ShouldBe(string.Empty);
+ // Dark until Batch 4: see class summary. EquipmentTags is emitted empty this batch and the
+ // Tag.EquipmentId binding this test relied on is retired (Tag is raw-only). The Batch-4 seam
+ // fans equipment tags in via UnsTagReference; re-author the assertion then.
}
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerDeviceHostTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerDeviceHostTests.cs
index 36285256..8393f9a3 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerDeviceHostTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerDeviceHostTests.cs
@@ -1,98 +1,27 @@
-using Shouldly;
using Xunit;
-using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
///
-/// Covers follow-up E projection: carries the equipment's
-/// DriverInstanceId / DeviceId bindings and the resolved DeviceHost (parsed from
-/// the bound 's schemaless DeviceConfig JSON via the shared
-/// DeviceConfigIntent.TryExtractHost). A later task grafts a driver's discovered
-/// FixedTree onto a zero-tag equipment and partitions a multi-device driver by host using these.
-/// The direct host-extraction + normalization unit tests moved to
-/// Commons.Tests/DeviceConfigIntentTests (R2-11) — this suite keeps the Compose-level projection
-/// coverage.
+/// RETIRED (v3): this suite characterized follow-up E's projection of an
+/// equipment's DriverInstanceId / DeviceId bindings + the resolved DeviceHost
+/// (parsed from the bound device's DeviceConfig). In v3 the equipment↔device binding is
+/// architecturally removed — Equipment no longer carries DriverInstanceId/DeviceId,
+/// AddressSpaceComposer.Compose no longer takes a devices list, and equipment references
+/// raw tags via UnsTagReference instead of hosting a device. EquipmentNode's
+/// driver/device/host fields are therefore always null.
+/// The coverage did not vanish, it moved: the device-endpoint merge is exercised by
+/// Runtime.Tests.Drivers.DeploymentArtifactRawPathTests (nested RawPath + device-endpoint merge),
+/// and the host-extraction/normalization unit lives in Commons.Tests.DeviceConfigIntentTests
+/// (R2-11). The Skipped placeholder below preserves the retirement rationale in test output.
///
public sealed class AddressSpaceComposerDeviceHostTests
{
- /// An equipment bound to a driver + a device whose config carries a top-level
- /// HostAddress resolves all three fields, with the host trimmed + lower-cased.
- [Fact]
+ [Fact(Skip = DarkAddressSpaceReasons.EquipmentDeviceBindingRetired)]
public void Compose_resolves_driver_device_and_device_host()
{
- var equipment = new[] { NewEquipment("eq-1", driver: "d1", device: "dev1") };
- var devices = new[] { NewDevice("dev1", "d1", "{\"HostAddress\":\"10.0.0.5:8193\"}") };
-
- var node = AddressSpaceComposer.Compose(
- Array.Empty(), Array.Empty(), equipment,
- Array.Empty(), Array.Empty(), devices: devices)
- .EquipmentNodes.ShouldHaveSingleItem();
-
- node.EquipmentId.ShouldBe("eq-1");
- node.DriverInstanceId.ShouldBe("d1");
- node.DeviceId.ShouldBe("dev1");
- node.DeviceHost.ShouldBe("10.0.0.5:8193");
+ // Retired: see class summary. Equipment no longer binds a driver/device; the EquipmentNode
+ // driver/device/host projection is gone. Coverage moved to DeploymentArtifactRawPathTests
+ // (device-endpoint merge) + DeviceConfigIntentTests (host extraction).
}
-
- /// An equipment with no driver and no device → all three new fields null (driver-less,
- /// no device to resolve a host from).
- [Fact]
- public void Compose_equipment_without_driver_or_device_yields_null_bindings()
- {
- var equipment = new[] { NewEquipment("eq-1", driver: null, device: null) };
-
- var node = AddressSpaceComposer.Compose(
- Array.Empty(), Array.Empty(), equipment,
- Array.Empty(), Array.Empty(), devices: Array.Empty())
- .EquipmentNodes.ShouldHaveSingleItem();
-
- node.DriverInstanceId.ShouldBeNull();
- node.DeviceId.ShouldBeNull();
- node.DeviceHost.ShouldBeNull();
- }
-
- /// A bound DeviceId with no matching device row, or a device whose config has no
- /// HostAddress, resolves DeviceHost to null while DeviceId is still carried.
- [Fact]
- public void Compose_device_host_is_null_when_unresolvable()
- {
- var equipment = new[]
- {
- NewEquipment("eq-missing", driver: "d1", device: "dev-missing"),
- NewEquipment("eq-nohost", driver: "d1", device: "dev-nohost"),
- };
- var devices = new[] { NewDevice("dev-nohost", "d1", "{\"Port\":502}") };
-
- var nodes = AddressSpaceComposer.Compose(
- Array.Empty(), Array.Empty(), equipment,
- Array.Empty(), Array.Empty(), devices: devices)
- .EquipmentNodes;
-
- var missing = nodes.Single(n => n.EquipmentId == "eq-missing");
- missing.DeviceId.ShouldBe("dev-missing");
- missing.DeviceHost.ShouldBeNull();
-
- var noHost = nodes.Single(n => n.EquipmentId == "eq-nohost");
- noHost.DeviceId.ShouldBe("dev-nohost");
- noHost.DeviceHost.ShouldBeNull();
- }
-
- private static Equipment NewEquipment(string id, string? driver, string? device) => new()
- {
- EquipmentId = id,
- DriverInstanceId = driver,
- DeviceId = device,
- UnsLineId = "line-1",
- Name = id,
- MachineCode = id.ToUpperInvariant(),
- };
-
- private static Device NewDevice(string deviceId, string driverInstanceId, string deviceConfig) => new()
- {
- DeviceId = deviceId,
- DriverInstanceId = driverInstanceId,
- Name = deviceId,
- DeviceConfig = deviceConfig,
- };
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerEquipTokenTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerEquipTokenTests.cs
index 3f216ab3..8f7c32c9 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerEquipTokenTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerEquipTokenTests.cs
@@ -1,7 +1,6 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
-using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
@@ -10,6 +9,13 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// reserved {{equip}} token in a shared VirtualTag script with each owning equipment's
/// derived tag base (from its child-tag FullNames) — so one script reused across N
/// identical machines resolves to N machine-specific dependency graphs.
+///
+/// v3 Batch-1 DARK: the per-equipment tag base is DERIVED from equipment-tag
+/// FullNames, and equipment-tag variable plans do not materialize until Batch 4 (the composer
+/// no longer takes tags — baseByEquip is empty this batch). With no base to substitute, the
+/// {{equip}} token stays literal and these per-machine assertions have nothing to compare, so
+/// the test is Skipped-with-reason. The seed + assertions are preserved verbatim (minus the retired
+/// entity columns) so re-enabling in Batch 4 is a one-line change once the fan-out feeds the base.
///
public sealed class AddressSpaceComposerEquipTokenTests
{
@@ -17,21 +23,17 @@ public sealed class AddressSpaceComposerEquipTokenTests
/// to two equipments (TestMachine_001 / _002) each with one equipment Tag whose FullName carries
/// the per-machine base. Compose must expand the token per equipment in both the Expression and
/// the parsed DependencyRefs.
- [Fact]
+ [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
public void Compose_substitutes_equip_token_per_equipment()
{
- var ns = new Namespace
- {
- NamespaceId = "ns-eq",
- ClusterId = "c1",
- Kind = NamespaceKind.Equipment,
- NamespaceUri = "urn:eq",
- };
+ // v3: no Namespace entity; DriverInstance no longer binds a Namespace; Equipment no longer
+ // binds a driver. In Batch 4 the {{equip}} base derives from the equipment's child-tag
+ // FullNames — here TestMachine_001.Source / TestMachine_002.Source — which flow through the
+ // (dark this batch) equipment-tag plan set. Seeded for intent; not passed to Compose.
var driver1 = new DriverInstance
{
DriverInstanceId = "drv-1",
ClusterId = "c1",
- NamespaceId = "ns-eq",
Name = "Modbus1",
DriverType = "Modbus",
DriverConfig = "{}",
@@ -40,35 +42,14 @@ public sealed class AddressSpaceComposerEquipTokenTests
{
DriverInstanceId = "drv-2",
ClusterId = "c1",
- NamespaceId = "ns-eq",
Name = "Modbus2",
DriverType = "Modbus",
DriverConfig = "{}",
};
var area = new UnsArea { UnsAreaId = "area-1", ClusterId = "c1", Name = "filling" };
var line = new UnsLine { UnsLineId = "line-1", UnsAreaId = "area-1", Name = "line-1" };
- var equip1 = new Equipment { EquipmentId = "eq-1", DriverInstanceId = "drv-1", UnsLineId = "line-1", Name = "TestMachine_001", MachineCode = "TESTMACHINE_001" };
- var equip2 = new Equipment { EquipmentId = "eq-2", DriverInstanceId = "drv-2", UnsLineId = "line-1", Name = "TestMachine_002", MachineCode = "TESTMACHINE_002" };
- var tag1 = new Tag
- {
- TagId = "tag-1",
- DriverInstanceId = "drv-1",
- EquipmentId = "eq-1",
- Name = "Source",
- DataType = "Int32",
- AccessLevel = TagAccessLevel.Read,
- TagConfig = "{\"FullName\":\"TestMachine_001.Source\",\"DataType\":\"Int32\"}",
- };
- var tag2 = new Tag
- {
- TagId = "tag-2",
- DriverInstanceId = "drv-2",
- EquipmentId = "eq-2",
- Name = "Source",
- DataType = "Int32",
- AccessLevel = TagAccessLevel.Read,
- TagConfig = "{\"FullName\":\"TestMachine_002.Source\",\"DataType\":\"Int32\"}",
- };
+ var equip1 = new Equipment { EquipmentId = "eq-1", UnsLineId = "line-1", Name = "TestMachine_001", MachineCode = "TESTMACHINE_001" };
+ var equip2 = new Equipment { EquipmentId = "eq-2", UnsLineId = "line-1", Name = "TestMachine_002", MachineCode = "TESTMACHINE_002" };
var script = new Script
{
ScriptId = "s-equip",
@@ -82,7 +63,6 @@ public sealed class AddressSpaceComposerEquipTokenTests
var result = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip1, equip2 },
new[] { driver1, driver2 }, Array.Empty(),
- new[] { tag1, tag2 }, new[] { ns },
virtualTags: new[] { vt1, vt2 },
scripts: new[] { script });
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerPurityTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerPurityTests.cs
index 075cd5f9..13baacae 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerPurityTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerPurityTests.cs
@@ -136,7 +136,6 @@ public sealed class AddressSpaceComposerPurityTests
var result = AddressSpaceComposer.Compose(
Array.Empty(), Array.Empty(), Array.Empty(),
Array.Empty(), Array.Empty(),
- Array.Empty(), Array.Empty(),
virtualTags: new[] { vt },
scripts: new[] { script });
@@ -174,7 +173,6 @@ public sealed class AddressSpaceComposerPurityTests
var result = AddressSpaceComposer.Compose(
Array.Empty(), Array.Empty(), Array.Empty(),
Array.Empty(), Array.Empty(),
- Array.Empty(), Array.Empty(),
virtualTags: new[] { vt },
scripts: new[] { script });
@@ -200,7 +198,6 @@ public sealed class AddressSpaceComposerPurityTests
var result = AddressSpaceComposer.Compose(
Array.Empty(), Array.Empty(), Array.Empty(),
Array.Empty(), Array.Empty(),
- Array.Empty(), Array.Empty(),
virtualTags: new[] { vt },
scripts: Array.Empty