feat(runtime): DriverInstanceActor forwards batched data changes

This commit is contained in:
Joseph Doherty
2026-08-15 12:16:49 -04:00
parent 04e6a4690c
commit e04ff2488c
5 changed files with 332 additions and 27 deletions
@@ -121,14 +121,16 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
new DriverInstanceActor.Subscribe(new[] { "tag-a", "tag-b" }, TimeSpan.FromMilliseconds(250)),
TimeSpan.FromSeconds(3));
// Driver fires an OnDataChange — actor should forward it to its parent as
// AttributeValuePublished carrying the driver's status code.
// Driver fires an OnDataChange — actor should forward it to its parent as an
// AttributeValueBatchPublished (single-item, since this driver is per-tag-only) carrying the
// driver's status code.
driver.FireDataChange("tag-a", value: 3.14, statusCode: 0u);
var published = parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>(TimeSpan.FromSeconds(2));
published.FullReference.ShouldBe("tag-a");
published.Value.ShouldBe(3.14);
published.StatusCode.ShouldBe(OpcUaStatus.Good);
var published = parent.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>(TimeSpan.FromSeconds(2));
published.Items.Count.ShouldBe(1);
published.Items[0].FullReference.ShouldBe("tag-a");
published.Items[0].Value.ShouldBe(3.14);
published.Items[0].StatusCode.ShouldBe(OpcUaStatus.Good);
}
/// <summary>
@@ -162,17 +164,19 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
// Uncertain — severity bits 01 (top 2 bits = 01).
driver.FireDataChange("tag-1", value: 1, statusCode: 0x40000000u);
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().StatusCode.ShouldBe(OpcUaStatus.Uncertain);
parent.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>().Items[0].StatusCode
.ShouldBe(OpcUaStatus.Uncertain);
// Bad — severity bits 10.
driver.FireDataChange("tag-1", value: 2, statusCode: 0x80000000u);
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().StatusCode.ShouldBe(OpcUaStatus.Bad);
parent.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>().Items[0].StatusCode
.ShouldBe(OpcUaStatus.Bad);
// The regression itself: a Bad SUB-code must arrive intact, not flattened to OpcUaStatus.Bad.
// This is the exact code MTConnect maps the Agent's UNAVAILABLE sentinel to, and the live gate
// that opened #519 observed it reaching a client as 0x80000000.
driver.FireDataChange("tag-1", value: null, statusCode: OpcUaStatus.BadNoCommunication);
var noComms = parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>();
var noComms = parent.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>().Items[0];
noComms.StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
noComms.StatusCode.ShouldNotBe(OpcUaStatus.Bad, "the sub-code was collapsed to a generic Bad");
OpcUaStatus.IsBad(noComms.StatusCode).ShouldBeTrue("it must still classify as Bad");
@@ -180,7 +184,8 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
// A second, DIFFERENT Bad sub-code — the two were indistinguishable before the fix, so asserting
// one alone would not prove the sub-code is carried rather than hard-coded.
driver.FireDataChange("tag-1", value: null, statusCode: 0x80740000u /* BadTypeMismatch */);
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().StatusCode.ShouldBe(0x80740000u);
parent.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>().Items[0].StatusCode
.ShouldBe(0x80740000u);
}
/// <summary>
@@ -209,7 +214,8 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
// The auto-subscription is live — a data change reaches the parent.
driver.FireDataChange("tag-a", value: 7, statusCode: 0u);
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>(TimeSpan.FromSeconds(2)).Value.ShouldBe(7);
parent.ExpectMsg<DriverInstanceActor.AttributeValueBatchPublished>(TimeSpan.FromSeconds(2))
.Items[0].Value.ShouldBe(7);
// Reconnect → the desired set is re-established without any new host message.
actor.Tell(new DriverInstanceActor.DisconnectObserved("backend blip"));