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:
+57
@@ -0,0 +1,57 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
||||
|
||||
/// <summary>lmxopcua#783 — <c>VirtualTagNoEffectiveTrigger</c>, the VirtualTag mirror of
|
||||
/// <c>CalculationNoEffectiveTrigger</c>.</summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class DraftValidatorVirtualTagTriggerTests
|
||||
{
|
||||
private static Script ScriptRow(string id, string source) => new()
|
||||
{
|
||||
ScriptId = id, Name = id, SourceCode = source, SourceHash = "h-" + id,
|
||||
};
|
||||
|
||||
private static VirtualTag Vt(bool changeTriggered, int? timer, string scriptId = "s1") => new()
|
||||
{
|
||||
VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "V", DataType = "Double", ScriptId = scriptId,
|
||||
ChangeTriggered = changeTriggered, TimerIntervalMs = timer,
|
||||
};
|
||||
|
||||
private static IEnumerable<ValidationError> Run(VirtualTag vt, string source) =>
|
||||
DraftValidator.Validate(new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c",
|
||||
VirtualTags = [vt],
|
||||
Scripts = [ScriptRow("s1", source)],
|
||||
}).Where(e => e.Code == "VirtualTagNoEffectiveTrigger");
|
||||
|
||||
[Fact]
|
||||
public void Change_triggered_with_no_read_and_no_timer_is_rejected() =>
|
||||
Run(Vt(true, null), "return 1;").ShouldHaveSingleItem().Context.ShouldBe("vt-1");
|
||||
|
||||
[Fact]
|
||||
public void Not_change_triggered_and_no_timer_is_rejected() =>
|
||||
Run(Vt(false, null), "return (double)ctx.GetTag(\"a/b\").Value;").ShouldHaveSingleItem();
|
||||
|
||||
[Fact]
|
||||
public void A_timer_alone_is_an_effective_trigger() =>
|
||||
Run(Vt(true, 1000), "return 1;").ShouldBeEmpty();
|
||||
|
||||
[Fact]
|
||||
public void A_literal_read_with_change_trigger_is_an_effective_trigger() =>
|
||||
Run(Vt(true, null), "return (double)ctx.GetTag(\"a/b\").Value;").ShouldBeEmpty();
|
||||
|
||||
[Fact]
|
||||
public void An_unresolved_script_reports_no_trigger_error()
|
||||
{
|
||||
var errors = DraftValidator.Validate(new DraftSnapshot
|
||||
{
|
||||
GenerationId = 1, ClusterId = "c", VirtualTags = [Vt(true, null, "s-missing")],
|
||||
});
|
||||
errors.ShouldNotContain(e => e.Code == "VirtualTagNoEffectiveTrigger");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user