merge worktree-agent-ad3207d913b3b74e6 (B3-WP4) into v3/batch3-uns-rework

This commit is contained in:
Joseph Doherty
2026-07-16 06:59:22 -04:00
23 changed files with 1142 additions and 426 deletions
@@ -297,6 +297,117 @@ public sealed class DraftValidatorTests
DisplayNameOverride = overrideName,
};
// ------------------------------------------------------------------------------------
// ValidateEquipReferenceResolution — v3 WP4: {{equip}}/<RefName> must resolve to a reference
// ------------------------------------------------------------------------------------
private static Script BuildScript(string id, string source) => new()
{
ScriptId = id, Name = id, SourceCode = source, SourceHash = $"h-{id}",
};
private static Tag BuildRawTag(string tagId, string name) => new()
{
TagId = tagId, DeviceId = "dev-1", Name = name, DataType = "Float",
AccessLevel = TagAccessLevel.Read, TagConfig = "{}",
};
private static UnsTagReference BuildReference(string id, string equipmentId, string tagId, string? overrideName = null) => new()
{
UnsTagReferenceId = id, EquipmentId = equipmentId, TagId = tagId, DisplayNameOverride = overrideName,
};
/// <summary>A VirtualTag script using <c>{{equip}}/Speed</c> with no reference "Speed" on the equipment is
/// a deploy error naming the equipment + the missing ref.</summary>
[Fact]
public void VirtualTag_equip_ref_unresolved_is_deploy_error()
{
var draft = new DraftSnapshot
{
GenerationId = 1, ClusterId = "c",
Scripts = [BuildScript("s-1", "return ctx.GetTag(\"{{equip}}/Speed\").Value;")],
VirtualTags = [BuildVirtualTag(equipmentId: "eq-1", name: "vt")],
};
var err = DraftValidator.Validate(draft).First(e => e.Code == "EquipReferenceUnresolved");
err.Message.ShouldContain("eq-1");
err.Message.ShouldContain("Speed");
}
/// <summary>The same script resolves cleanly when the equipment has a reference whose effective name is
/// "Speed" (backing raw tag's Name).</summary>
[Fact]
public void VirtualTag_equip_ref_resolved_is_clean()
{
var draft = new DraftSnapshot
{
GenerationId = 1, ClusterId = "c",
Scripts = [BuildScript("s-1", "return ctx.GetTag(\"{{equip}}/Speed\").Value;")],
VirtualTags = [BuildVirtualTag(equipmentId: "eq-1", name: "vt")],
Tags = [BuildRawTag("tag-1", "Speed")],
UnsTagReferences = [BuildReference("ref-1", "eq-1", "tag-1")],
};
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "EquipReferenceUnresolved");
}
/// <summary>A DisplayNameOverride is the effective name: <c>{{equip}}/MotorSpeed</c> resolves to a
/// reference overridden to "MotorSpeed" even though the backing raw tag's Name is "raw_speed".</summary>
[Fact]
public void VirtualTag_equip_ref_resolves_by_override_name()
{
var draft = new DraftSnapshot
{
GenerationId = 1, ClusterId = "c",
Scripts = [BuildScript("s-1", "return ctx.GetTag(\"{{equip}}/MotorSpeed\").Value;")],
VirtualTags = [BuildVirtualTag(equipmentId: "eq-1", name: "vt")],
Tags = [BuildRawTag("tag-1", "raw_speed")],
UnsTagReferences = [BuildReference("ref-1", "eq-1", "tag-1", overrideName: "MotorSpeed")],
};
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "EquipReferenceUnresolved");
}
/// <summary>A reference on a DIFFERENT equipment does not satisfy the token — resolution is per owning
/// equipment.</summary>
[Fact]
public void VirtualTag_equip_ref_does_not_resolve_across_equipment()
{
var draft = new DraftSnapshot
{
GenerationId = 1, ClusterId = "c",
Scripts = [BuildScript("s-1", "return ctx.GetTag(\"{{equip}}/Speed\").Value;")],
VirtualTags = [BuildVirtualTag(equipmentId: "eq-1", name: "vt")],
Tags = [BuildRawTag("tag-1", "Speed")],
UnsTagReferences = [BuildReference("ref-1", "eq-2", "tag-1")], // reference is on eq-2, not eq-1
};
DraftValidator.Validate(draft).ShouldContain(e => e.Code == "EquipReferenceUnresolved");
}
/// <summary>Alarm message-template <c>{{equip}}/Temp</c> follows the same rule — an unresolved ref in the
/// template is a deploy error.</summary>
[Fact]
public void ScriptedAlarm_message_template_equip_ref_unresolved_is_deploy_error()
{
var draft = new DraftSnapshot
{
GenerationId = 1, ClusterId = "c",
Scripts = [BuildScript("s-1", "return true;")],
ScriptedAlarms =
[
new ScriptedAlarm
{
ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "overheat",
AlarmType = "LimitAlarm", MessageTemplate = "Too hot: {{equip}}/Temp",
PredicateScriptId = "s-1",
},
],
};
DraftValidator.Validate(draft).ShouldContain(e => e.Code == "EquipReferenceUnresolved");
}
// ------------------------------------------------------------------------------------
// Phase 6.3 task #148 part 2 — ValidateClusterTopology (unchanged in v3)
// ------------------------------------------------------------------------------------