refactor(opcuaserver): rename Phase7* address-space pipeline to AddressSpace*
v2-ci / build (push) Failing after 37s
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

The OPC UA address-space build pipeline was named after a v2-roadmap
milestone number rather than its domain. Rename the family to describe
what it does (build/diff/apply the OPC UA address space):

  Phase7Composer          -> AddressSpaceComposer
  Phase7CompositionResult -> AddressSpaceComposition
  Phase7Planner           -> AddressSpacePlanner
  Phase7Plan              -> AddressSpacePlan
  Phase7Applier           -> AddressSpaceApplier
  Phase7ApplyOutcome      -> AddressSpaceApplyOutcome

The 9 Phase7*Tests suites follow suit; Phase7ScriptingEntitiesTests ->
ScriptingEntitiesTests (it tests the scripting migration, not the
pipeline). Log-message prefixes move to the new class names.

Pure mechanical rename, no behavioral change. EF migration classes/IDs
(AddPhase7ScriptingTables, ExtendComputeGenerationDiffWithPhase7) are
immutable and left untouched, as are historical design docs.

Build clean; OpcUaServer 261/261, Runtime 272/272, ScriptingEntities
12/12 green.
This commit is contained in:
Joseph Doherty
2026-06-18 19:16:28 -04:00
parent 6af54ac935
commit 40e8a23e7c
44 changed files with 364 additions and 364 deletions
@@ -73,7 +73,7 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
/// <summary>Forwards an in-place tag-attribute update (F10b) to the inner sink when it supports the
/// surgical capability. Returns false otherwise — before the real <c>SdkAddressSpaceSink</c> is
/// swapped in (inner is still the null sink), or any inner sink that isn't surgical — so the caller
/// (Phase7Applier) falls back to a full rebuild. Without this forward the surgical optimization is
/// (AddressSpaceApplier) falls back to a full rebuild. Without this forward the surgical optimization is
/// inert on every driver-role host, because actors inject THIS wrapper, not the inner sink.</summary>
/// <param name="variableNodeId">The node ID of the variable to update in place.</param>
/// <param name="writable">Whether the node should be read/write.</param>
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
/// Single source of truth for equipment-namespace OPC UA NodeId strings. The variable NodeId is
/// FOLDER-SCOPED (<c>{parent}/{Name}</c>), NOT the driver-side FullName — a driver wire ref is not
/// unique across identical machines, so FullName-as-NodeId would collide in the sink. Used by the
/// materialiser (Phase7Applier), the VirtualTag publish map, and the driver live-value router so all
/// materialiser (AddressSpaceApplier), the VirtualTag publish map, and the driver live-value router so all
/// three agree on the exact NodeId a variable was placed at.
/// </summary>
public static class EquipmentNodeIds
@@ -31,7 +31,7 @@ public interface IOpcUaAddressSpaceSink
/// Materialise a real OPC UA Part 9 alarm-condition node under its equipment folder so clients
/// can browse it as a proper condition (with basic Active/Ack state). The node id equals the
/// alarm node id (the ScriptedAlarmId) so subsequent <see cref="WriteAlarmCondition"/> calls update
/// it. Used by <c>Phase7Applier.MaterialiseScriptedAlarms</c>. Idempotent.
/// it. Used by <c>AddressSpaceApplier.MaterialiseScriptedAlarms</c>. Idempotent.
/// </summary>
/// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId); becomes the condition's NodeId.</param>
/// <param name="equipmentNodeId">The equipment folder node ID the condition parents under.</param>
@@ -44,7 +44,7 @@ public interface IOpcUaAddressSpaceSink
void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false);
/// <summary>
/// Ensure a folder node exists under the given parent. Used by <c>Phase7Applier</c> to
/// Ensure a folder node exists under the given parent. Used by <c>AddressSpaceApplier</c> to
/// materialise the UNS Area/Line/Equipment hierarchy in the address space. When
/// <paramref name="parentNodeId"/> is null the folder is parented under the namespace
/// root. Idempotent: calling twice with the same id is safe.
@@ -58,7 +58,7 @@ public interface IOpcUaAddressSpaceSink
/// Ensure a Variable node exists at <paramref name="variableNodeId"/>, parented under
/// <paramref name="parentFolderNodeId"/> (or the namespace root when null). Created with
/// Bad quality + null value; subsequent <see cref="WriteValue"/> calls update both.
/// Used by <c>Phase7Applier</c> to materialise equipment-namespace tags ahead of any
/// Used by <c>AddressSpaceApplier</c> to materialise equipment-namespace tags ahead of any
/// driver-side subscribe so OPC UA clients can browse them. Idempotent.
/// </summary>
/// <param name="variableNodeId">The OPC UA node ID for the variable.</param>
@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
/// <summary>Optional capability on an address-space sink: surgical in-place attribute updates on an
/// EXISTING variable node, used by Phase7Applier to avoid a full RebuildAddressSpace for pure-property
/// EXISTING variable node, used by AddressSpaceApplier to avoid a full RebuildAddressSpace for pure-property
/// tag changes (Writable / Historizing). A sink that does not implement it ⇒ caller falls back to a
/// full rebuild (safe default).</summary>
public interface ISurgicalAddressSpaceSink
@@ -88,7 +88,7 @@ public static class EquipmentScriptPaths
/// <summary>
/// Distinct <c>ctx.GetTag("ref")</c> string literals in first-seen order — the
/// dependency refs the <c>VirtualTagActor</c> subscribes to. The single shared copy
/// formerly duplicated in <c>Phase7Composer</c> + <c>DeploymentArtifact</c>. GetTag
/// formerly duplicated in <c>AddressSpaceComposer</c> + <c>DeploymentArtifact</c>. GetTag
/// only (writes are not dependencies).
/// </summary>
/// <param name="scriptSource">The (already substituted) script source.</param>
@@ -112,7 +112,7 @@ public static class EquipmentScriptPaths
/// UNION the distinct <c>{TagPath}</c> token paths referenced in the message template (first-seen
/// order, appended after the predicate reads, trimmed + non-empty). The reserved
/// <c>{{equip}}</c> double-brace form is excluded by the token regex. Deterministic so the live
/// composer (<c>Phase7Composer</c>) and the artifact-decode mirror (<c>DeploymentArtifact</c>)
/// composer (<c>AddressSpaceComposer</c>) and the artifact-decode mirror (<c>DeploymentArtifact</c>)
/// produce the exact same ordered list — the byte-parity contract <c>EquipmentScriptedAlarmPlan</c>
/// equality depends on. Scripted alarms do NOT use <c>{{equip}}</c> substitution (only virtual
/// tags do) — pass the predicate source as-is.
@@ -54,7 +54,7 @@ public static class DraftValidator
}
// Minimal reader for the top-level "FullName" string in a tag's schemaless TagConfig JSON
// (mirrors Phase7Composer.ExtractTagFullName — a small local copy, consistent with this codebase
// (mirrors AddressSpaceComposer.ExtractTagFullName — a small local copy, consistent with this codebase
// where the composer keeps its own).
private static string? ExtractTagConfigFullName(string? tagConfig)
{
@@ -139,7 +139,7 @@
@* Driver-agnostic server-side HistoryRead intent. Distinct from the native-alarm
"Historize to AVEVA" toggle below: THIS gates TAG-VALUE history (root keys
`isHistorized` / `historianTagname`, read by Phase7Composer.ExtractTagHistorize),
`isHistorized` / `historianTagname`, read by AddressSpaceComposer.ExtractTagHistorize),
merged onto the canonical TagConfig via the pure TagHistorizeConfig seam so it
composes with the typed editor's driver-specific fields (both preserve unknown keys).
Shown for EVERY driver once one is picked. *@
@@ -41,15 +41,15 @@ public sealed record ScriptTagInfo(string Path, string Kind, string DataType, st
/// <para>
/// <b>Fidelity over breadth.</b> Verified: the live runtime resolves a <c>ctx.GetTag("X")</c>
/// literal against the driver <c>FullName</c> — the resolution chain is
/// <c>Phase7Composer</c> (via <c>EquipmentScriptPaths.ExtractDependencyRefs</c>) harvesting the <c>ctx.GetTag("…")</c> literals
/// <c>AddressSpaceComposer</c> (via <c>EquipmentScriptPaths.ExtractDependencyRefs</c>) harvesting the <c>ctx.GetTag("…")</c> literals
/// into <c>EquipmentVirtualTagPlan.DependencyRefs</c>
/// (<c>src/Server/…OpcUaServer/Phase7Composer.cs</c>); those become
/// (<c>src/Server/…OpcUaServer/AddressSpaceComposer.cs</c>); those become
/// <c>VirtualTagActor._dependencyRefs</c>, registered with the
/// <c>DependencyMuxActor</c>, whose <c>_byRef</c> map is keyed by
/// <c>DriverInstanceActor.AttributeValuePublished.FullReference</c>
/// (<c>src/Server/…Runtime/VirtualTags/DependencyMuxActor.cs:97</c>) — and that
/// <c>FullReference</c> is the <c>FullName</c> field extracted from <c>Tag.TagConfig</c>
/// (see <c>Phase7Composer.ExtractTagFullName</c> + <c>EquipmentNodeWalker.ExtractFullName</c>).
/// (see <c>AddressSpaceComposer.ExtractTagFullName</c> + <c>EquipmentNodeWalker.ExtractFullName</c>).
/// The UNS-path engine (<c>Core.VirtualTags.VirtualTagEngine</c>, keyed by a slash-joined
/// <c>Enterprise/Site/Area/Line/Equipment/TagName</c>) is dormant — it is NOT wired into the
/// host — so UNS browse paths never resolve at runtime and are intentionally NOT suggested.
@@ -175,7 +175,7 @@ public sealed class ScriptTagCatalog(IDbContextFactory<OtOpcUaConfigDbContext> d
/// <summary>
/// Extracts the driver-side full reference from a <c>Tag.TagConfig</c> JSON blob — the
/// top-level <c>FullName</c> string every shipped driver stores. Mirrors
/// <c>EquipmentNodeWalker.ExtractFullName</c> / <c>Phase7Composer.ExtractTagFullName</c>
/// <c>EquipmentNodeWalker.ExtractFullName</c> / <c>AddressSpaceComposer.ExtractTagFullName</c>
/// (AdminUI does not reference those assemblies). Falls back to the raw blob when it is not
/// a JSON object carrying a string <c>FullName</c>.
/// </summary>
@@ -223,7 +223,7 @@ public sealed class ScriptAnalysisService
if (inv.Expression is not MemberAccessExpressionSyntax ma) return false;
// Receiver guard: only ctx.GetTag(...) / ctx.SetVirtualTag(...) are real tag-path calls. Mirrors the
// runtime harvest (EquipmentScriptPaths.GetTagRefRegex is syntactically `ctx`-anchored), so the editor
// offers tag completions/hover for exactly what Phase7Composer harvests — not an unrelated x.GetTag(...).
// offers tag completions/hover for exactly what AddressSpaceComposer harvests — not an unrelated x.GetTag(...).
if (ma.Expression is not IdentifierNameSyntax { Identifier.ValueText: "ctx" }) return false;
var method = ma.Name.Identifier.ValueText;
if (method is not ("GetTag" or "SetVirtualTag")) return false;
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
/// the driver reads against). Preserves unrecognised JSON keys across a load→save.</summary>
/// <remarks>
/// The <c>FullName</c> key is intentionally PascalCase: the deploy-time composer + node walker
/// (<c>Phase7Composer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a
/// (<c>AddressSpaceComposer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a
/// case-sensitive <c>TryGetProperty("FullName", …)</c>, so the editor MUST persist that exact
/// casing. The driver-agnostic server-side HistoryRead intent keys (<c>isHistorized</c> /
/// <c>historianTagname</c>) are NOT modelled here — they are owned by the TagModal-merge seam
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
/// Typed working model for the optional native-alarm <c>alarm</c> sub-object inside a tag's
/// <c>TagConfig</c> JSON. A tag whose <c>TagConfig</c> carries an <c>alarm</c> object is materialised
/// as an OPC UA Part 9 condition (rather than a value variable); the fields here mirror what the
/// server's <c>Phase7Composer.ExtractTagAlarm</c> / <c>DeploymentArtifact.ExtractTagAlarm</c> parse.
/// server's <c>AddressSpaceComposer.ExtractTagAlarm</c> / <c>DeploymentArtifact.ExtractTagAlarm</c> parse.
///
/// <para>
/// <see cref="HistorizeToAveva"/> is the per-condition opt-out of the DURABLE AVEVA historian write
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
/// Preserves unrecognised JSON keys across a load→save.</summary>
/// <remarks>
/// The <c>FullName</c> key is intentionally PascalCase: the deploy-time composer + node walker
/// (<c>Phase7Composer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a
/// (<c>AddressSpaceComposer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a
/// case-sensitive <c>TryGetProperty("FullName", …)</c>, so the editor MUST persist that exact
/// casing. The driver-agnostic server-side HistoryRead intent keys (<c>isHistorized</c> /
/// <c>historianTagname</c>) are NOT modelled here — they are owned by the TagModal-merge seam
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
/// Pure, driver-agnostic merge helper for the two server-side HistoryRead intent keys at the ROOT of a
/// tag's <c>TagConfig</c> JSON: <c>isHistorized</c> (camelCase bool — omit/false default) and
/// <c>historianTagname</c> (camelCase string override — omit when blank). These map to what the server's
/// <c>Phase7Composer.ExtractTagHistorize</c> reads (see <c>docs/Historian.md</c>).
/// <c>AddressSpaceComposer.ExtractTagHistorize</c> reads (see <c>docs/Historian.md</c>).
///
/// <para>
/// This is the TagModal-merge seam: the TagModal owns the canonical TagConfig JSON; the driver's typed
@@ -4,9 +4,9 @@ using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// <summary>
/// Side-effecting orchestrator over <see cref="Phase7Plan"/>. Drives an
/// Side-effecting orchestrator over <see cref="AddressSpacePlan"/>. Drives an
/// <see cref="IOpcUaAddressSpaceSink"/> to materialise the diff between two
/// <see cref="Phase7CompositionResult"/> snapshots:
/// <see cref="AddressSpaceComposition"/> snapshots:
///
/// <list type="bullet">
/// <item>RemovedEquipment / RemovedAlarms — write Bad-quality on every removed
@@ -23,15 +23,15 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// production binds a real SDK sink, dev/Mac binds <see cref="NullOpcUaAddressSpaceSink"/>,
/// and tests can capture every call.
/// </summary>
public sealed class Phase7Applier
public sealed class AddressSpaceApplier
{
private readonly IOpcUaAddressSpaceSink _sink;
private readonly ILogger<Phase7Applier> _logger;
private readonly ILogger<AddressSpaceApplier> _logger;
/// <summary>Initializes a new instance of the Phase7Applier class.</summary>
/// <summary>Initializes a new instance of the AddressSpaceApplier class.</summary>
/// <param name="sink">The OPC UA address space sink to apply changes to.</param>
/// <param name="logger">The logger instance.</param>
public Phase7Applier(IOpcUaAddressSpaceSink sink, ILogger<Phase7Applier> logger)
public AddressSpaceApplier(IOpcUaAddressSpaceSink sink, ILogger<AddressSpaceApplier> logger)
{
ArgumentNullException.ThrowIfNull(sink);
ArgumentNullException.ThrowIfNull(logger);
@@ -44,15 +44,15 @@ public sealed class Phase7Applier
/// callers (OpcUaPublishActor) can correlate the work back to the originating deployment.
/// </summary>
/// <param name="plan">The plan to apply.</param>
/// <returns>A Phase7ApplyOutcome summarizing the applied changes.</returns>
public Phase7ApplyOutcome Apply(Phase7Plan plan)
/// <returns>A AddressSpaceApplyOutcome summarizing the applied changes.</returns>
public AddressSpaceApplyOutcome Apply(AddressSpacePlan plan)
{
ArgumentNullException.ThrowIfNull(plan);
if (plan.IsEmpty)
{
_logger.LogDebug("Phase7Applier: plan is empty; skipping sink writes");
return new Phase7ApplyOutcome(RemovedNodes: 0, AddedNodes: 0, ChangedNodes: 0, RebuildCalled: false);
_logger.LogDebug("AddressSpaceApplier: plan is empty; skipping sink writes");
return new AddressSpaceApplyOutcome(RemovedNodes: 0, AddedNodes: 0, ChangedNodes: 0, RebuildCalled: false);
}
var ts = DateTime.UtcNow;
@@ -68,7 +68,7 @@ public sealed class Phase7Applier
removedCount++;
}
// Removed equipment tags / VirtualTags are plain variable nodes (no Part 9 condition to write
// before tear-down), but they ARE real removals — count them so Phase7ApplyOutcome.RemovedNodes
// before tear-down), but they ARE real removals — count them so AddressSpaceApplyOutcome.RemovedNodes
// is accurate on a removed-tag-only deploy, which now reaches the rebuild path below.
removedCount += plan.RemovedEquipmentTags.Count + plan.RemovedEquipmentVirtualTags.Count;
@@ -136,7 +136,7 @@ public sealed class Phase7Applier
try { ok = surgical.UpdateTagAttributes(nodeId, writable, historian); }
catch (Exception ex)
{
_logger.LogError(ex, "Phase7Applier: surgical UpdateTagAttributes threw for {Node}", nodeId);
_logger.LogError(ex, "AddressSpaceApplier: surgical UpdateTagAttributes threw for {Node}", nodeId);
ok = false;
}
if (!ok) { allApplied = false; break; }
@@ -152,10 +152,10 @@ public sealed class Phase7Applier
}
_logger.LogInformation(
"Phase7Applier: applied plan (added={Added}, removed={Removed}, changed={Changed}, surgicalTags={Surgical}, rebuild={Rebuild})",
"AddressSpaceApplier: applied plan (added={Added}, removed={Removed}, changed={Changed}, surgicalTags={Surgical}, rebuild={Rebuild})",
addedCount, removedCount, changedCount, rebuilt ? 0 : surgicalTagDeltas.Count, rebuilt);
return new Phase7ApplyOutcome(removedCount, addedCount, changedCount, rebuilt);
return new AddressSpaceApplyOutcome(removedCount, addedCount, changedCount, rebuilt);
}
private void SafeRebuild()
@@ -166,7 +166,7 @@ public sealed class Phase7Applier
}
catch (Exception ex)
{
_logger.LogError(ex, "Phase7Applier: sink.RebuildAddressSpace threw");
_logger.LogError(ex, "AddressSpaceApplier: sink.RebuildAddressSpace threw");
}
}
@@ -178,7 +178,7 @@ public sealed class Phase7Applier
/// present, so re-applies are cheap.
/// </summary>
/// <param name="composition">The composition result containing the hierarchy to materialise.</param>
public void MaterialiseHierarchy(Phase7CompositionResult composition)
public void MaterialiseHierarchy(AddressSpaceComposition composition)
{
ArgumentNullException.ThrowIfNull(composition);
@@ -198,7 +198,7 @@ public sealed class Phase7Applier
}
_logger.LogInformation(
"Phase7Applier: hierarchy materialised (areas={Areas}, lines={Lines}, equipment={Equipment})",
"AddressSpaceApplier: hierarchy materialised (areas={Areas}, lines={Lines}, equipment={Equipment})",
composition.UnsAreas.Count, composition.UnsLines.Count, composition.EquipmentNodes.Count);
}
@@ -223,7 +223,7 @@ public sealed class Phase7Applier
/// </para>
/// </summary>
/// <param name="composition">The composition result containing the equipment tags to materialise.</param>
public void MaterialiseEquipmentTags(Phase7CompositionResult composition)
public void MaterialiseEquipmentTags(AddressSpaceComposition composition)
{
ArgumentNullException.ThrowIfNull(composition);
if (composition.EquipmentTags.Count == 0) return;
@@ -276,7 +276,7 @@ public sealed class Phase7Applier
}
_logger.LogInformation(
"Phase7Applier: equipment tags materialised (tags={Tags}, equipment={Equipment})",
"AddressSpaceApplier: equipment tags materialised (tags={Tags}, equipment={Equipment})",
composition.EquipmentTags.Count,
composition.EquipmentTags.Select(t => t.EquipmentId).Distinct(StringComparer.Ordinal).Count());
}
@@ -294,7 +294,7 @@ public sealed class Phase7Applier
/// Idempotent (per-variable idempotency relies on the sink's own <c>EnsureVariable</c>).
/// </summary>
/// <param name="composition">The composition result containing the equipment VirtualTags to materialise.</param>
public void MaterialiseEquipmentVirtualTags(Phase7CompositionResult composition)
public void MaterialiseEquipmentVirtualTags(AddressSpaceComposition composition)
{
ArgumentNullException.ThrowIfNull(composition);
if (composition.EquipmentVirtualTags.Count == 0) return;
@@ -328,7 +328,7 @@ public sealed class Phase7Applier
}
_logger.LogInformation(
"Phase7Applier: equipment virtualtags materialised (vtags={Vtags}, equipment={Equipment})",
"AddressSpaceApplier: equipment virtualtags materialised (vtags={Vtags}, equipment={Equipment})",
composition.EquipmentVirtualTags.Count,
composition.EquipmentVirtualTags.Select(v => v.EquipmentId).Distinct(StringComparer.Ordinal).Count());
}
@@ -343,7 +343,7 @@ public sealed class Phase7Applier
/// <c>MaterialiseAlarmCondition</c> re-creates cleanly on re-apply).
/// </summary>
/// <param name="composition">The composition result containing the scripted alarms to materialise.</param>
public void MaterialiseScriptedAlarms(Phase7CompositionResult composition)
public void MaterialiseScriptedAlarms(AddressSpaceComposition composition)
{
ArgumentNullException.ThrowIfNull(composition);
if (composition.EquipmentScriptedAlarms.Count == 0) return;
@@ -357,7 +357,7 @@ public sealed class Phase7Applier
}
_logger.LogInformation(
"Phase7Applier: scripted alarms materialised (alarms={Alarms}, equipment={Equipment})",
"AddressSpaceApplier: scripted alarms materialised (alarms={Alarms}, equipment={Equipment})",
materialised,
composition.EquipmentScriptedAlarms.Where(a => a.Enabled)
.Select(a => a.EquipmentId).Distinct(StringComparer.Ordinal).Count());
@@ -366,13 +366,13 @@ public sealed class Phase7Applier
private void SafeEnsureFolder(string nodeId, string? parentNodeId, string displayName)
{
try { _sink.EnsureFolder(nodeId, parentNodeId, displayName); }
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: EnsureFolder threw for {Node}", nodeId); }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: EnsureFolder threw for {Node}", nodeId); }
}
private void SafeEnsureVariable(string nodeId, string? parentNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
{
try { _sink.EnsureVariable(nodeId, parentNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength); }
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: EnsureVariable threw for {Node}", nodeId); }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: EnsureVariable threw for {Node}", nodeId); }
}
// A VirtualTag's materialised OPC UA node (MaterialiseEquipmentVirtualTags) is derived ONLY from
@@ -381,7 +381,7 @@ public sealed class Phase7Applier
// so a delta changing ONLY those three leaves a byte-identical node and needs no address-space rebuild.
// Whitelist-of-may-differ via `with` + the record's custom Equals: any OTHER field difference (current
// or future) makes the override unequal → falls back to a full rebuild (safe default).
private static bool VtagDeltaIsNodeIrrelevant(Phase7Plan.EquipmentVirtualTagDelta d) =>
private static bool VtagDeltaIsNodeIrrelevant(AddressSpacePlan.EquipmentVirtualTagDelta d) =>
(d.Previous with
{
Expression = d.Current.Expression,
@@ -394,7 +394,7 @@ public sealed class Phase7Applier
// ISurgicalAddressSpaceSink.UpdateTagAttributes, avoiding a full rebuild (preserving subscriptions).
// DataType / IsArray / ArrayLength / FullName / DriverInstanceId / identity / alarm differences fall
// through to a rebuild — the override-unequal default also covers any future field.
private static bool TagDeltaIsSurgicalEligible(Phase7Plan.EquipmentTagDelta d) =>
private static bool TagDeltaIsSurgicalEligible(AddressSpacePlan.EquipmentTagDelta d) =>
d.Previous.Alarm is null && d.Current.Alarm is null &&
(d.Previous with
{
@@ -418,18 +418,18 @@ public sealed class Phase7Applier
private void SafeWriteAlarmCondition(string nodeId, AlarmConditionSnapshot state, DateTime ts)
{
try { _sink.WriteAlarmCondition(nodeId, state, ts); }
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: WriteAlarmCondition threw for {Node}", nodeId); }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: WriteAlarmCondition threw for {Node}", nodeId); }
}
private void SafeMaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative)
{
try { _sink.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative); }
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: MaterialiseAlarmCondition threw for {Node}", alarmNodeId); }
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: MaterialiseAlarmCondition threw for {Node}", alarmNodeId); }
}
}
/// <summary>Summary of one apply pass. Useful for tests + audit-log entries on the deploy path.</summary>
public sealed record Phase7ApplyOutcome(
public sealed record AddressSpaceApplyOutcome(
int RemovedNodes,
int AddedNodes,
int ChangedNodes,
@@ -6,11 +6,11 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// <summary>Outcome of <see cref="Phase7Composer.Compose"/> — pure value tuple, no side effects.
/// <summary>Outcome of <see cref="AddressSpaceComposer.Compose"/> — pure value tuple, no side effects.
/// <see cref="UnsAreas"/> + <see cref="UnsLines"/> carry the UNS topology so the applier can
/// materialise the Area/Line/Equipment folder hierarchy in the address space; equipment carries
/// its parent line id so the applier knows where to hang each equipment folder.</summary>
public sealed record Phase7CompositionResult(
public sealed record AddressSpaceComposition(
IReadOnlyList<UnsAreaProjection> UnsAreas,
IReadOnlyList<UnsLineProjection> UnsLines,
IReadOnlyList<EquipmentNode> EquipmentNodes,
@@ -21,7 +21,7 @@ public sealed record Phase7CompositionResult(
/// <param name="equipmentNodes">The equipment nodes.</param>
/// <param name="driverInstancePlans">The driver instance plans.</param>
/// <param name="scriptedAlarmPlans">The scripted alarm plans.</param>
public Phase7CompositionResult(
public AddressSpaceComposition(
IReadOnlyList<EquipmentNode> equipmentNodes,
IReadOnlyList<DriverInstancePlan> driverInstancePlans,
IReadOnlyList<ScriptedAlarmPlan> scriptedAlarmPlans)
@@ -32,7 +32,7 @@ public sealed record Phase7CompositionResult(
/// <summary>
/// Equipment-namespace tags — a <see cref="Tag"/> with non-null <see cref="Tag.EquipmentId"/>
/// in an <c>Equipment</c>-kind namespace. <c>Phase7Applier.MaterialiseEquipmentTags</c>
/// in an <c>Equipment</c>-kind namespace. <c>AddressSpaceApplier.MaterialiseEquipmentTags</c>
/// materialises each as a Variable under its existing equipment folder. Declared as an
/// init-only member defaulting to empty (rather than a positional parameter) so every existing
/// convenience constructor + call site keeps compiling unchanged; new producers set it via
@@ -66,7 +66,7 @@ public sealed record ScriptedAlarmPlan(string ScriptedAlarmId, string EquipmentI
/// One Equipment-namespace tag from a <see cref="Tag"/> row whose <see cref="Tag.EquipmentId"/>
/// is non-null and whose owning driver's namespace is <c>Equipment</c>-kind. Carries the stable
/// <see cref="TagId"/> (diff identity), the parent <see cref="EquipmentId"/> folder (already
/// materialised by <c>Phase7Applier.MaterialiseHierarchy</c>) the variable hangs under, the
/// materialised by <c>AddressSpaceApplier.MaterialiseHierarchy</c>) the variable hangs under, the
/// optional <see cref="FolderPath"/> sub-folder, the leaf <see cref="Name"/> display, the OPC UA
/// <see cref="DataType"/>, and the driver-side <see cref="FullName"/> reference (extracted from
/// <c>Tag.TagConfig</c>) the later values milestone routes reads/writes by. The variable's NodeId
@@ -117,7 +117,7 @@ public sealed record EquipmentTagAlarmInfo(string AlarmType, int Severity, bool?
/// <summary>
/// One Equipment-namespace VirtualTag from a <see cref="VirtualTag"/> row (joined to its
/// <see cref="Script"/> for the expression). The VirtualTag value analogue of
/// <see cref="EquipmentTagPlan"/>: <c>Phase7Applier.MaterialiseEquipmentVirtualTags</c>
/// <see cref="EquipmentTagPlan"/>: <c>AddressSpaceApplier.MaterialiseEquipmentVirtualTags</c>
/// materialises each as a Variable under its equipment folder with a folder-scoped NodeId
/// (<c>EquipmentId/Name</c>, or <c>EquipmentId/FolderPath/Name</c> when a sub-folder is set),
/// and <c>VirtualTagHostActor</c> spawns a <c>VirtualTagActor</c> per plan that evaluates
@@ -252,19 +252,19 @@ public sealed record EquipmentScriptedAlarmPlan(
/// startup (Task 53) consumes the result and hands it to the node-manager factory.
///
/// #85 — the composer now carries UNS topology (<see cref="UnsAreaProjection"/> +
/// <see cref="UnsLineProjection"/>) so <c>Phase7Applier</c> can build the
/// <see cref="UnsLineProjection"/>) so <c>AddressSpaceApplier</c> can build the
/// <c>Area/Line/Equipment</c> folder hierarchy in the SDK's address space. The legacy
/// <c>EquipmentNodeWalker</c> integration that did this server-side is fully replaced by the
/// (composer → applier → sink → node manager) chain.
/// </summary>
public static class Phase7Composer
public static class AddressSpaceComposer
{
/// <summary>Convenience overload for legacy callers + tests that don't supply UNS topology or tags.</summary>
/// <param name="equipment">The equipment.</param>
/// <param name="driverInstances">The driver instances.</param>
/// <param name="scriptedAlarms">The scripted alarms.</param>
/// <returns>The composition result.</returns>
public static Phase7CompositionResult Compose(
public static AddressSpaceComposition Compose(
IReadOnlyList<Equipment> equipment,
IReadOnlyList<DriverInstance> driverInstances,
IReadOnlyList<ScriptedAlarm> scriptedAlarms) =>
@@ -278,7 +278,7 @@ public static class Phase7Composer
/// <param name="driverInstances">The driver instances.</param>
/// <param name="scriptedAlarms">The scripted alarms.</param>
/// <returns>The composition result.</returns>
public static Phase7CompositionResult Compose(
public static AddressSpaceComposition Compose(
IReadOnlyList<UnsArea> unsAreas,
IReadOnlyList<UnsLine> unsLines,
IReadOnlyList<Equipment> equipment,
@@ -300,7 +300,7 @@ public static class Phase7Composer
/// <param name="virtualTags">The Equipment-namespace virtual (calculated) tags. <c>null</c> = none.</param>
/// <param name="scripts">The scripts joined to <paramref name="virtualTags"/> by ScriptId for the expression. <c>null</c> = none.</param>
/// <returns>The composition result.</returns>
public static Phase7CompositionResult Compose(
public static AddressSpaceComposition Compose(
IReadOnlyList<UnsArea> unsAreas,
IReadOnlyList<UnsLine> unsLines,
IReadOnlyList<Equipment> equipment,
@@ -434,7 +434,7 @@ public static class Phase7Composer
if (!scriptsById.TryGetValue(a.PredicateScriptId, out var s))
{
Trace.TraceWarning(
"Phase7Composer: scripted alarm '{0}' (equipment '{1}') references predicate " +
"AddressSpaceComposer: scripted alarm '{0}' (equipment '{1}') references predicate " +
"script '{2}' which is not in the supplied scripts — skipping.",
a.ScriptedAlarmId, a.EquipmentId, a.PredicateScriptId);
continue;
@@ -458,7 +458,7 @@ public static class Phase7Composer
Enabled: a.Enabled));
}
return new Phase7CompositionResult(areas, lines, nodes, plans, alarms)
return new AddressSpaceComposition(areas, lines, nodes, plans, alarms)
{
EquipmentTags = equipmentTags,
EquipmentVirtualTags = equipmentVirtualTags,
@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// <summary>
/// Pure diff between two <see cref="Phase7CompositionResult"/> snapshots — the
/// Pure diff between two <see cref="AddressSpaceComposition"/> snapshots — the
/// <c>previous</c> currently-applied composition and the <c>next</c> from a freshly-applied
/// deployment. Three lists per entity class (Equipment / DriverInstance / ScriptedAlarm)
/// captured by stable identity: added items are new, removed items have to be torn down,
@@ -12,22 +12,22 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// nodes that actually changed — full tear-down + rebuild is reserved for first-boot or
/// drastic schema flips.
/// </summary>
public sealed record Phase7Plan(
public sealed record AddressSpacePlan(
IReadOnlyList<EquipmentNode> AddedEquipment,
IReadOnlyList<EquipmentNode> RemovedEquipment,
IReadOnlyList<Phase7Plan.EquipmentDelta> ChangedEquipment,
IReadOnlyList<AddressSpacePlan.EquipmentDelta> ChangedEquipment,
IReadOnlyList<DriverInstancePlan> AddedDrivers,
IReadOnlyList<DriverInstancePlan> RemovedDrivers,
IReadOnlyList<Phase7Plan.DriverDelta> ChangedDrivers,
IReadOnlyList<AddressSpacePlan.DriverDelta> ChangedDrivers,
IReadOnlyList<ScriptedAlarmPlan> AddedAlarms,
IReadOnlyList<ScriptedAlarmPlan> RemovedAlarms,
IReadOnlyList<Phase7Plan.AlarmDelta> ChangedAlarms)
IReadOnlyList<AddressSpacePlan.AlarmDelta> ChangedAlarms)
{
/// <summary>
/// Equipment-namespace tag diff sets, keyed by <see cref="EquipmentTagPlan.TagId"/>. Added as
/// init-only members (defaulting empty) rather than positional parameters so existing
/// <c>Phase7Plan</c> construction sites compile unchanged — consistent with how
/// <see cref="Phase7CompositionResult.EquipmentTags"/> was added. Without these, an
/// <c>AddressSpacePlan</c> construction sites compile unchanged — consistent with how
/// <see cref="AddressSpaceComposition.EquipmentTags"/> was added. Without these, an
/// incremental deploy that changes ONLY equipment tags produced an empty plan and
/// <c>OpcUaPublishActor.HandleRebuild</c> short-circuited before materialising them.
/// </summary>
@@ -67,7 +67,7 @@ public sealed record Phase7Plan(
public sealed record EquipmentVirtualTagDelta(EquipmentVirtualTagPlan Previous, EquipmentVirtualTagPlan Current);
}
public static class Phase7Planner
public static class AddressSpacePlanner
{
/// <summary>
/// Diff two compositions, emitting Added/Removed/Changed sets per entity class.
@@ -77,7 +77,7 @@ public static class Phase7Planner
/// </summary>
/// <param name="previous">The previous composition result.</param>
/// <param name="next">The new composition result.</param>
public static Phase7Plan Compute(Phase7CompositionResult previous, Phase7CompositionResult next)
public static AddressSpacePlan Compute(AddressSpaceComposition previous, AddressSpaceComposition next)
{
ArgumentNullException.ThrowIfNull(previous);
ArgumentNullException.ThrowIfNull(next);
@@ -85,22 +85,22 @@ public static class Phase7Planner
var (addedEq, removedEq, changedEq) = DiffById(
previous.EquipmentNodes, next.EquipmentNodes,
n => n.EquipmentId,
(a, b) => new Phase7Plan.EquipmentDelta(a, b));
(a, b) => new AddressSpacePlan.EquipmentDelta(a, b));
var (addedDrv, removedDrv, changedDrv) = DiffById(
previous.DriverInstancePlans, next.DriverInstancePlans,
d => d.DriverInstanceId,
(a, b) => new Phase7Plan.DriverDelta(a, b));
(a, b) => new AddressSpacePlan.DriverDelta(a, b));
var (addedAlarm, removedAlarm, changedAlarm) = DiffById(
previous.ScriptedAlarmPlans, next.ScriptedAlarmPlans,
a => a.ScriptedAlarmId,
(a, b) => new Phase7Plan.AlarmDelta(a, b));
(a, b) => new AddressSpacePlan.AlarmDelta(a, b));
var (addedEqTags, removedEqTags, changedEqTags) = DiffById(
previous.EquipmentTags, next.EquipmentTags,
t => t.TagId,
(a, b) => new Phase7Plan.EquipmentTagDelta(a, b));
(a, b) => new AddressSpacePlan.EquipmentTagDelta(a, b));
// VirtualTags diff by VirtualTagId, mirroring the EquipmentTags pass. EquipmentVirtualTagPlan
// overrides record equality to compare ALL fields by value — scalars (Expression/DataType/
@@ -110,9 +110,9 @@ public static class Phase7Planner
var (addedVTags, removedVTags, changedVTags) = DiffById(
previous.EquipmentVirtualTags, next.EquipmentVirtualTags,
t => t.VirtualTagId,
(a, b) => new Phase7Plan.EquipmentVirtualTagDelta(a, b));
(a, b) => new AddressSpacePlan.EquipmentVirtualTagDelta(a, b));
return new Phase7Plan(
return new AddressSpacePlan(
addedEq, removedEq, changedEq,
addedDrv, removedDrv, changedDrv,
addedAlarm, removedAlarm, changedAlarm)
@@ -25,7 +25,7 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
///
/// Node-id encoding uses the manager's default namespace + the caller-supplied string id
/// as the identifier portion (e.g. <c>"ns=2;s=eq-1/temp"</c>). Equipment-folder hierarchy
/// and OPC UA type metadata still come from the Phase7Applier / EquipmentNodeWalker
/// and OPC UA type metadata still come from the AddressSpaceApplier / EquipmentNodeWalker
/// integration (F14b, tracked under #85) — this manager treats every id as a flat
/// <see cref="BaseDataVariableState"/> under the namespace root.
/// </summary>
@@ -1252,7 +1252,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// <summary>
/// Ensure a folder node exists at <paramref name="folderNodeId"/> with the given display
/// name, parented under <paramref name="parentNodeId"/> (or the namespace root when null).
/// #85 — used by <see cref="Phase7Applier"/> to materialise the UNS Area/Line/Equipment
/// #85 — used by <see cref="AddressSpaceApplier"/> to materialise the UNS Area/Line/Equipment
/// folder hierarchy. Idempotent: the second call with the same id returns the cached
/// folder so adding child variables under it still works.
/// </summary>
@@ -1424,7 +1424,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
_ => DataTypeIds.BaseDataType,
};
/// <summary>Clear every registered variable + folder from the address space. Phase7Applier
/// <summary>Clear every registered variable + folder from the address space. AddressSpaceApplier
/// calls this when Equipment/Alarm topology changes; the populator then re-adds via
/// EnsureFolder + WriteValue on the next pass.</summary>
public void RebuildAddressSpace()
@@ -169,8 +169,8 @@ public static class DeploymentArtifact
}
/// <summary>
/// Parse the artifact into the projected <see cref="Phase7CompositionResult"/> used by
/// <c>Phase7Planner</c> + <c>Phase7Applier</c>. Returns an empty composition for empty/
/// Parse the artifact into the projected <see cref="AddressSpaceComposition"/> used by
/// <c>AddressSpacePlanner</c> + <c>AddressSpaceApplier</c>. Returns an empty composition for empty/
/// malformed blobs so callers can treat parse failure as a no-op deploy.
///
/// The artifact JSON is produced by <c>ConfigComposer.SnapshotAndFlattenAsync</c> in the
@@ -179,7 +179,7 @@ public static class DeploymentArtifact
/// nodes.
/// </summary>
/// <param name="blob">The deployment artifact blob to parse.</param>
public static Phase7CompositionResult ParseComposition(ReadOnlySpan<byte> blob)
public static AddressSpaceComposition ParseComposition(ReadOnlySpan<byte> blob)
{
if (blob.IsEmpty) return Empty();
@@ -197,7 +197,7 @@ public static class DeploymentArtifact
var equipmentVirtualTags = BuildEquipmentVirtualTagPlans(root, equipmentTags);
var equipmentScriptedAlarms = BuildEquipmentScriptedAlarmPlans(root);
return new Phase7CompositionResult(areas, lines, equipment, drivers, alarms)
return new AddressSpaceComposition(areas, lines, equipment, drivers, alarms)
{
EquipmentTags = equipmentTags,
EquipmentVirtualTags = equipmentVirtualTags,
@@ -224,7 +224,7 @@ public static class DeploymentArtifact
/// <param name="nodeId">This node's identity in "host:port" form.</param>
/// <param name="onInconsistency">Optional diagnostic callback for cross-cluster orphan bindings; null disables the check.</param>
/// <returns>The filtered composition per the node's scoping decision.</returns>
public static Phase7CompositionResult ParseComposition(
public static AddressSpaceComposition ParseComposition(
ReadOnlySpan<byte> blob, string nodeId, Action<string>? onInconsistency = null)
{
var scope = ResolveClusterScope(blob, nodeId);
@@ -253,7 +253,7 @@ public static class DeploymentArtifact
}
}
return new Phase7CompositionResult(
return new AddressSpaceComposition(
full.UnsAreas.Where(a => sets.AreaIds.Contains(a.UnsAreaId)).ToArray(),
keptLines,
keptEquipment,
@@ -348,7 +348,7 @@ public static class DeploymentArtifact
}
}
private static Phase7CompositionResult Empty() => new(
private static AddressSpaceComposition Empty() => new(
Array.Empty<UnsAreaProjection>(),
Array.Empty<UnsLineProjection>(),
Array.Empty<EquipmentNode>(),
@@ -359,7 +359,7 @@ public static class DeploymentArtifact
/// Cross-reference the artifact's Tags + Namespaces + DriverInstances arrays to find
/// Equipment-namespace tags (non-null EquipmentId, owning namespace Kind == Equipment), then
/// emit one <see cref="EquipmentTagPlan"/> per qualifying tag. The artifact-decode mirror of
/// <c>Phase7Composer.Compose</c>'s equipment filter — so the compose-side + artifact-decode
/// <c>AddressSpaceComposer.Compose</c>'s equipment filter — so the compose-side + artifact-decode
/// plans agree on the same set of tags. FullName is read from each tag's TagConfig blob
/// (top-level "FullName" field).
/// </summary>
@@ -421,7 +421,7 @@ public static class DeploymentArtifact
// AccessLevel → Writable. ConfigComposer serialises the TagAccessLevel enum WITHOUT a
// string converter, so it lands as a number (Read = 0, ReadWrite = 1); tolerate the string
// form ("ReadWrite") too — same defensive both-forms parse as the Kind gate above. MUST match
// Phase7Composer's `AccessLevel == TagAccessLevel.ReadWrite` exactly (byte-parity). A missing
// AddressSpaceComposer's `AccessLevel == TagAccessLevel.ReadWrite` exactly (byte-parity). A missing
// field defaults to non-writable (read-only).
var writable = el.TryGetProperty("AccessLevel", out var alEl) && alEl.ValueKind switch
{
@@ -469,7 +469,7 @@ public static class DeploymentArtifact
/// <summary>
/// Join the artifact's VirtualTags array to its Scripts array (by ScriptId) to emit one
/// <see cref="EquipmentVirtualTagPlan"/> per VirtualTag. The artifact-decode mirror of
/// <c>Phase7Composer.Compose</c>'s VirtualTag producer — so the compose-side + artifact-decode
/// <c>AddressSpaceComposer.Compose</c>'s VirtualTag producer — so the compose-side + artifact-decode
/// plans agree. The reserved <c>{{equip}}</c> token in the joined Script's <c>SourceCode</c> is
/// substituted with the owning equipment's tag base (derived from <paramref name="equipmentTags"/>'
/// FullNames) BEFORE refs are extracted, byte-parity with the composer. <c>Expression</c> = the
@@ -529,14 +529,14 @@ public static class DeploymentArtifact
// Historize: the artifact carries a Pascal-case "Historize" bool (ConfigComposer serialises
// the whole VirtualTag entity with DefaultIgnoreCondition.Never). Robust parse — default
// false; only honoured when the JSON value is an actual boolean — so absent/non-bool ⇒ false,
// byte-parity with Phase7Composer's entity-default-false behaviour.
// byte-parity with AddressSpaceComposer's entity-default-false behaviour.
var historize = el.TryGetProperty("Historize", out var hEl)
&& (hEl.ValueKind == JsonValueKind.True || hEl.ValueKind == JsonValueKind.False)
&& hEl.GetBoolean();
// Substitute the {{equip}} token with the owning equipment's tag base BEFORE extracting
// refs, so both Expression and DependencyRefs are machine-specific — byte-parity with
// Phase7Composer.Compose.
// AddressSpaceComposer.Compose.
var expanded = EquipmentScriptPaths.SubstituteEquipmentToken(
source, baseByEquip.GetValueOrDefault(equipmentId!));
@@ -562,7 +562,7 @@ public static class DeploymentArtifact
/// <summary>
/// Join the artifact's ScriptedAlarms array to its Scripts array (by PredicateScriptId) to emit
/// one <see cref="EquipmentScriptedAlarmPlan"/> per alarm. The artifact-decode mirror of
/// <c>Phase7Composer.Compose</c>'s scripted-alarm producer — so the compose-side + artifact-decode
/// <c>AddressSpaceComposer.Compose</c>'s scripted-alarm producer — so the compose-side + artifact-decode
/// plans agree byte-for-byte. An alarm whose <c>PredicateScriptId</c> has no matching Script row is
/// SKIPPED (matching the composer's skip behaviour) to preserve parity. <c>PredicateSource</c> = the
/// joined script source ("" when missing — but such alarms are skipped above); <c>DependencyRefs</c>
@@ -616,7 +616,7 @@ public static class DeploymentArtifact
if (string.IsNullOrWhiteSpace(scriptedAlarmId)) continue;
// Skip alarms whose predicate script is missing — matching Phase7Composer's skip behaviour
// Skip alarms whose predicate script is missing — matching AddressSpaceComposer's skip behaviour
// so both sides emit the same set (byte-parity).
if (predicateScriptId is null || !scriptSourceById.TryGetValue(predicateScriptId, out var source))
continue;
@@ -646,7 +646,7 @@ public static class DeploymentArtifact
/// <summary>
/// Extract the driver-side full reference from a tag's TagConfig JSON (top-level "FullName"
/// field). The artifact-decode mirror of <c>Phase7Composer.ExtractTagFullName</c> /
/// field). The artifact-decode mirror of <c>AddressSpaceComposer.ExtractTagFullName</c> /
/// <c>EquipmentNodeWalker.ExtractFullName</c> — replicated because Runtime does not reference
/// the Core driver assembly. Falls back to the raw blob when absent or non-JSON.
/// </summary>
@@ -669,7 +669,7 @@ public static class DeploymentArtifact
/// <summary>Parses the optional <c>alarm</c> object from a tag's <c>TagConfig</c> JSON. Returns null
/// when absent, non-object, or non-JSON (the tag is then a plain variable). Never throws. The
/// live-edit side (<c>Phase7Composer.ExtractTagAlarm</c>) MUST parse identically (byte-parity).</summary>
/// live-edit side (<c>AddressSpaceComposer.ExtractTagAlarm</c>) MUST parse identically (byte-parity).</summary>
private static EquipmentTagAlarmInfo? ExtractTagAlarm(string? tagConfig)
{
if (string.IsNullOrWhiteSpace(tagConfig)) return null;
@@ -683,7 +683,7 @@ public static class DeploymentArtifact
var sev = a.TryGetProperty("severity", out var sEl) && sEl.ValueKind == JsonValueKind.Number
&& sEl.TryGetInt32(out var sv) ? sv : 500;
// historizeToAveva (bool?, absent ⇒ null ⇒ historize): byte-parity with
// Phase7Composer.ExtractTagAlarm — only an explicit false suppresses the durable AVEVA write.
// AddressSpaceComposer.ExtractTagAlarm — only an explicit false suppresses the durable AVEVA write.
bool? historize = a.TryGetProperty("historizeToAveva", out var hEl)
&& hEl.ValueKind is JsonValueKind.True or JsonValueKind.False
? hEl.GetBoolean()
@@ -699,7 +699,7 @@ public static class DeploymentArtifact
/// whitespace-or-empty ⇒ <c>null</c>, meaning the historian tagname defaults to the tag's FullName,
/// resolved later). The raw string value is used — not trimmed — matching <c>ExtractTagFullName</c> /
/// <c>ExtractTagAlarm</c>. Never throws. The live-edit composer side
/// (<c>Phase7Composer.ExtractTagHistorize</c>) MUST parse identically (byte-parity).</summary>
/// (<c>AddressSpaceComposer.ExtractTagHistorize</c>) MUST parse identically (byte-parity).</summary>
private static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string? tagConfig)
{
if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null);
@@ -727,7 +727,7 @@ public static class DeploymentArtifact
/// <c>arrayLength</c> uint (honoured ONLY when <c>isArray</c> is true AND the prop is a JSON number
/// that fits <c>uint</c>; else <c>null</c>). Mirrors <see cref="ExtractTagHistorize"/> in structure +
/// null/blank/non-object/malformed-JSON tolerance. Never throws. The live-edit composer side
/// (<c>Phase7Composer.ExtractTagArray</c>) MUST parse identically (byte-parity).</summary>
/// (<c>AddressSpaceComposer.ExtractTagArray</c>) MUST parse identically (byte-parity).</summary>
private static (bool IsArray, uint? ArrayLength) ExtractTagArray(string? tagConfig)
{
if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null);
@@ -764,7 +764,7 @@ public static class DeploymentArtifact
var item = reader(el);
if (item is not null) result.Add(item);
}
// Match Phase7Composer's natural-key sort so plan diffs are deterministic across
// Match AddressSpaceComposer's natural-key sort so plan diffs are deterministic across
// artifact-decode + composer-compose passes.
return result.OrderBy(IdentityOf, StringComparer.Ordinal).ToList();
}
@@ -939,7 +939,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
return;
}
Phase7CompositionResult composition;
AddressSpaceComposition composition;
try
{
composition = DeploymentArtifact.ParseComposition(blob, _localNode.Value);
@@ -63,7 +63,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
private readonly bool _subscribeRedundancyTopic;
private readonly NodeId? _localNode;
private readonly IDbContextFactory<OtOpcUaConfigDbContext>? _dbFactory;
private readonly Phase7Applier? _applier;
private readonly AddressSpaceApplier? _applier;
private readonly IActorRef? _dbHealthProbe;
private readonly TimeSpan _staleWindow;
private readonly TimeSpan _probeFreshnessWindow;
@@ -77,7 +77,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
private DbHealthProbeActor.DbHealthStatus? _lastDbHealth;
private RedundancyStateChanged? _lastSnapshot;
private (bool Ok, DateTime At)? _probeAboutMe;
private Phase7CompositionResult _lastApplied = new(
private AddressSpaceComposition _lastApplied = new(
Array.Empty<UnsAreaProjection>(),
Array.Empty<UnsLineProjection>(),
Array.Empty<EquipmentNode>(),
@@ -116,7 +116,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
IServiceLevelPublisher? serviceLevel = null,
NodeId? localNode = null,
IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null,
Phase7Applier? applier = null,
AddressSpaceApplier? applier = null,
IActorRef? dbHealthProbe = null,
TimeSpan? staleWindow = null,
TimeSpan? probeFreshnessWindow = null,
@@ -157,7 +157,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
bool subscribeRedundancyTopic = false,
NodeId? localNode = null,
IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null,
Phase7Applier? applier = null,
AddressSpaceApplier? applier = null,
IActorRef? dbHealthProbe = null,
TimeSpan? staleWindow = null,
TimeSpan? probeFreshnessWindow = null,
@@ -197,7 +197,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
bool subscribeRedundancyTopic,
NodeId? localNode,
IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null,
Phase7Applier? applier = null,
AddressSpaceApplier? applier = null,
IActorRef? dbHealthProbe = null,
TimeSpan? staleWindow = null,
TimeSpan? probeFreshnessWindow = null,
@@ -308,7 +308,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
? DeploymentArtifact.ParseComposition(artifact, ln.Value,
inconsistency => _log.Warning("OpcUaPublish {Node}: cross-cluster binding — {Message}", ln, inconsistency))
: DeploymentArtifact.ParseComposition(artifact);
var plan = Phase7Planner.Compute(_lastApplied, composition);
var plan = AddressSpacePlanner.Compute(_lastApplied, composition);
if (plan.IsEmpty)
{
@@ -321,7 +321,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
_lastApplied = composition;
// #85 — after the plan diff lands, rebuild the UNS folder hierarchy so OPC UA
// clients see Area/Line/Equipment as proper folders. Idempotent; Phase7Applier
// clients see Area/Line/Equipment as proper folders. Idempotent; AddressSpaceApplier
// skips folders that already exist with the same node id.
_applier.MaterialiseHierarchy(composition);
// T14 — scripted alarms get their own pass right after the hierarchy so the equipment
@@ -206,9 +206,9 @@ public static class ServiceCollectionExtensions
registry.Register<DependencyMuxActorKey>(mux);
// OPC UA publish actor — pinned dispatcher, owns the address-space side of the
// pipeline. Phase7Applier is constructed here so the actor + applier share the
// pipeline. AddressSpaceApplier is constructed here so the actor + applier share the
// same sink reference (when DeferredAddressSpaceSink swaps later, both see it).
var applier = new Phase7Applier(addressSpaceSink, loggerFactory.CreateLogger<Phase7Applier>());
var applier = new AddressSpaceApplier(addressSpaceSink, loggerFactory.CreateLogger<AddressSpaceApplier>());
var publishActor = system.ActorOf(
OpcUaPublishActor.Props(
sink: addressSpaceSink,
@@ -20,7 +20,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
///
/// <para>
/// The published NodeId is computed by the shared <see cref="EquipmentNodeIds.Variable"/> —
/// the single source of truth <c>Phase7Applier.MaterialiseEquipmentVirtualTags</c> also
/// the single source of truth <c>AddressSpaceApplier.MaterialiseEquipmentVirtualTags</c> also
/// materialises against — so the value always lands on a NodeId that exists.
/// </para>
/// </summary>
@@ -198,7 +198,7 @@ public sealed class VirtualTagHostActor : ReceiveActor
}
/// <summary>Folder-scoped NodeId for a VirtualTag plan. The formula now lives in the shared
/// <see cref="EquipmentNodeIds"/> (the single source of truth that <c>Phase7Applier</c> also
/// <see cref="EquipmentNodeIds"/> (the single source of truth that <c>AddressSpaceApplier</c> also
/// materialises against), so the published value always lands on the NodeId that was materialised.</summary>
private static string NodeIdFor(EquipmentVirtualTagPlan p) =>
EquipmentNodeIds.Variable(p.EquipmentId, p.FolderPath, p.Name);
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
/// the plan decisions call for. Introspection only — no SQL Server required.
/// </summary>
[Trait("Category", "Unit")]
public sealed class Phase7ScriptingEntitiesTests
public sealed class ScriptingEntitiesTests
{
private static OtOpcUaConfigDbContext BuildCtx()
{
@@ -20,7 +20,7 @@ namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
/// seam that the OpcUaServer unit tests only approximate with hand-built JSON.
/// <para>
/// The OPC UA address-space browse is exercised separately against a real SDK node manager in
/// <c>Phase7ApplierHierarchyTests.Equipment_namespace_structure_materialises_end_to_end_against_real_SDK</c>,
/// <c>AddressSpaceApplierHierarchyTests.Equipment_namespace_structure_materialises_end_to_end_against_real_SDK</c>,
/// because the in-process <see cref="TwoNodeClusterHarness"/> binds the no-op address-space sink.
/// </para>
/// </summary>
@@ -10,13 +10,13 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary>
/// #85 — verifies <see cref="Phase7Applier.MaterialiseHierarchy"/> builds the UNS
/// #85 — verifies <see cref="AddressSpaceApplier.MaterialiseHierarchy"/> builds the UNS
/// Area/Line/Equipment folder tree through <see cref="IOpcUaAddressSpaceSink.EnsureFolder"/>.
/// One pure unit test (recording sink) confirms ordering + parenting; one boot-verify test
/// drives a real <see cref="OtOpcUaNodeManager"/> and inspects the resulting predefined-node
/// count to prove the folders land in the SDK address space.
/// </summary>
public sealed class Phase7ApplierHierarchyTests : IDisposable
public sealed class AddressSpaceApplierHierarchyTests : IDisposable
{
private static CancellationToken Ct => TestContext.Current.CancellationToken;
@@ -29,9 +29,9 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
public void MaterialiseHierarchy_creates_areas_then_lines_then_equipment_with_correct_parents()
{
var sink = new RecordingFolderSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
UnsAreas: new[] { new UnsAreaProjection("area-1", "Plant North") },
UnsLines: new[] { new UnsLineProjection("line-1", "area-1", "Cell A") },
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Pump-1", "line-1") },
@@ -52,9 +52,9 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
public void MaterialiseHierarchy_orphan_equipment_hangs_under_root()
{
var sink = new RecordingFolderSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
UnsAreas: Array.Empty<UnsAreaProjection>(),
UnsLines: Array.Empty<UnsLineProjection>(),
EquipmentNodes: new[] { new EquipmentNode("eq-orphan", "Orphan", UnsLineId: "") },
@@ -86,9 +86,9 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
sdkServer.NodeManager.ShouldNotBeNull();
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
applier.MaterialiseHierarchy(new Phase7CompositionResult(
applier.MaterialiseHierarchy(new AddressSpaceComposition(
UnsAreas: new[] { new UnsAreaProjection("area-A", "Area A"), new UnsAreaProjection("area-B", "Area B") },
UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") },
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") },
@@ -98,7 +98,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
sdkServer.NodeManager!.FolderCount.ShouldBe(5); // 2 areas + 1 line + 2 equipment
// Idempotent: re-applying with the same composition doesn't create duplicates.
applier.MaterialiseHierarchy(new Phase7CompositionResult(
applier.MaterialiseHierarchy(new AddressSpaceComposition(
UnsAreas: new[] { new UnsAreaProjection("area-A", "Area A"), new UnsAreaProjection("area-B", "Area B") },
UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") },
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") },
@@ -132,9 +132,9 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
sdkServer.NodeManager.ShouldNotBeNull();
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
UnsAreas: Array.Empty<UnsAreaProjection>(),
UnsLines: Array.Empty<UnsLineProjection>(),
EquipmentNodes: new[] { new EquipmentNode("eq-1", "filling-eq", UnsLineId: "") },
@@ -157,7 +157,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
/// <summary>
/// Full structure-materialisation pipeline against a real SDK node manager: real Config
/// entities (Area / Line / Equipment + an Equipment-namespace Tag) → <see cref="Phase7Composer.Compose"/>
/// entities (Area / Line / Equipment + an Equipment-namespace Tag) → <see cref="AddressSpaceComposer.Compose"/>
/// → MaterialiseHierarchy + MaterialiseEquipmentTags → <see cref="OtOpcUaNodeManager"/>. Proves
/// an Equipment namespace lands its Area/Line/Equipment folder tree + the equipment-signal
/// Variable in a live OPC UA address space (structure-only; live values are a later milestone).
@@ -192,7 +192,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
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 composition = Phase7Composer.Compose(
var composition = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equipment }, new[] { driver },
Array.Empty<ScriptedAlarm>(), new[] { tag }, new[] { ns });
@@ -202,7 +202,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
planned.FullName.ShouldBe("40001");
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
applier.MaterialiseHierarchy(composition);
applier.MaterialiseEquipmentTags(composition);
@@ -6,14 +6,14 @@ using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
public sealed class Phase7ApplierTests
public sealed class AddressSpaceApplierTests
{
/// <summary>Verifies that an empty plan does not call the sink or trigger a rebuild.</summary>
[Fact]
public void Empty_plan_does_not_call_sink_and_does_not_trigger_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var outcome = applier.Apply(EmptyPlan);
@@ -30,7 +30,7 @@ public sealed class Phase7ApplierTests
public void Removed_equipment_writes_inactive_alarm_state_per_id_and_triggers_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = WithEquipmentRemoval("eq-1", "eq-2");
var outcome = applier.Apply(plan);
@@ -48,18 +48,18 @@ public sealed class Phase7ApplierTests
public void Added_equipment_triggers_rebuild_without_alarm_writes()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = new Phase7Plan(
var plan = new AddressSpacePlan(
AddedEquipment: new[] { new EquipmentNode("new", "New", "line-1") },
RemovedEquipment: Array.Empty<EquipmentNode>(),
ChangedEquipment: Array.Empty<Phase7Plan.EquipmentDelta>(),
ChangedEquipment: Array.Empty<AddressSpacePlan.EquipmentDelta>(),
AddedDrivers: Array.Empty<DriverInstancePlan>(),
RemovedDrivers: Array.Empty<DriverInstancePlan>(),
ChangedDrivers: Array.Empty<Phase7Plan.DriverDelta>(),
ChangedDrivers: Array.Empty<AddressSpacePlan.DriverDelta>(),
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>());
ChangedAlarms: Array.Empty<AddressSpacePlan.AlarmDelta>());
var outcome = applier.Apply(plan);
@@ -74,23 +74,23 @@ public sealed class Phase7ApplierTests
public void Driver_only_changes_do_not_trigger_address_space_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = new Phase7Plan(
var plan = new AddressSpacePlan(
AddedEquipment: Array.Empty<EquipmentNode>(),
RemovedEquipment: Array.Empty<EquipmentNode>(),
ChangedEquipment: Array.Empty<Phase7Plan.EquipmentDelta>(),
ChangedEquipment: Array.Empty<AddressSpacePlan.EquipmentDelta>(),
AddedDrivers: new[] { new DriverInstancePlan("d-new", "Modbus", "{}") },
RemovedDrivers: Array.Empty<DriverInstancePlan>(),
ChangedDrivers: new[]
{
new Phase7Plan.DriverDelta(
new AddressSpacePlan.DriverDelta(
new DriverInstancePlan("d-1", "Modbus", "{\"v\":1}"),
new DriverInstancePlan("d-1", "Modbus", "{\"v\":2}")),
},
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>());
ChangedAlarms: Array.Empty<AddressSpacePlan.AlarmDelta>());
var outcome = applier.Apply(plan);
@@ -103,7 +103,7 @@ public sealed class Phase7ApplierTests
public void Sink_exception_in_WriteAlarmCondition_does_not_propagate_and_rebuild_still_fires()
{
var sink = new ThrowingSink(throwOnAlarmWrite: true);
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = WithEquipmentRemoval("eq-1");
@@ -120,9 +120,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_creates_variable_under_equipment_folder()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
UnsAreas: Array.Empty<UnsAreaProjection>(),
UnsLines: Array.Empty<UnsLineProjection>(),
EquipmentNodes: Array.Empty<EquipmentNode>(),
@@ -151,9 +151,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_nests_FolderPath_subfolder_under_equipment()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -178,9 +178,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_identical_FullName_across_two_equipments_does_not_collide()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -206,9 +206,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_alarm_bearing_tag_becomes_condition_plain_tag_stays_variable()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -241,9 +241,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_alarm_bearing_tag_with_FolderPath_conditions_under_subfolder()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -271,9 +271,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_resolves_historian_tagname_default_override_and_null()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -304,9 +304,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_blank_override_falls_back_to_full_name()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -324,16 +324,16 @@ public sealed class Phase7ApplierTests
}
/// <summary>Array-support Task 2 — an <see cref="EquipmentTagPlan"/> with <c>IsArray: true,
/// ArrayLength: 16</c> flowing through <see cref="Phase7Applier.MaterialiseEquipmentTags"/> must
/// ArrayLength: 16</c> flowing through <see cref="AddressSpaceApplier.MaterialiseEquipmentTags"/> must
/// forward BOTH flags verbatim to the sink's <c>EnsureVariable</c>. Guards against arg-order swaps or
/// accidental drops in the wire-through.</summary>
[Fact]
public void MaterialiseEquipmentTags_array_plan_forwards_isArray_and_arrayLength_to_sink()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -358,9 +358,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_scalar_plan_forwards_isArray_false_to_sink()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -387,9 +387,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_array_writable_true_is_forced_read_only()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -417,9 +417,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_scalar_writable_true_stays_writable()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -446,9 +446,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentVirtualTags_creates_variable_under_equipment_folder()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -469,7 +469,7 @@ public sealed class Phase7ApplierTests
/// <summary>Golden/parity guard: the materialiser's Variable NodeId for BOTH the equipment-tag and
/// the equipment-VirtualTag pass is byte-identical to <see cref="EquipmentNodeIds.Variable"/> — the
/// single source of truth Phase7Applier + VirtualTagHostActor both point at. Covers null/empty
/// single source of truth AddressSpaceApplier + VirtualTagHostActor both point at. Covers null/empty
/// FolderPath (directly under equipment) and a non-empty FolderPath (sub-folder scoped). This test
/// LOCKS the formula against drift: any change to the materialiser NodeId that diverges from the
/// shared helper fails here.</summary>
@@ -477,9 +477,9 @@ public sealed class Phase7ApplierTests
public void Materialised_variable_node_ids_match_shared_EquipmentNodeIds_formula()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -512,9 +512,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentVirtualTags_two_under_same_equipment_do_not_collide()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -541,9 +541,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseScriptedAlarms_materialises_enabled_and_skips_disabled()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult(
var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentScriptedAlarms = new[]
@@ -574,7 +574,7 @@ public sealed class Phase7ApplierTests
public void Added_equipment_tags_trigger_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = EmptyPlan with
{
@@ -598,7 +598,7 @@ public sealed class Phase7ApplierTests
public void Added_equipment_virtual_tags_trigger_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = EmptyPlan with
{
@@ -624,7 +624,7 @@ public sealed class Phase7ApplierTests
public void Changed_equipment_tags_only_trigger_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
@@ -632,7 +632,7 @@ public sealed class Phase7ApplierTests
var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Int32", FullName: "40001", Writable: true, Alarm: null));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: ONLY ChangedEquipmentTags is populated.
plan.ChangedEquipmentTags.Count.ShouldBe(1);
@@ -659,7 +659,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_expression_only_skips_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -669,7 +669,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
Expression: "ctx.GetTag(\"a\") * 120", DependencyRefs: new[] { "a" }));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: ONLY ChangedEquipmentVirtualTags is populated.
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
@@ -690,7 +690,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_dependency_refs_only_skips_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -703,7 +703,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a", "b" }));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -719,7 +719,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_historize_only_skips_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -729,7 +729,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }, Historize: true));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -747,7 +747,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_data_type_change_still_rebuilds()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -757,7 +757,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Int32",
Expression: "ctx.GetTag(\"a\") * 60", DependencyRefs: new[] { "a" }));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -772,7 +772,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_name_change_still_rebuilds()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -781,7 +781,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "EfficiencyPct", DataType: "Float64",
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -796,7 +796,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_folder_path_change_still_rebuilds()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -805,7 +805,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "Calc", Name: "Efficiency", DataType: "Float64",
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -822,9 +822,9 @@ public sealed class Phase7ApplierTests
public void Node_irrelevant_vtag_edit_mixed_with_another_change_still_rebuilds()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = new Phase7CompositionResult(
var previous = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -838,7 +838,7 @@ public sealed class Phase7ApplierTests
},
};
// Expression-only vtag edit (node-irrelevant) AND a node-affecting tag DataType flip.
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -852,7 +852,7 @@ public sealed class Phase7ApplierTests
},
};
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
// Both a node-irrelevant vtag change AND a node-affecting tag change are present.
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
@@ -873,7 +873,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_equipment_id_triggers_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -884,7 +884,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-2", FolderPath: "", Name: "Efficiency", DataType: "Float64",
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: the planner sees this as a changed vtag (same id, different EquipmentId).
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
@@ -907,9 +907,9 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tags_one_irrelevant_one_structural_triggers_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = new Phase7CompositionResult(
var previous = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -922,7 +922,7 @@ public sealed class Phase7ApplierTests
Expression: "ctx.GetTag(\"load\")", DependencyRefs: new[] { "load" }),
},
};
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -936,7 +936,7 @@ public sealed class Phase7ApplierTests
},
};
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: both vtags are diffed as changed, nothing else.
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(2);
@@ -961,13 +961,13 @@ public sealed class Phase7ApplierTests
public void Changed_alarms_only_trigger_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithAlarms(new ScriptedAlarmPlan("alm-1", "eq-1", "scr-1", "Temp high"));
// Same alarm id, edited message template — the planner classifies this as a change.
var next = CompositionWithAlarms(new ScriptedAlarmPlan("alm-1", "eq-1", "scr-1", "Temp critically high"));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: ONLY ChangedAlarms is populated.
plan.ChangedAlarms.Count.ShouldBe(1);
@@ -991,12 +991,12 @@ public sealed class Phase7ApplierTests
public void Changed_drivers_only_do_not_trigger_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithDrivers(new DriverInstancePlan("d-1", "Modbus", "{\"v\":1}"));
var next = CompositionWithDrivers(new DriverInstancePlan("d-1", "Modbus", "{\"v\":2}"));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: ONLY ChangedDrivers is populated.
plan.ChangedDrivers.Count.ShouldBe(1);
@@ -1017,15 +1017,15 @@ public sealed class Phase7ApplierTests
/// <summary>H1a (review follow-up) — a deploy that ONLY removes existing equipment tag / VirtualTag
/// nodes must rebuild AND tally the removals. Removed tags/VirtualTags are plain variable nodes (no
/// Part 9 condition to write), so before the fix they reached the rebuild path but were never added
/// to <c>removedCount</c> — <c>Phase7ApplyOutcome.RemovedNodes</c> reported 0, a misleading audit
/// to <c>removedCount</c> — <c>AddressSpaceApplyOutcome.RemovedNodes</c> reported 0, a misleading audit
/// entry. This pins both the rebuild and the accurate count.</summary>
[Fact]
public void Removed_equipment_tags_and_virtual_tags_only_rebuild_and_are_counted()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = new Phase7CompositionResult(
var previous = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -1038,10 +1038,10 @@ public sealed class Phase7ApplierTests
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }),
},
};
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: ONLY the two Removed sets are populated.
plan.RemovedEquipmentTags.Count.ShouldBe(1);
@@ -1069,7 +1069,7 @@ public sealed class Phase7ApplierTests
public void Changed_tag_writable_only_skips_rebuild_and_updates_in_place()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
@@ -1077,7 +1077,7 @@ public sealed class Phase7ApplierTests
var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: true, Alarm: null));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1);
plan.AddedEquipmentTags.ShouldBeEmpty();
plan.RemovedEquipmentTags.ShouldBeEmpty();
@@ -1101,8 +1101,8 @@ public sealed class Phase7ApplierTests
{
// false → true (no override) ⇒ historian defaults to FullName.
var sinkOn = new RecordingSink();
var applierOn = new Phase7Applier(sinkOn, NullLogger<Phase7Applier>.Instance);
var planOn = Phase7Planner.Compute(
var applierOn = new AddressSpaceApplier(sinkOn, NullLogger<AddressSpaceApplier>.Instance);
var planOn = AddressSpacePlanner.Compute(
CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
FullName: "T.A", Writable: false, Alarm: null, IsHistorized: false, HistorianTagname: null)),
CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
@@ -1117,8 +1117,8 @@ public sealed class Phase7ApplierTests
// true → false ⇒ historian null.
var sinkOff = new RecordingSink();
var applierOff = new Phase7Applier(sinkOff, NullLogger<Phase7Applier>.Instance);
var planOff = Phase7Planner.Compute(
var applierOff = new AddressSpaceApplier(sinkOff, NullLogger<AddressSpaceApplier>.Instance);
var planOff = AddressSpacePlanner.Compute(
CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
FullName: "T.A", Writable: false, Alarm: null, IsHistorized: true, HistorianTagname: null)),
CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
@@ -1138,7 +1138,7 @@ public sealed class Phase7ApplierTests
public void Changed_tag_historian_tagname_only_skips_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
@@ -1147,7 +1147,7 @@ public sealed class Phase7ApplierTests
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
FullName: "T.A", Writable: false, Alarm: null, IsHistorized: true, HistorianTagname: "WW.New"));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -1163,7 +1163,7 @@ public sealed class Phase7ApplierTests
public void Changed_tag_data_type_change_rebuilds_and_no_surgical_call()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
@@ -1171,7 +1171,7 @@ public sealed class Phase7ApplierTests
var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Int32", FullName: "40001", Writable: true, Alarm: null));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -1187,7 +1187,7 @@ public sealed class Phase7ApplierTests
public void Changed_tag_is_array_change_rebuilds()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Buffer", DataType: "Int16",
@@ -1196,7 +1196,7 @@ public sealed class Phase7ApplierTests
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Buffer", DataType: "Int16",
FullName: "40001", Writable: false, Alarm: null, IsArray: true, ArrayLength: 16u));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -1212,14 +1212,14 @@ public sealed class Phase7ApplierTests
public void Changed_tag_full_name_change_rebuilds()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40002", Writable: false, Alarm: null));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -1235,14 +1235,14 @@ public sealed class Phase7ApplierTests
public void Changed_tag_name_change_rebuilds()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "SpeedRpm", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -1259,7 +1259,7 @@ public sealed class Phase7ApplierTests
public void Changed_tag_alarm_presence_change_rebuilds()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean", FullName: "00001", Writable: false, Alarm: null));
@@ -1267,7 +1267,7 @@ public sealed class Phase7ApplierTests
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean", FullName: "00001", Writable: false,
Alarm: new EquipmentTagAlarmInfo("OffNormalAlarm", 700)));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -1288,7 +1288,7 @@ public sealed class Phase7ApplierTests
public void Changed_alarm_bearing_tag_writable_only_still_rebuilds()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
// Both previous and current carry an identical, non-null alarm — the tag is an alarm-bearing node.
var alarm = new EquipmentTagAlarmInfo("OffNormalAlarm", 700);
@@ -1300,7 +1300,7 @@ public sealed class Phase7ApplierTests
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean",
FullName: "00001", Writable: true, Alarm: alarm));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: the planner sees a changed tag (Writable differs) and nothing else.
plan.ChangedEquipmentTags.Count.ShouldBe(1);
@@ -1323,10 +1323,10 @@ public sealed class Phase7ApplierTests
public void Two_surgical_eligible_tag_deltas_both_apply_in_place_no_rebuild()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
// Two distinct plain (no-alarm) tags, both will flip Writable — both are surgical-eligible.
var previous = new Phase7CompositionResult(
var previous = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -1336,7 +1336,7 @@ public sealed class Phase7ApplierTests
},
};
// Only Writable flips on both tags; everything else is identical.
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -1346,7 +1346,7 @@ public sealed class Phase7ApplierTests
},
};
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: exactly two changed tags, nothing else.
plan.ChangedEquipmentTags.Count.ShouldBe(2);
@@ -1380,9 +1380,9 @@ public sealed class Phase7ApplierTests
public void Surgical_eligible_tag_delta_mixed_with_added_equipment_rebuilds()
{
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = new Phase7CompositionResult(
var previous = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -1391,7 +1391,7 @@ public sealed class Phase7ApplierTests
},
};
// Surgical-eligible Writable flip on the tag AND a brand-new equipment node.
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-new", "New", "line-1") }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -1400,7 +1400,7 @@ public sealed class Phase7ApplierTests
},
};
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1);
plan.AddedEquipment.Count.ShouldBe(1);
@@ -1418,14 +1418,14 @@ public sealed class Phase7ApplierTests
public void Surgical_eligible_delta_on_non_surgical_sink_rebuilds()
{
var sink = new PlainRecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: true, Alarm: null));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -1441,14 +1441,14 @@ public sealed class Phase7ApplierTests
public void Surgical_sink_returning_false_node_missing_falls_back_to_rebuild()
{
var sink = new RecordingSink { SurgicalReturns = false };
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: true, Alarm: null));
var plan = Phase7Planner.Compute(previous, next);
var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan);
@@ -1458,44 +1458,44 @@ public sealed class Phase7ApplierTests
sink.SurgicalCalls.ShouldHaveSingleItem(); // the surgical update was attempted first
}
private static Phase7CompositionResult CompositionWithTags(params EquipmentTagPlan[] tags) =>
private static AddressSpaceComposition CompositionWithTags(params EquipmentTagPlan[] tags) =>
new(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = tags,
};
private static Phase7CompositionResult CompositionWithVirtualTags(params EquipmentVirtualTagPlan[] vtags) =>
private static AddressSpaceComposition CompositionWithVirtualTags(params EquipmentVirtualTagPlan[] vtags) =>
new(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = vtags,
};
private static Phase7CompositionResult CompositionWithAlarms(params ScriptedAlarmPlan[] alarms) =>
private static AddressSpaceComposition CompositionWithAlarms(params ScriptedAlarmPlan[] alarms) =>
// ScriptedAlarmPlans is the set the planner diffs into Added/Removed/ChangedAlarms.
new(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), alarms);
private static Phase7CompositionResult CompositionWithDrivers(params DriverInstancePlan[] drivers) =>
private static AddressSpaceComposition CompositionWithDrivers(params DriverInstancePlan[] drivers) =>
new(
Array.Empty<EquipmentNode>(), drivers, Array.Empty<ScriptedAlarmPlan>());
private static Phase7Plan EmptyPlan => new(
Array.Empty<EquipmentNode>(), Array.Empty<EquipmentNode>(), Array.Empty<Phase7Plan.EquipmentDelta>(),
Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(), Array.Empty<Phase7Plan.DriverDelta>(),
Array.Empty<ScriptedAlarmPlan>(), Array.Empty<ScriptedAlarmPlan>(), Array.Empty<Phase7Plan.AlarmDelta>());
private static AddressSpacePlan EmptyPlan => new(
Array.Empty<EquipmentNode>(), Array.Empty<EquipmentNode>(), Array.Empty<AddressSpacePlan.EquipmentDelta>(),
Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(), Array.Empty<AddressSpacePlan.DriverDelta>(),
Array.Empty<ScriptedAlarmPlan>(), Array.Empty<ScriptedAlarmPlan>(), Array.Empty<AddressSpacePlan.AlarmDelta>());
private static Phase7Plan WithEquipmentRemoval(params string[] ids) => new(
private static AddressSpacePlan WithEquipmentRemoval(params string[] ids) => new(
AddedEquipment: Array.Empty<EquipmentNode>(),
RemovedEquipment: ids.Select(id => new EquipmentNode(id, id, "line-1")).ToArray(),
ChangedEquipment: Array.Empty<Phase7Plan.EquipmentDelta>(),
ChangedEquipment: Array.Empty<AddressSpacePlan.EquipmentDelta>(),
AddedDrivers: Array.Empty<DriverInstancePlan>(),
RemovedDrivers: Array.Empty<DriverInstancePlan>(),
ChangedDrivers: Array.Empty<Phase7Plan.DriverDelta>(),
ChangedDrivers: Array.Empty<AddressSpacePlan.DriverDelta>(),
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>());
ChangedAlarms: Array.Empty<AddressSpacePlan.AlarmDelta>());
private sealed class RecordingSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink
{
@@ -6,13 +6,13 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary>
/// Verifies the live-edit compose seam (<see cref="Phase7Composer.Compose"/>) treats a Galaxy
/// Verifies the live-edit compose seam (<see cref="AddressSpaceComposer.Compose"/>) treats a Galaxy
/// point as an ordinary equipment tag: <c>GalaxyMxGateway</c> is a standard Equipment-kind driver.
/// An equipment-scoped <see cref="Tag"/> (non-null <see cref="Tag.EquipmentId"/>) bound to a
/// <c>GalaxyMxGateway</c> driver living in an <c>Equipment</c>-kind namespace must surface under
/// <see cref="Phase7CompositionResult.EquipmentTags"/> (carrying its driver-side FullName).
/// <see cref="AddressSpaceComposition.EquipmentTags"/> (carrying its driver-side FullName).
/// </summary>
public sealed class Phase7ComposerAliasTagTests
public sealed class AddressSpaceComposerAliasTagTests
{
/// <summary>A <c>GalaxyMxGateway</c> driver in an Equipment-kind namespace carries an
/// equipment-scoped Galaxy tag (EquipmentId set, FolderPath null, TagConfig FullName = the Galaxy
@@ -61,7 +61,7 @@ public sealed class Phase7ComposerAliasTagTests
TagConfig = "{\"FullName\":\"TestMachine_020.TestChangingInt\"}",
};
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip },
new[] { driver }, Array.Empty<ScriptedAlarm>(),
new[] { galaxyTag }, new[] { ns });
@@ -6,12 +6,12 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary>
/// Verifies the live-edit compose seam (<see cref="Phase7Composer.Compose"/>) substitutes the
/// Verifies the live-edit compose seam (<see cref="AddressSpaceComposer.Compose"/>) substitutes the
/// reserved <c>{{equip}}</c> token in a shared VirtualTag script with each owning equipment's
/// derived tag base (from its child-tag <c>FullName</c>s) — so one script reused across N
/// identical machines resolves to N machine-specific dependency graphs.
/// </summary>
public sealed class Phase7ComposerEquipTokenTests
public sealed class AddressSpaceComposerEquipTokenTests
{
/// <summary>One shared <see cref="Script"/> using <c>ctx.GetTag("{{equip}}.Source")</c>, bound
/// to two equipments (TestMachine_001 / _002) each with one equipment Tag whose FullName carries
@@ -79,7 +79,7 @@ public sealed class Phase7ComposerEquipTokenTests
var vt1 = new VirtualTag { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "over50", DataType = "Boolean", ScriptId = "s-equip" };
var vt2 = new VirtualTag { VirtualTagId = "vt-2", EquipmentId = "eq-2", Name = "over50", DataType = "Boolean", ScriptId = "s-equip" };
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip1, equip2 },
new[] { driver1, driver2 }, Array.Empty<ScriptedAlarm>(),
new[] { tag1, tag2 }, new[] { ns },
@@ -4,13 +4,13 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
public sealed class Phase7ComposerPurityTests
public sealed class AddressSpaceComposerPurityTests
{
/// <summary>Verifies empty inputs produce empty result.</summary>
[Fact]
public void Empty_inputs_produce_empty_result()
{
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
equipment: Array.Empty<Equipment>(),
driverInstances: Array.Empty<DriverInstance>(),
scriptedAlarms: Array.Empty<ScriptedAlarm>());
@@ -31,12 +31,12 @@ public sealed class Phase7ComposerPurityTests
var a1 = NewAlarm("a-1", "eq-1");
var a2 = NewAlarm("a-2", "eq-2");
var r1 = Phase7Composer.Compose(
var r1 = AddressSpaceComposer.Compose(
equipment: new[] { e1, e2 },
driverInstances: new[] { d1, d2 },
scriptedAlarms: new[] { a1, a2 });
var r2 = Phase7Composer.Compose(
var r2 = AddressSpaceComposer.Compose(
equipment: new[] { e2, e1 },
driverInstances: new[] { d2, d1 },
scriptedAlarms: new[] { a2, a1 });
@@ -54,8 +54,8 @@ public sealed class Phase7ComposerPurityTests
var drivers = new[] { NewDriver("drv-x") };
var alarms = new[] { NewAlarm("alarm-1", "eq-a") };
var r1 = Phase7Composer.Compose(equipment, drivers, alarms);
var r2 = Phase7Composer.Compose(equipment, drivers, alarms);
var r1 = AddressSpaceComposer.Compose(equipment, drivers, alarms);
var r2 = AddressSpaceComposer.Compose(equipment, drivers, alarms);
// Record equality won't help here — IReadOnlyList<T> uses reference equality. Compare
// element-wise to verify the pure-function contract.
@@ -69,7 +69,7 @@ public sealed class Phase7ComposerPurityTests
public void Output_is_sorted_by_natural_key()
{
var equipment = new[] { NewEquipment("z"), NewEquipment("a"), NewEquipment("m") };
var result = Phase7Composer.Compose(equipment, Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>());
var result = AddressSpaceComposer.Compose(equipment, Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>());
result.EquipmentNodes.Select(e => e.EquipmentId)
.ShouldBe(new[] { "a", "m", "z" });
@@ -83,7 +83,7 @@ public sealed class Phase7ComposerPurityTests
{
var equipment = new[] { NewEquipment("filling-eq") }; // Name="filling-eq", MachineCode="FILLING-EQ"
var node = Phase7Composer.Compose(equipment, Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>())
var node = AddressSpaceComposer.Compose(equipment, Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>())
.EquipmentNodes.ShouldHaveSingleItem();
node.EquipmentId.ShouldBe("filling-eq"); // NodeId stays the logical Id
@@ -94,7 +94,7 @@ public sealed class Phase7ComposerPurityTests
[Fact]
public void Composition_carries_empty_equipment_virtualtags_by_default()
{
var r = new Phase7CompositionResult(
var r = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
r.EquipmentVirtualTags.ShouldBeEmpty();
}
@@ -133,7 +133,7 @@ public sealed class Phase7ComposerPurityTests
SourceHash = "hash-1",
};
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(),
Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -171,7 +171,7 @@ public sealed class Phase7ComposerPurityTests
SourceHash = "hash-1",
};
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(),
Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -197,7 +197,7 @@ public sealed class Phase7ComposerPurityTests
ScriptId = "s-does-not-exist",
};
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(),
Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -5,7 +5,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary>
/// Verifies the live-edit compose seam (<see cref="Phase7Composer.Compose"/>) builds the richer
/// Verifies the live-edit compose seam (<see cref="AddressSpaceComposer.Compose"/>) builds the richer
/// per-equipment scripted-alarm projection (<see cref="EquipmentScriptedAlarmPlan"/>): each alarm
/// is joined to its predicate <see cref="Script"/> for the source, carries the parsed
/// <c>ctx.GetTag("…")</c> read refs UNION the <c>{TagPath}</c> tokens referenced in its
@@ -13,7 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// (including disabled alarms). The projection is deterministic so the upcoming
/// artifact-byte-parity test (sibling task) is reliable.
/// </summary>
public sealed class Phase7ComposerScriptedAlarmTests
public sealed class AddressSpaceComposerScriptedAlarmTests
{
/// <summary>Two equipments, each with a scripted alarm whose predicate script reads one tag via
/// <c>ctx.GetTag("X.Y")</c>. Each plan must carry the resolved PredicateSource, DependencyRefs
@@ -62,7 +62,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
Enabled = true,
};
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { alarm1, alarm2 },
Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -118,7 +118,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
PredicateScriptId = "s-1",
};
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { alarm },
Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -155,7 +155,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
PredicateScriptId = "s-1",
};
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { alarm },
Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -199,7 +199,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
PredicateScriptId = "s-does-not-exist",
};
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { goodAlarm, orphanAlarm },
Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -234,7 +234,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
Enabled = false,
};
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { alarm },
Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -261,12 +261,12 @@ public sealed class Phase7ComposerScriptedAlarmTests
var a3 = NewAlarm("al-3", "eq-1", "s-3");
var scripts = new[] { script1, script2, script3 };
var r1 = Phase7Composer.Compose(
var r1 = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { a1, a2, a3 },
Array.Empty<Tag>(), Array.Empty<Namespace>(), scripts: scripts);
var r2 = Phase7Composer.Compose(
var r2 = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { a3, a1, a2 },
Array.Empty<Tag>(), Array.Empty<Namespace>(), scripts: scripts);
@@ -282,7 +282,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
[Fact]
public void Composition_carries_empty_scripted_alarms_by_default()
{
var r = new Phase7CompositionResult(
var r = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
r.EquipmentScriptedAlarms.ShouldBeEmpty();
}
@@ -6,13 +6,13 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary>
/// Verifies the live-edit compose seam (<see cref="Phase7Composer.Compose"/>) carries the
/// Verifies the live-edit compose seam (<see cref="AddressSpaceComposer.Compose"/>) carries the
/// <see cref="VirtualTag.Historize"/> entity column onto the resulting
/// <see cref="EquipmentVirtualTagPlan.Historize"/> (H5a). The flag was authored in the UI but
/// never threaded onto the plan, leaving equipment-namespace runtime historization dead. Both a
/// true and a false case are asserted so the default (false) and the explicit set both prove out.
/// </summary>
public sealed class Phase7ComposerVirtualTagHistorizeTests
public sealed class AddressSpaceComposerVirtualTagHistorizeTests
{
[Fact]
public void Compose_carries_virtual_tag_historize_flag_onto_plan()
@@ -57,7 +57,7 @@ public sealed class Phase7ComposerVirtualTagHistorizeTests
var vtHist = new VirtualTag { VirtualTagId = "vt-hist", EquipmentId = "eq-1", Name = "Historized", DataType = "Int32", ScriptId = "s-1", Historize = true };
var vtPlain = new VirtualTag { VirtualTagId = "vt-plain", EquipmentId = "eq-1", Name = "Plain", DataType = "Int32", ScriptId = "s-1", Historize = false };
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip },
new[] { driver }, Array.Empty<ScriptedAlarm>(),
new[] { tag }, new[] { ns },
@@ -3,16 +3,16 @@ using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
public sealed class Phase7PlannerTests
public sealed class AddressSpacePlannerTests
{
/// <summary>Verifies that empty inputs produce an empty plan.</summary>
[Fact]
public void Empty_inputs_produce_empty_plan()
{
var prev = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var prev = new AddressSpaceComposition(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = prev;
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeTrue();
}
@@ -22,10 +22,10 @@ public sealed class Phase7PlannerTests
public void Identical_compositions_produce_empty_plan()
{
var eq = new EquipmentNode("eq-1", "Eq 1", "line-1");
var prev = new Phase7CompositionResult(new[] { eq }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(new[] { eq }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var prev = new AddressSpaceComposition(new[] { eq }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new AddressSpaceComposition(new[] { eq }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeTrue();
}
@@ -36,9 +36,9 @@ public sealed class Phase7PlannerTests
[Fact]
public void Equipment_tag_only_change_yields_non_empty_plan_with_added_tag()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -47,7 +47,7 @@ public sealed class Phase7PlannerTests
},
};
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse();
plan.AddedEquipmentTags.Single().TagId.ShouldBe("tag-1");
@@ -61,9 +61,9 @@ public sealed class Phase7PlannerTests
[Fact]
public void Equipment_virtual_tag_only_change_yields_non_empty_plan_with_added_tag()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -73,7 +73,7 @@ public sealed class Phase7PlannerTests
},
};
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse();
plan.AddedEquipmentVirtualTags.Single().VirtualTagId.ShouldBe("vt-1");
@@ -85,7 +85,7 @@ public sealed class Phase7PlannerTests
[Fact]
public void Disappeared_virtual_tag_goes_to_RemovedEquipmentVirtualTags()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -94,10 +94,10 @@ public sealed class Phase7PlannerTests
Expression: "a + b", DependencyRefs: new[] { "a", "b" }),
},
};
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse();
plan.RemovedEquipmentVirtualTags.Single().VirtualTagId.ShouldBe("vt-1");
@@ -111,7 +111,7 @@ public sealed class Phase7PlannerTests
[Fact]
public void Same_id_with_different_expression_routes_to_ChangedEquipmentVirtualTags()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -120,7 +120,7 @@ public sealed class Phase7PlannerTests
Expression: "a + b", DependencyRefs: new[] { "a", "b" }),
},
};
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -130,7 +130,7 @@ public sealed class Phase7PlannerTests
},
};
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse();
plan.ChangedEquipmentVirtualTags.Single().Previous.Expression.ShouldBe("a + b");
@@ -146,7 +146,7 @@ public sealed class Phase7PlannerTests
[Fact]
public void Same_id_with_toggled_historize_routes_to_ChangedEquipmentVirtualTags()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -155,7 +155,7 @@ public sealed class Phase7PlannerTests
Expression: "a + b", DependencyRefs: new[] { "a", "b" }, Historize: false),
},
};
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -165,7 +165,7 @@ public sealed class Phase7PlannerTests
},
};
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse();
plan.ChangedEquipmentVirtualTags.Single().Previous.Historize.ShouldBeFalse();
@@ -181,7 +181,7 @@ public sealed class Phase7PlannerTests
[Fact]
public void Same_id_with_toggled_isarray_routes_to_ChangedEquipmentTags()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -190,7 +190,7 @@ public sealed class Phase7PlannerTests
FullName: "40001", Writable: false, Alarm: null, IsArray: false, ArrayLength: null),
},
};
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
@@ -200,7 +200,7 @@ public sealed class Phase7PlannerTests
},
};
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse();
plan.ChangedEquipmentTags.Single().Previous.IsArray.ShouldBeFalse();
@@ -221,7 +221,7 @@ public sealed class Phase7PlannerTests
var refsA = new[] { "EQ1.Speed", "EQ1.Torque" };
var refsB = new[] { "EQ1.Speed", "EQ1.Torque" };
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -230,7 +230,7 @@ public sealed class Phase7PlannerTests
Expression: "ctx.GetTag(\"EQ1.Speed\") / ctx.GetTag(\"EQ1.Torque\")", DependencyRefs: refsA),
},
};
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentVirtualTags = new[]
@@ -240,7 +240,7 @@ public sealed class Phase7PlannerTests
},
};
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeTrue();
plan.ChangedEquipmentVirtualTags.ShouldBeEmpty();
@@ -252,13 +252,13 @@ public sealed class Phase7PlannerTests
[Fact]
public void New_equipment_goes_to_AddedEquipment()
{
var prev = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-1", "A", "line-1") },
Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.AddedEquipment.Single().EquipmentId.ShouldBe("eq-1");
plan.RemovedEquipment.ShouldBeEmpty();
@@ -269,13 +269,13 @@ public sealed class Phase7PlannerTests
[Fact]
public void Disappeared_equipment_goes_to_RemovedEquipment()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-1", "A", "line-1") },
Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new AddressSpaceComposition(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.RemovedEquipment.Single().EquipmentId.ShouldBe("eq-1");
plan.AddedEquipment.ShouldBeEmpty();
@@ -285,16 +285,16 @@ public sealed class Phase7PlannerTests
[Fact]
public void Same_id_with_different_display_name_routes_to_ChangedEquipment()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-1", "Old", "line-1") },
Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-1", "New", "line-1") },
Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.ChangedEquipment.Single().Previous.DisplayName.ShouldBe("Old");
plan.ChangedEquipment.Single().Current.DisplayName.ShouldBe("New");
@@ -306,16 +306,16 @@ public sealed class Phase7PlannerTests
[Fact]
public void Driver_config_change_routes_to_ChangedDrivers()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(),
new[] { new DriverInstancePlan("drv-1", "Modbus", "{\"host\":\"old\"}") },
Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(),
new[] { new DriverInstancePlan("drv-1", "Modbus", "{\"host\":\"new\"}") },
Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.ChangedDrivers.Single().Current.ConfigJson.ShouldContain("new");
}
@@ -324,16 +324,16 @@ public sealed class Phase7PlannerTests
[Fact]
public void Alarm_message_template_change_routes_to_ChangedAlarms()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(),
Array.Empty<DriverInstancePlan>(),
new[] { new ScriptedAlarmPlan("a-1", "eq-1", "script-1", "old") });
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(),
Array.Empty<DriverInstancePlan>(),
new[] { new ScriptedAlarmPlan("a-1", "eq-1", "script-1", "new") });
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.ChangedAlarms.Single().Current.MessageTemplate.ShouldBe("new");
}
@@ -342,13 +342,13 @@ public sealed class Phase7PlannerTests
[Fact]
public void Added_and_removed_lists_are_sorted_by_id_for_deterministic_ordering()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
new[] { new EquipmentNode("z", "Z", "line-1"), new EquipmentNode("a", "A", "line-1") },
Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new AddressSpaceComposition(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.RemovedEquipment.Select(e => e.EquipmentId).ShouldBe(new[] { "a", "z" });
}
@@ -357,16 +357,16 @@ public sealed class Phase7PlannerTests
[Fact]
public void Mixed_changes_across_all_three_classes_are_captured_in_one_pass()
{
var prev = new Phase7CompositionResult(
var prev = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-keep", "Keep", "line-1"), new EquipmentNode("eq-drop", "Drop", "line-1") },
new[] { new DriverInstancePlan("drv-keep", "Modbus", "{}"), new DriverInstancePlan("drv-change", "Modbus", "{\"v\":1}") },
new[] { new ScriptedAlarmPlan("a-keep", "eq-keep", "s1", "t1") });
var next = new Phase7CompositionResult(
var next = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-keep", "Keep", "line-1"), new EquipmentNode("eq-new", "New", "line-1") },
new[] { new DriverInstancePlan("drv-keep", "Modbus", "{}"), new DriverInstancePlan("drv-change", "Modbus", "{\"v\":2}") },
new[] { new ScriptedAlarmPlan("a-keep", "eq-keep", "s1", "t1"), new ScriptedAlarmPlan("a-new", "eq-new", "s2", "t2") });
var plan = Phase7Planner.Compute(prev, next);
var plan = AddressSpacePlanner.Compute(prev, next);
plan.AddedEquipment.Single().EquipmentId.ShouldBe("eq-new");
plan.RemovedEquipment.Single().EquipmentId.ShouldBe("eq-drop");
@@ -84,7 +84,7 @@ public sealed class DeferredAddressSpaceSinkTests
}
/// <summary>F10b regression: the deferred wrapper MUST forward the surgical capability to a
/// surgical inner sink — otherwise <c>Phase7Applier</c> (which injects THIS wrapper on every
/// surgical inner sink — otherwise <c>AddressSpaceApplier</c> (which injects THIS wrapper on every
/// driver-role host, not the inner <c>SdkAddressSpaceSink</c>) never sees the capability and the
/// in-place tag-attribute optimization is inert in production (it silently falls back to rebuild).</summary>
[Fact]
@@ -14,7 +14,7 @@ public class ExtractTagAlarmTests
[InlineData("{\"FullName\":\"X.Y\",\"alarm\":\"oops\"}", false, null, 0)]
public void ExtractTagAlarm_parses_or_returns_null(string cfg, bool present, string? type, int sev)
{
var info = Phase7Composer.ExtractTagAlarm(cfg);
var info = AddressSpaceComposer.ExtractTagAlarm(cfg);
if (!present) { info.ShouldBeNull(); return; }
info!.AlarmType.ShouldBe(type);
info.Severity.ShouldBe(sev);
@@ -30,7 +30,7 @@ public class ExtractTagAlarmTests
[InlineData("{\"alarm\":{\"alarmType\":\"LimitAlarm\",\"severity\":500,\"historizeToAveva\":\"oops\"}}", null)]
public void ExtractTagAlarm_parses_historizeToAveva(string cfg, bool? expected)
{
var info = Phase7Composer.ExtractTagAlarm(cfg);
var info = AddressSpaceComposer.ExtractTagAlarm(cfg);
info.ShouldNotBeNull();
info!.HistorizeToAveva.ShouldBe(expected);
}
@@ -7,12 +7,12 @@ using ZB.MOM.WW.OtOpcUa.OpcUaServer;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary>
/// Verifies <see cref="Phase7Composer.ExtractTagArray"/> parses the optional array intent from a
/// tag's <c>TagConfig</c> JSON exactly as <see cref="Phase7Composer.ExtractTagHistorize"/> parses
/// Verifies <see cref="AddressSpaceComposer.ExtractTagArray"/> parses the optional array intent from a
/// tag's <c>TagConfig</c> JSON exactly as <see cref="AddressSpaceComposer.ExtractTagHistorize"/> parses
/// the historize intent: the <c>isArray</c> bool (absent / not a bool / non-object root / blank /
/// malformed ⇒ <c>false</c>) and the optional <c>arrayLength</c> uint (only honoured when
/// <c>isArray</c> is true AND the prop is a JSON number that fits uint; else <c>null</c>). Never
/// throws. Also pins the end-to-end <see cref="Phase7Composer.Compose"/> thread-through onto
/// throws. Also pins the end-to-end <see cref="AddressSpaceComposer.Compose"/> thread-through onto
/// <see cref="EquipmentTagPlan.IsArray"/> / <see cref="EquipmentTagPlan.ArrayLength"/>.
/// </summary>
public class ExtractTagArrayTests
@@ -45,13 +45,13 @@ public class ExtractTagArrayTests
[InlineData("{\"isArray\":true,\"arrayLength\":4294967296}", true, null)]
public void ExtractTagArray_parses_or_returns_defaults(string? cfg, bool expectedIsArray, uint? expectedLength)
{
var (isArray, arrayLength) = Phase7Composer.ExtractTagArray(cfg);
var (isArray, arrayLength) = AddressSpaceComposer.ExtractTagArray(cfg);
isArray.ShouldBe(expectedIsArray);
arrayLength.ShouldBe(expectedLength);
}
/// <summary>End-to-end: an equipment tag whose TagConfig carries <c>isArray</c>/<c>arrayLength</c>
/// surfaces those on its <see cref="EquipmentTagPlan"/> through <see cref="Phase7Composer.Compose"/>,
/// surfaces those on its <see cref="EquipmentTagPlan"/> through <see cref="AddressSpaceComposer.Compose"/>,
/// exactly as the historize keys thread through.</summary>
[Fact]
public void Compose_threads_array_keys_onto_equipment_tag_plan()
@@ -94,7 +94,7 @@ public class ExtractTagArrayTests
TagConfig = "{\"FullName\":\"40001\",\"isArray\":true,\"arrayLength\":16}",
};
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip },
new[] { driver }, Array.Empty<ScriptedAlarm>(),
new[] { arrayTag }, new[] { ns });
@@ -147,7 +147,7 @@ public class ExtractTagArrayTests
TagConfig = "{\"FullName\":\"40005\"}",
};
var result = Phase7Composer.Compose(
var result = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip },
new[] { driver }, Array.Empty<ScriptedAlarm>(),
new[] { scalarTag }, new[] { ns });
@@ -28,7 +28,7 @@ public class ExtractTagHistorizeTests
[InlineData("{\"isHistorized\":true,\"historianTagname\":123}", true, null)]
public void ExtractTagHistorize_parses_or_returns_defaults(string? cfg, bool expectedHistorized, string? expectedTagname)
{
var (isHistorized, historianTagname) = Phase7Composer.ExtractTagHistorize(cfg);
var (isHistorized, historianTagname) = AddressSpaceComposer.ExtractTagHistorize(cfg);
isHistorized.ShouldBe(expectedHistorized);
historianTagname.ShouldBe(expectedTagname);
}
@@ -178,7 +178,7 @@ public sealed class DeploymentArtifactAliasParityTests
/// <summary>
/// The load-bearing direct byte-parity proof for the two equipment-tag producers: for the SAME
/// input draft, the live-edit composer (<see cref="Phase7Composer.Compose"/>) and the
/// input draft, the live-edit composer (<see cref="AddressSpaceComposer.Compose"/>) and the
/// artifact decoder (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>)
/// must emit IDENTICAL <c>EquipmentTags</c> — element-wise equal on every field
/// (TagId, EquipmentId, DriverInstanceId, FolderPath, Name, DataType, FullName) AND in the same
@@ -275,7 +275,7 @@ public sealed class DeploymentArtifactAliasParityTests
var namespaces = new[] { ns };
// ---- Side 1: the live-edit composer ----
var composed = Phase7Composer.Compose(
var composed = AddressSpaceComposer.Compose(
areas, lines, equipment, drivers, Array.Empty<ScriptedAlarm>(), tags, namespaces);
// ---- Side 2: serialise the SAME draft to the artifact blob shape ConfigComposer emits
@@ -349,7 +349,7 @@ public sealed class DeploymentArtifactAliasParityTests
/// The native-alarm <c>historizeToAveva</c> opt-out (bool?, the per-condition durable-AVEVA-write
/// gate) is parsed by BOTH equipment-tag producers' <c>ExtractTagAlarm</c> and MUST stay byte-parity:
/// for the SAME tag TagConfig carrying <c>alarm.historizeToAveva: true</c> (and <c>: false</c>), the
/// live-edit composer (<see cref="Phase7Composer.Compose"/>) and the artifact decoder
/// live-edit composer (<see cref="AddressSpaceComposer.Compose"/>) and the artifact decoder
/// (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>) must derive the
/// identical <c>EquipmentTagAlarmInfo.HistorizeToAveva</c>. The galaxy-tag parity test above already
/// covers the absent ⇒ null case; this pins the explicit-bool branch on both sides.
@@ -399,7 +399,7 @@ public sealed class DeploymentArtifactAliasParityTests
};
// ---- Side 1: the live-edit composer ----
var composed = Phase7Composer.Compose(
var composed = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip }, new[] { driver },
Array.Empty<ScriptedAlarm>(), new[] { alarmTag }, new[] { ns });
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary>
/// Proves the Phase 4c array intent (<c>isArray</c> + optional <c>arrayLength</c>), which rides
/// inside the raw <c>TagConfig</c> JSON blob, round-trips with byte-parity through both
/// equipment-tag producers: the live-edit composer (<see cref="Phase7Composer.Compose"/>) and the
/// equipment-tag producers: the live-edit composer (<see cref="AddressSpaceComposer.Compose"/>) and the
/// artifact decoder (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>).
/// A secondary/follower node decoding a serialized deployment artifact MUST materialise array tags
/// identically to the primary, so the artifact side must derive <c>IsArray</c> / <c>ArrayLength</c>
@@ -153,7 +153,7 @@ public sealed class DeploymentArtifactArrayParityTests
var namespaces = new[] { ns };
// ---- Side 1: the live-edit composer ----
var composed = Phase7Composer.Compose(
var composed = AddressSpaceComposer.Compose(
areas, lines, equipment, drivers, Array.Empty<ScriptedAlarm>(), tags, namespaces);
// ---- Side 2: serialise the SAME draft to the artifact blob shape, then decode it ----
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// Verifies the artifact-decode mirror substitutes the reserved <c>{{equip}}</c> token in a
/// VirtualTag script's <c>ctx.GetTag("…")</c> literals with the owning equipment's tag base
/// (derived from its child Equipment-namespace tag's FullName) — byte-parity with
/// <c>Phase7Composer.Compose</c>'s live-edit path, using the same shared
/// <c>AddressSpaceComposer.Compose</c>'s live-edit path, using the same shared
/// <c>EquipmentScriptPaths</c> helper and the same equipmentTags-derived base.
/// </summary>
public sealed class DeploymentArtifactEquipTokenTests
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary>
/// Proves the Phase C HistoryRead intent (<c>isHistorized</c> + optional <c>historianTagname</c>),
/// which rides inside the raw <c>TagConfig</c> JSON blob, round-trips with byte-parity through both
/// equipment-tag producers: the live-edit composer (<see cref="Phase7Composer.Compose"/>) and the
/// equipment-tag producers: the live-edit composer (<see cref="AddressSpaceComposer.Compose"/>) and the
/// artifact decoder (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>).
/// The artifact serializer re-parses the SAME <c>TagConfig</c> string both sides emit, so no
/// ConfigComposer change is needed — the flags are already carried in the blob.
@@ -120,7 +120,7 @@ public sealed class DeploymentArtifactHistorizeParityTests
var namespaces = new[] { ns };
// ---- Side 1: the live-edit composer ----
var composed = Phase7Composer.Compose(
var composed = AddressSpaceComposer.Compose(
areas, lines, equipment, drivers, Array.Empty<ScriptedAlarm>(), tags, namespaces);
// ---- Side 2: serialise the SAME draft to the artifact blob shape, then decode it ----
@@ -11,7 +11,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary>
/// Byte-parity tests for the scripted-alarm deployment-artifact decode path
/// (<c>DeploymentArtifact.BuildEquipmentScriptedAlarmPlans</c>) against the live compose seam
/// (<c>Phase7Composer.Compose</c>). Both sides derive <c>EquipmentScriptedAlarmPlan</c> from the
/// (<c>AddressSpaceComposer.Compose</c>). Both sides derive <c>EquipmentScriptedAlarmPlan</c> from the
/// same ScriptedAlarm + Script data via the shared <c>EquipmentScriptPaths.ExtractAlarmDependencyRefs</c>
/// helper, so the decoded plans must equal the composer's element-wise (the record has value
/// equality including DependencyRefs order). Mirrors the existing EquipmentVirtualTags parity
@@ -70,7 +70,7 @@ public sealed class DeploymentArtifactScriptedAlarmParityTests
Enabled = false,
};
var composed = Phase7Composer.Compose(
var composed = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { alarm1, alarm2 },
Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -189,7 +189,7 @@ public sealed class DeploymentArtifactScriptedAlarmParityTests
Enabled = true,
};
var composed = Phase7Composer.Compose(
var composed = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { goodAlarm, orphanAlarm },
Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -257,7 +257,7 @@ public sealed class DeploymentArtifactTests
/// 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.
/// artifact-decode mirror of <c>AddressSpaceComposer.Compose</c>'s VirtualTag producer.
/// </summary>
[Fact]
public void ParseComposition_reads_EquipmentVirtualTags_from_virtualtags_and_scripts()
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary>
/// Byte-parity tests for the VirtualTag <c>Historize</c> flag across the artifact-decode path
/// (<c>DeploymentArtifact.BuildEquipmentVirtualTagPlans</c>) and the live compose seam
/// (<c>Phase7Composer.Compose</c>) — H5b. The artifact JSON already carries a Pascal-case
/// (<c>AddressSpaceComposer.Compose</c>) — H5b. The artifact JSON already carries a Pascal-case
/// <c>"Historize"</c> bool (ConfigComposer serialises the whole VirtualTag entity with
/// <c>DefaultIgnoreCondition.Never</c>); the decode just had to read it. Both sides default to
/// <c>false</c> when the flag is unset/absent/non-bool, so the decoded plans must equal the
@@ -65,7 +65,7 @@ public sealed class DeploymentArtifactVirtualTagHistorizeParityTests
var vtHist = new VirtualTag { VirtualTagId = "vt-hist", EquipmentId = "eq-1", Name = "Historized", DataType = "Int32", ScriptId = "s-1", Historize = true };
var vtPlain = new VirtualTag { VirtualTagId = "vt-plain", EquipmentId = "eq-1", Name = "Plain", DataType = "Int32", ScriptId = "s-1", Historize = false };
var composed = Phase7Composer.Compose(
var composed = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip },
new[] { driver }, Array.Empty<ScriptedAlarm>(),
new[] { tag }, new[] { ns },
@@ -24,7 +24,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
{
var db = NewInMemoryDbFactory();
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
SeedDeployment(db, equipmentIds: new[] { "eq-1", "eq-2" }, driverIds: new[] { "drv-1" });
@@ -49,7 +49,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
{
var db = NewInMemoryDbFactory();
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
// No deployment seeded — LoadLatestArtifact returns empty blob.
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
@@ -69,7 +69,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
{
var db = NewInMemoryDbFactory();
var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
SeedDeployment(db, equipmentIds: new[] { "eq-1" }, driverIds: Array.Empty<string>());
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
@@ -114,7 +114,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
// --- SITE-A node: only the SITE-A tag's variable, never MAIN's. ---
var dbA = NewInMemoryDbFactory();
var sinkA = new RecordingSink();
var applierA = new Phase7Applier(sinkA, NullLogger<Phase7Applier>.Instance);
var applierA = new AddressSpaceApplier(sinkA, NullLogger<AddressSpaceApplier>.Instance);
SeedMultiClusterDeployment(dbA);
var siteActor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
@@ -134,7 +134,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
// --- MAIN node: the mirror — only MAIN's tag's variable, never SITE-A's. ---
var dbM = NewInMemoryDbFactory();
var sinkM = new RecordingSink();
var applierM = new Phase7Applier(sinkM, NullLogger<Phase7Applier>.Instance);
var applierM = new AddressSpaceApplier(sinkM, NullLogger<AddressSpaceApplier>.Instance);
SeedMultiClusterDeployment(dbM);
var mainActor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(