test(runtime): red repro — VT script failure after success leaves node stale-Good (02/S13)
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
{
|
{
|
||||||
"id": "R2-03-T1",
|
"id": "R2-03-T1",
|
||||||
"subject": "S13 stale-Good repro test at host level (RED — must fail on f6eaa267): fail-after-success evaluator, publish probe sees Good then expects Bad AttributeValueUpdate",
|
"subject": "S13 stale-Good repro test at host level (RED — must fail on f6eaa267): fail-after-success evaluator, publish probe sees Good then expects Bad AttributeValueUpdate",
|
||||||
"status": "pending",
|
"status": "completed",
|
||||||
"blockedBy": []
|
"blockedBy": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -365,6 +365,65 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
|||||||
AwaitAssert(() => evaluator.ClearCount.ShouldBe(2));
|
AwaitAssert(() => evaluator.ClearCount.ShouldBe(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 02/S13 stale-Good repro (RED on current code): a script that succeeds once then starts failing
|
||||||
|
/// must degrade its node's quality to Bad — carrying the last-known value — rather than freezing on
|
||||||
|
/// the last Good value forever. Drives the REAL host + real child + parent-Tell bridge: apply one
|
||||||
|
/// plan (deps ["a"]), capture the spawned child via the mux probe's RegisterInterest sender, then
|
||||||
|
/// feed it two dependency changes. The first evaluates Ok(42) → a Good AttributeValueUpdate; the
|
||||||
|
/// second fails → the fix publishes a Bad AttributeValueUpdate carrying the stale 42. On the
|
||||||
|
/// unfixed tree the second publish never arrives (the second ExpectMsg times out).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Failing_script_after_success_degrades_node_quality_to_Bad()
|
||||||
|
{
|
||||||
|
var publish = CreateTestProbe();
|
||||||
|
var mux = CreateTestProbe();
|
||||||
|
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux.Ref,
|
||||||
|
new FailAfterFirstSuccessEvaluator()));
|
||||||
|
|
||||||
|
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") }));
|
||||||
|
|
||||||
|
// The child self-registers with the mux in PreStart — capture its ref from the sender so we can
|
||||||
|
// drive it directly through the real evaluate → parent-Tell → bridge path.
|
||||||
|
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>();
|
||||||
|
var child = mux.LastSender;
|
||||||
|
|
||||||
|
var ts1 = new DateTime(2026, 7, 12, 12, 0, 0, DateTimeKind.Utc);
|
||||||
|
var ts2 = new DateTime(2026, 7, 12, 12, 0, 1, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
// First dep change → evaluator Ok(42) → Good publish.
|
||||||
|
child.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, ts1));
|
||||||
|
var good = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||||
|
good.Quality.ShouldBe(OpcUaQuality.Good);
|
||||||
|
good.Value.ShouldBe(42.0);
|
||||||
|
|
||||||
|
// Second dep change → evaluator Failure → Bad publish carrying the last-known value 42.
|
||||||
|
child.Tell(new VirtualTagActor.DependencyValueChanged("a", 2, ts2));
|
||||||
|
var bad = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
|
||||||
|
bad.Quality.ShouldBe(OpcUaQuality.Bad);
|
||||||
|
bad.Value.ShouldBe(42.0);
|
||||||
|
bad.TimestampUtc.ShouldBe(ts2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Evaluator that returns <c>Ok(42.0)</c> on its first call and <c>Failure</c> thereafter —
|
||||||
|
/// the S13 "was working, now broken" shape. Interlocked counter so it is safe across the actor's
|
||||||
|
/// evaluation thread.</summary>
|
||||||
|
private sealed class FailAfterFirstSuccessEvaluator : IVirtualTagEvaluator
|
||||||
|
{
|
||||||
|
private int _calls;
|
||||||
|
|
||||||
|
/// <summary>Returns Ok(42.0) once, then Failure("boom").</summary>
|
||||||
|
/// <param name="id">The tag identifier.</param>
|
||||||
|
/// <param name="expr">The expression string.</param>
|
||||||
|
/// <param name="deps">The dependency values.</param>
|
||||||
|
/// <returns>Ok(42.0) on the first call; Failure on every subsequent call.</returns>
|
||||||
|
public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
|
||||||
|
=> Interlocked.Increment(ref _calls) == 1
|
||||||
|
? VirtualTagEvalResult.Ok(42.0)
|
||||||
|
: VirtualTagEvalResult.Failure("boom");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Capturing <see cref="IHistoryWriter"/>: records every Record call so H5c tests can
|
/// <summary>Capturing <see cref="IHistoryWriter"/>: records every Record call so H5c tests can
|
||||||
/// assert the host historizes (or does not) and with what path + snapshot.</summary>
|
/// assert the host historizes (or does not) and with what path + snapshot.</summary>
|
||||||
private sealed class CapturingHistoryWriter : IHistoryWriter
|
private sealed class CapturingHistoryWriter : IHistoryWriter
|
||||||
|
|||||||
Reference in New Issue
Block a user