fix(historian): provision tags whose historization flips on or historian name changes (#600)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 06:45:48 -04:00
parent 3baa332d6b
commit dc54b883eb
2 changed files with 73 additions and 9 deletions
@@ -146,6 +146,46 @@ public sealed class AddressSpaceApplierProvisioningTests
prov.Seen[0].TagName.ShouldBe("Pump1.Good");
}
/// <summary>#600.1: a CHANGED raw tag that flips to historized, or whose historian name / data type
/// changes, is provisioned; an unchanged-historization edit and an alarm-bearing tag are not. The
/// added "Control" tag is the positive control so the absences are not a vacuous no-dispatch.</summary>
[Fact]
public async Task Apply_provisions_changed_tags_whose_historization_flips_or_name_changes()
{
var prov = new CapturingProvisioner();
var applier = new AddressSpaceApplier(NullOpcUaAddressSpaceSink.Instance, NullLogger<AddressSpaceApplier>.Instance, prov);
var flipOff = NonHistorizedTag("Flip", "Float32") with { TagId = "t-flip", NodeId = "Plant/Flip" };
var flipOn = flipOff with { IsHistorized = true };
var renOld = HistorizedTag("Ren", "Old.Name", "Float32") with { TagId = "t-ren" };
var renNew = renOld with { HistorianTagname = "New.Name" };
var sameOld = HistorizedTag("Same", "Same.Name", "Float32") with { TagId = "t-same", Writable = false };
var sameNew = sameOld with { Writable = true }; // non-historian edit
var alarmOld = NonHistorizedTag("Alm", "Float32") with { TagId = "t-alm" };
var alarmNew = alarmOld with
{
IsHistorized = true,
Alarm = new EquipmentTagAlarmInfo("AlarmCondition", 500),
};
var plan = PlanWithAddedTags(HistorizedTag("Control", "Control.Name", "Float32")) with
{
ChangedRawTags = new[]
{
new AddressSpacePlan.RawTagDelta(flipOff, flipOn),
new AddressSpacePlan.RawTagDelta(renOld, renNew),
new AddressSpacePlan.RawTagDelta(sameOld, sameNew),
new AddressSpacePlan.RawTagDelta(alarmOld, alarmNew),
},
};
applier.Apply(plan);
await prov.Called.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
prov.Seen.Select(r => r.TagName).ShouldBe(
new[] { "Control.Name", "Plant/Flip", "New.Name" }, ignoreOrder: true);
}
/// <summary>Capturing <see cref="ILogger{T}"/> that records every log line and signals a
/// <see cref="TaskCompletionSource{TResult}"/> the first time a "historian provisioning completed" tally is
/// emitted, so the fire-and-forget continuation's log can be awaited deterministically.</summary>