merge: equipment-namespace live values (VirtualTag route)
v2-ci / build (push) Failing after 36s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

This commit is contained in:
Joseph Doherty
2026-06-07 09:33:21 -04:00
17 changed files with 1671 additions and 13 deletions
@@ -251,6 +251,50 @@ public sealed class DeploymentArtifactTests
c.GalaxyTags.ShouldContain(g => g.TagId == "tag-gx");
}
/// <summary>
/// Verifies ParseComposition surfaces Equipment-namespace VirtualTags (joined to their Script
/// by ScriptId for the expression source) as <c>EquipmentVirtualTags</c>, with the
/// <c>DependencyRefs</c> extracted from the script's <c>ctx.GetTag("…")</c> literals — the
/// artifact-decode mirror of <c>Phase7Composer.Compose</c>'s VirtualTag producer.
/// </summary>
[Fact]
public void ParseComposition_reads_EquipmentVirtualTags_from_virtualtags_and_scripts()
{
var blob = JsonSerializer.SerializeToUtf8Bytes(new
{
Scripts = new[]
{
new
{
ScriptId = "scr-1",
SourceCode = "return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;",
},
},
VirtualTags = new[]
{
new
{
VirtualTagId = "vt-1",
EquipmentId = "eq-1",
Name = "Doubled",
DataType = "Float",
ScriptId = "scr-1",
},
},
});
var c = DeploymentArtifact.ParseComposition(blob);
var vt = c.EquipmentVirtualTags.ShouldHaveSingleItem();
vt.VirtualTagId.ShouldBe("vt-1");
vt.EquipmentId.ShouldBe("eq-1");
vt.Name.ShouldBe("Doubled");
vt.DataType.ShouldBe("Float");
vt.FolderPath.ShouldBe("");
vt.Expression.ShouldBe("return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;");
vt.DependencyRefs.ShouldBe(new[] { "TestMachine_001.TestDouble" });
}
/// <summary>
/// Verifies ParseComposition sets the equipment folder DisplayName to the UNS <c>Name</c>
/// segment — the source the live rebuild actually uses — not the colloquial MachineCode, so
@@ -377,6 +421,47 @@ public sealed class DeploymentArtifactTests
comp.DriverInstancePlans.ShouldBeEmpty();
}
/// <summary>Verifies the cluster-scoped overload keeps only EquipmentVirtualTags whose EquipmentId
/// belongs to an in-cluster driver (mirroring how EquipmentTags + ScriptedAlarms are filtered).</summary>
[Fact]
public void ParseComposition_scoped_keeps_only_my_clusters_virtual_tags()
{
var blob = BlobOf(new
{
Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
Nodes = new[]
{
new { NodeId = "central-1:4053", ClusterId = "MAIN" },
new { NodeId = "site-a-1:4053", ClusterId = "SITE-A" },
},
DriverInstances = new[]
{
new { DriverInstanceId = "main-modbus", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "MAIN", NamespaceId = "main-ns" },
new { DriverInstanceId = "sa-modbus", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "SITE-A", NamespaceId = "sa-ns" },
},
Equipment = new[]
{
new { EquipmentId = "eq-main", Name = "eqm", UnsLineId = "l1", DriverInstanceId = "main-modbus" },
new { EquipmentId = "eq-sa", Name = "eqs", UnsLineId = "l2", DriverInstanceId = "sa-modbus" },
},
Scripts = new[]
{
new { ScriptId = "scr", SourceCode = "return 1;" },
},
VirtualTags = new[]
{
new { VirtualTagId = "vt-main", EquipmentId = "eq-main", Name = "VM", DataType = "Float", ScriptId = "scr" },
new { VirtualTagId = "vt-sa", EquipmentId = "eq-sa", Name = "VS", DataType = "Float", ScriptId = "scr" },
},
});
var main = DeploymentArtifact.ParseComposition(blob, "central-1:4053");
main.EquipmentVirtualTags.Select(v => v.VirtualTagId).ShouldBe(new[] { "vt-main" });
var siteA = DeploymentArtifact.ParseComposition(blob, "site-a-1:4053");
siteA.EquipmentVirtualTags.Select(v => v.VirtualTagId).ShouldBe(new[] { "vt-sa" });
}
[Fact]
public void ParseComposition_single_cluster_node_id_overload_matches_legacy()
{
@@ -0,0 +1,141 @@
using System.Text.Json;
using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Fleet;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary>
/// Verifies the live-deploy wiring of <see cref="VirtualTagHostActor"/>: the
/// <see cref="DriverHostActor"/> must forward the composition's
/// <c>EquipmentVirtualTags</c> to its spawned VirtualTag host via
/// <see cref="VirtualTagHostActor.ApplyVirtualTags"/> on BOTH the fresh-apply path and the
/// bootstrap-restore path (both route through <c>PushDesiredSubscriptions</c>). The host is
/// injected as a <see cref="Akka.TestKit.TestProbe"/> via the Props override seam so the
/// ApplyVirtualTags can be intercepted.
/// </summary>
public sealed class DriverHostActorVirtualTagTests : RuntimeActorTestBase
{
private static readonly NodeId TestNode = NodeId.Parse("driver-vt-test");
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
/// <summary>Fresh apply: dispatching a deployment whose artifact carries one Equipment
/// VirtualTag forwards an <see cref="VirtualTagHostActor.ApplyVirtualTags"/> carrying that
/// plan to the injected VirtualTag host.</summary>
[Fact]
public void Apply_forwards_EquipmentVirtualTags_to_virtual_tag_host()
{
var db = NewInMemoryDbFactory();
var deploymentId = SeedDeploymentWithVirtualTag(db, RevA);
var coordinator = CreateTestProbe();
var vtHost = CreateTestProbe();
var actor = Sys.ActorOf(DriverHostActor.Props(
db, TestNode, coordinator.Ref,
localRoles: new HashSet<string> { "driver" },
virtualTagEvaluator: NullVirtualTagEvaluator.Instance,
virtualTagHostOverride: vtHost.Ref));
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
var apply = vtHost.ExpectMsg<VirtualTagHostActor.ApplyVirtualTags>(TimeSpan.FromSeconds(5));
var plan = apply.Plans.ShouldHaveSingleItem();
plan.VirtualTagId.ShouldBe("vt-1");
plan.EquipmentId.ShouldBe("eq-1");
plan.Name.ShouldBe("Doubled");
}
/// <summary>Bootstrap-restore: a node that already has an <c>Applied</c> NodeDeploymentState
/// row for a VirtualTag-carrying deployment re-forwards the
/// <see cref="VirtualTagHostActor.ApplyVirtualTags"/> on PreStart (no dispatch needed), so a
/// restarted node restores its live VirtualTag children.</summary>
[Fact]
public void Restore_on_bootstrap_forwards_EquipmentVirtualTags_to_virtual_tag_host()
{
var db = NewInMemoryDbFactory();
var deploymentId = SeedDeploymentWithVirtualTag(db, RevA);
SeedAppliedNodeState(db, deploymentId);
var coordinator = CreateTestProbe();
var vtHost = CreateTestProbe();
// No DispatchDeployment — Bootstrap() should detect the Applied row and run RestoreApplied,
// which routes through PushDesiredSubscriptions and forwards ApplyVirtualTags.
Sys.ActorOf(DriverHostActor.Props(
db, TestNode, coordinator.Ref,
localRoles: new HashSet<string> { "driver" },
virtualTagEvaluator: NullVirtualTagEvaluator.Instance,
virtualTagHostOverride: vtHost.Ref));
var apply = vtHost.ExpectMsg<VirtualTagHostActor.ApplyVirtualTags>(TimeSpan.FromSeconds(5));
apply.Plans.ShouldHaveSingleItem().VirtualTagId.ShouldBe("vt-1");
}
private static DeploymentId SeedDeploymentWithVirtualTag(
IDbContextFactory<OtOpcUaConfigDbContext> db, RevisionHash rev)
{
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
{
Scripts = new[]
{
new
{
ScriptId = "scr-1",
SourceCode = "return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;",
},
},
VirtualTags = new[]
{
new
{
VirtualTagId = "vt-1",
EquipmentId = "eq-1",
Name = "Doubled",
DataType = "Float",
ScriptId = "scr-1",
},
},
});
var id = DeploymentId.NewId();
using var ctx = db.CreateDbContext();
ctx.Deployments.Add(new Deployment
{
DeploymentId = id.Value,
RevisionHash = rev.Value,
Status = DeploymentStatus.Sealed,
CreatedBy = "test",
SealedAtUtc = DateTime.UtcNow,
ArtifactBlob = artifact,
});
ctx.SaveChanges();
return id;
}
private static void SeedAppliedNodeState(
IDbContextFactory<OtOpcUaConfigDbContext> db, DeploymentId deploymentId)
{
using var ctx = db.CreateDbContext();
ctx.NodeDeploymentStates.Add(new NodeDeploymentState
{
NodeId = TestNode.Value,
DeploymentId = deploymentId.Value,
Status = NodeDeploymentStatus.Applied,
StartedAtUtc = DateTime.UtcNow,
AppliedAtUtc = DateTime.UtcNow,
});
ctx.SaveChanges();
}
}