fix(virtualtags): consume VirtualTag.TimerIntervalMs; add VirtualTagNoEffectiveTrigger gate (lmxopcua#783)
v2-ci / unit-tests (push) Waiting to run
v2-ci / unit-tests (push) Waiting to run
Choice: implement the timer (option a). The field was designed and promised as a working trigger: the entity doc says ChangeTriggered and TimerIntervalMs together realize the trigger model, the DB check constraint requires ChangeTriggered OR a timer >= 50, the AdminUI virtual-tag modal exposes "TimerIntervalMs (optional)" and UnsTreeService rejects a tag with neither, and the 2026-06-08 design specified "ChangeTriggered OR TimerIntervalMs>=50". Only the runtime never honoured it (the standalone TimerTriggerScheduler was deleted in #603), so a timer-only tag sat at BadWaitingForInitialData forever. - EquipmentVirtualTagPlan carries ChangeTriggered/TimerIntervalMs (in equality, so an edit respawns the child); both the composer and the artifact decode fill them. - VirtualTagActor schedules a repeating tick (PreStart, cancelled in PostStop, clamped to the 50 ms floor). ChangeTriggered=false makes the timer the only trigger. A tick during an in-flight async run (#779) is dropped, not stashed, so slow scripts never build a backlog of runs. - DraftValidator VirtualTagNoEffectiveTrigger (hard error, same severity as CalculationNoEffectiveTrigger): no timer and no change trigger with a literal ctx.GetTag read. Tests: VirtualTagActorTimerTests, DraftValidatorVirtualTagTriggerTests, artifact decode test. Mutation-checked: no scheduling, stashing ticks, no cancel in PostStop, and removing the validator call each fail a test. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
+27
@@ -136,6 +136,33 @@ public sealed class DeploymentArtifactVirtualTagHistorizeParityTests
|
||||
decoded.EquipmentVirtualTags.ShouldHaveSingleItem().Historize.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>lmxopcua#783 — the triggers survive the artifact decode (they used to be dropped, so the runtime
|
||||
/// never knew a timer existed); absent fields keep the entity defaults.</summary>
|
||||
[Fact]
|
||||
public void ParseComposition_carries_timer_and_change_trigger()
|
||||
{
|
||||
var blob = BlobOf(new
|
||||
{
|
||||
Namespaces = new[] { new { NamespaceId = "ns-eq", Kind = 0 } },
|
||||
Scripts = new[] { new { ScriptId = "s-1", SourceCode = "return 1;" } },
|
||||
VirtualTags = new object[]
|
||||
{
|
||||
new { VirtualTagId = "vt-timer", EquipmentId = "eq-1", Name = "T", DataType = "Int32", ScriptId = "s-1",
|
||||
ChangeTriggered = false, TimerIntervalMs = 1000 },
|
||||
new { VirtualTagId = "vt-default", EquipmentId = "eq-1", Name = "D", DataType = "Int32", ScriptId = "s-1" },
|
||||
},
|
||||
});
|
||||
|
||||
var decoded = DeploymentArtifact.ParseComposition(blob);
|
||||
|
||||
var t = decoded.EquipmentVirtualTags.Single(p => p.VirtualTagId == "vt-timer");
|
||||
t.TimerIntervalMs.ShouldBe(1000);
|
||||
t.ChangeTriggered.ShouldBeFalse();
|
||||
var d = decoded.EquipmentVirtualTags.Single(p => p.VirtualTagId == "vt-default");
|
||||
d.TimerIntervalMs.ShouldBeNull();
|
||||
d.ChangeTriggered.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// The Pascal-case snapshot a VirtualTag EF entity serialises to (matches ConfigComposer's
|
||||
// DefaultIgnoreCondition.Never whole-entity serialisation — includes Historize).
|
||||
private static object ToSnapshot(VirtualTag v) => new
|
||||
|
||||
Reference in New Issue
Block a user