feat(runtime): EvaluationResult carries OpcUaQuality; VT bridge publishes it + historizes BadInternalError on Bad (02/S13)

This commit is contained in:
Joseph Doherty
2026-07-13 09:51:15 -04:00
parent b977548593
commit c5ca3e8587
4 changed files with 91 additions and 14 deletions
@@ -406,6 +406,48 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
bad.TimestampUtc.ShouldBe(ts2);
}
/// <summary>02/S13 bridge: a Bad-quality <see cref="VirtualTagActor.EvaluationResult"/> must bridge to
/// an <see cref="OpcUaPublishActor.AttributeValueUpdate"/> carrying <see cref="OpcUaQuality.Bad"/>
/// (not the old hard-coded Good).</summary>
[Fact]
public void Bad_quality_EvaluationResult_is_bridged_as_Bad()
{
var publish = CreateTestProbe();
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux: null, new StubEvaluator()));
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { Plan("vt-1", "eq-1", "speed-rpm") }));
var ts = new DateTime(2026, 7, 12, 12, 0, 0, DateTimeKind.Utc);
host.Tell(new VirtualTagActor.EvaluationResult(
"vt-1", 42.0, ts, CorrelationId.NewId(), OpcUaQuality.Bad));
var update = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
update.Quality.ShouldBe(OpcUaQuality.Bad);
update.Value.ShouldBe(42.0);
}
/// <summary>02/S13 historize parity: a Bad result on a historized plan must record the dormant
/// engine's BadInternalError status (0x80020000u) in the snapshot, not the Good 0u.</summary>
[Fact]
public void Historized_Bad_result_is_recorded_with_BadInternalError_status()
{
var publish = CreateTestProbe();
var writer = new CapturingHistoryWriter();
var host = Sys.ActorOf(VirtualTagHostActor.Props(publish.Ref, mux: null, new StubEvaluator(), writer));
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(
new[] { PlanH("vt-1", "eq-1", "speed-rpm", historize: true) }));
var ts = new DateTime(2026, 7, 12, 12, 0, 0, DateTimeKind.Utc);
host.Tell(new VirtualTagActor.EvaluationResult(
"vt-1", 42.0, ts, CorrelationId.NewId(), OpcUaQuality.Bad));
publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
AwaitAssert(() => writer.Calls.Count.ShouldBe(1));
writer.Calls.TryPeek(out var captured).ShouldBeTrue();
captured.Value.StatusCode.ShouldBe(0x80020000u); // BadInternalError — dormant-engine parity
}
/// <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>