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",
|
||||
"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": []
|
||||
},
|
||||
{
|
||||
|
||||
@@ -365,6 +365,65 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
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
|
||||
/// assert the host historizes (or does not) and with what path + snapshot.</summary>
|
||||
private sealed class CapturingHistoryWriter : IHistoryWriter
|
||||
|
||||
Reference in New Issue
Block a user