test(v3): migrate OpcUaServer.Tests + Host.IntegrationTests to v3 dark address space

Migrate the server composer/applier + 2-node harness tests off the retired
Namespace/NamespaceKind + equipment→driver/device binding schema onto the v3
greenfield shape (raw-only Tag; DriverInstance-RawFolderId; equipment via UnsLine).

OpcUaServer.Tests (134 build errors -> green, 335 pass / 4 skip):
- LIVE (migrated, passing): VirtualTag-historize + ScriptedAlarm composition,
  composer purity/hierarchy, applier MaterialiseHierarchy + MaterialiseEquipmentTags
  idempotency (hand-fed applier, not dark).
- SKIP dark-until-Batch-4: {{equip}} token expansion, applier equipment-namespace
  E2E (equipment-tag variable materialization).
- RETIRE-with-comment / skip: alias-tag (Tag.EquipmentId binding gone) + device-host
  equipment binding (architecturally removed).
- Add local DarkAddressSpaceReasons Skip-reason constants (mirrors Runtime.Tests).

Host.IntegrationTests (green, in-memory): drop deleted-Namespace seeds from
MultiClusterScoping + DriverReconnect; reshape EquipmentNamespaceMaterialization to
assert the DARK contract (folder nodes decode, EquipmentTags empty despite a seeded
v3 raw tag) with the full raw-tag->EquipmentTag materialization kept as a
Batch-4-pending Skip.

Tests-only; no src/ changes.
This commit is contained in:
Joseph Doherty
2026-07-15 21:41:04 -04:00
parent aaba2ebd66
commit 2c7fc65bae
11 changed files with 186 additions and 282 deletions
@@ -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.
/// </summary>
[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<ScriptedAlarm>(), new[] { tag }, new[] { ns });
Array.Empty<ScriptedAlarm>());
// 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");