docs+test(deploy): clarify driver-less attribution docs + no-line exclusion test (Task 2 review)
This commit is contained in:
@@ -209,9 +209,12 @@ public static class DeploymentArtifact
|
||||
|
||||
/// <summary>Cluster-scoped overload: the address-space composition a node should materialise given
|
||||
/// its NodeId. Filters every projection to the node's own ClusterId (see <see cref="ResolveClusterScope"/>).
|
||||
/// Equipment attribution is dual-path: driver-bound equipment (non-null DriverInstanceId) is kept when
|
||||
/// its driver is in-cluster; driver-less equipment (null DriverInstanceId) is kept when its UNS line's
|
||||
/// area is in-cluster.
|
||||
/// When <paramref name="onInconsistency"/> is supplied it is invoked with a human-readable message for each
|
||||
/// kept equipment whose owning UNS line is NOT in the node's cluster — a cross-cluster binding that
|
||||
/// violates the same-cluster invariant (decision #122) and would orphan the equipment folder. This is
|
||||
/// kept driver-bound equipment whose owning UNS line is NOT in the node's cluster — a cross-cluster binding
|
||||
/// that violates the same-cluster invariant (decision #122) and would orphan the equipment folder. This is
|
||||
/// detection only (observability); the equipment is still returned, since the upstream draft validator
|
||||
/// is the authority that should prevent the binding in the first place.</summary>
|
||||
/// <param name="blob">The deployment artifact blob.</param>
|
||||
@@ -234,6 +237,9 @@ public static class DeploymentArtifact
|
||||
if (onInconsistency is not null)
|
||||
{
|
||||
var keptLineIds = keptLines.Select(l => l.UnsLineId).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
// This cross-cluster check only fires for DRIVER-BOUND equipment. Driver-less equipment
|
||||
// is attributed by its UNS line's area cluster, so by construction its line is always in
|
||||
// keptLines — the condition `!keptLineIds.Contains(e.UnsLineId)` is never true for it.
|
||||
foreach (var e in keptEquipment)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(e.UnsLineId) && !keptLineIds.Contains(e.UnsLineId))
|
||||
@@ -265,7 +271,9 @@ public static class DeploymentArtifact
|
||||
private sealed record ClusterSets(HashSet<string> DriverIds, HashSet<string> AreaIds, HashSet<string> EquipmentIds);
|
||||
|
||||
/// <summary>Build the in-cluster id sets used to filter a composition: DriverInstanceIds + UnsAreaIds
|
||||
/// that directly carry the ClusterId, plus EquipmentIds whose DriverInstanceId is in-cluster.</summary>
|
||||
/// that directly carry the ClusterId, plus in-cluster EquipmentIds — driver-bound equipment attributed
|
||||
/// by its driver's cluster, and driver-less equipment (null DriverInstanceId) attributed by its UNS
|
||||
/// line's area cluster.</summary>
|
||||
/// <param name="blob">The deployment artifact blob.</param>
|
||||
/// <param name="clusterId">The node's ClusterId to scope to.</param>
|
||||
/// <returns>The resolved in-cluster id sets (empty on parse failure => empty composition).</returns>
|
||||
|
||||
@@ -579,6 +579,44 @@ public sealed class DeploymentArtifactTests
|
||||
comp.EquipmentVirtualTags.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies driver-less equipment with no UnsLineId (null/absent) is EXCLUDED from the scoped
|
||||
/// composition and throws nothing: without a line there is no area chain to resolve a cluster from.</summary>
|
||||
[Fact]
|
||||
public void Driverless_equipment_excluded_when_it_has_no_line()
|
||||
{
|
||||
var blob = BlobOf(new
|
||||
{
|
||||
Clusters = new[] { new { ClusterId = "C1" }, new { ClusterId = "C2" } },
|
||||
Nodes = new[]
|
||||
{
|
||||
new { NodeId = "c1-1:4053", ClusterId = "C1" },
|
||||
new { NodeId = "c2-1:4053", ClusterId = "C2" },
|
||||
},
|
||||
DriverInstances = new[]
|
||||
{
|
||||
// One driver per cluster so the artifact is genuinely multi-cluster (scoped, not None).
|
||||
new { DriverInstanceId = "c1-driver", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "C1", NamespaceId = "c1-ns" },
|
||||
new { DriverInstanceId = "c2-driver", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "C2", NamespaceId = "c2-ns" },
|
||||
},
|
||||
UnsAreas = new[] { new { UnsAreaId = "A1", Name = "Area1", ClusterId = "C1" } },
|
||||
// Equipment has no DriverInstanceId (driver-less) and no UnsLineId — cannot resolve a cluster.
|
||||
Equipment = new[]
|
||||
{
|
||||
new { EquipmentId = "E-noline", Name = "NoLine" },
|
||||
},
|
||||
Scripts = new[] { new { ScriptId = "scr", SourceCode = "return 1;" } },
|
||||
VirtualTags = new[]
|
||||
{
|
||||
new { VirtualTagId = "vt-noline", EquipmentId = "E-noline", Name = "VNL", DataType = "Float", ScriptId = "scr" },
|
||||
},
|
||||
});
|
||||
|
||||
var comp = DeploymentArtifact.ParseComposition(blob, "c1-1:4053");
|
||||
|
||||
comp.EquipmentNodes.ShouldBeEmpty();
|
||||
comp.EquipmentVirtualTags.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies the unchanged driver-bound path: equipment with an in-cluster DriverInstanceId is
|
||||
/// kept even when (as here) no UNS line/area rows describe it — attribution is still by driver.</summary>
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user