diff --git a/archreview/plans/R3-02-stale-good-family-plan.md b/archreview/plans/R3-02-stale-good-family-plan.md
index 0c497741..2926347e 100644
--- a/archreview/plans/R3-02-stale-good-family-plan.md
+++ b/archreview/plans/R3-02-stale-good-family-plan.md
@@ -731,3 +731,13 @@ test could only pass once the production change landed.
- The identical-value rule is honoured: the driver pushes **5.0 Good** before and after the outage, so the
recovery assertion cannot be satisfied by a value change. The control (`InitialPush = null`) asserts
`ExpectNoMsg` — no Good ever arrives — and that the audit fires with exactly 1 latched ref.
+
+### T12 — `DependencyConsumerMuxAdapter` forwards the real status code
+
+- RED: `--filter "…DependencyConsumerMuxAdapterTests"` ⇒ **2 failed**, both
+ `Shouldly.ShouldAssertException : consumer.Received[…].StatusCode should be 2150694912u` (0x80310000)
+ — the consumer received `0` (Good) for a `BadNoCommunication` input, on both the single and the batch
+ path. That is the finding, reproduced at the seam.
+- GREEN: 11 passed (`DependencyConsumerMuxAdapterTests` + `DependencyMuxActorTests`).
+- The false "quality is stripped at the mux boundary" comment is deleted and replaced with why it was
+ false (theme 6: the contract moved at #478/#519 and the comment outlived the fact).
diff --git a/archreview/plans/R3-02-stale-good-family-plan.md.tasks.json b/archreview/plans/R3-02-stale-good-family-plan.md.tasks.json
index ab034166..ac23153c 100644
--- a/archreview/plans/R3-02-stale-good-family-plan.md.tasks.json
+++ b/archreview/plans/R3-02-stale-good-family-plan.md.tasks.json
@@ -87,7 +87,7 @@
{
"id": "T12",
"subject": "DependencyConsumerMuxAdapter forwards the real StatusCode; delete the false 'quality is stripped' comment (05/STAB-19)",
- "status": "pending",
+ "status": "done",
"blockedBy": []
},
{
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/DependencyConsumerMuxAdapter.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/DependencyConsumerMuxAdapter.cs
index a33bc254..7a41c587 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/DependencyConsumerMuxAdapter.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/DependencyConsumerMuxAdapter.cs
@@ -66,10 +66,23 @@ public sealed class DependencyConsumerMuxAdapter : ReceiveActor
_log.Debug("DependencyConsumerMuxAdapter: registered {Count} dependency ref(s)", refs.Count);
}
+ ///
+ /// Forwards one dependency value to the consumer, status code included (archreview
+ /// 05/STAB-19).
+ ///
+ ///
+ /// This used to pass a hard-coded 0u behind a comment saying quality was stripped at the
+ /// mux boundary. That was true when the adapter was written and stopped being true at #478/#519 —
+ /// has carried the source driver's full status
+ /// code, sub-code and all, ever since. The stale comment outliving the fact is what kept a
+ /// comms-lost input arriving at the Calculation driver as arrived, Good, null:
+ /// indistinguishable from a genuine Good null, so the warm-up gate was satisfied by a value that
+ /// never arrived and the calc output stayed Good over a dead source.
+ ///
+ /// The dependency value, as the mux delivered it.
private void OnDependencyValueChanged(VirtualTagActor.DependencyValueChanged msg)
{
- // Quality is stripped at the mux boundary (value + timestamp only), so forward Good (0).
- _consumer.OnDependencyValue(msg.TagId, msg.Value, statusCode: 0u, msg.TimestampUtc);
+ _consumer.OnDependencyValue(msg.TagId, msg.Value, msg.StatusCode, msg.TimestampUtc);
}
/// The mux's batched delivery: one message carrying this adapter's whole matched slice of a
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/VirtualTags/DependencyConsumerMuxAdapterTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/VirtualTags/DependencyConsumerMuxAdapterTests.cs
new file mode 100644
index 00000000..33e4bb9a
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/VirtualTags/DependencyConsumerMuxAdapterTests.cs
@@ -0,0 +1,91 @@
+using Akka.Actor;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
+using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
+
+namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.VirtualTags;
+
+///
+/// Archreview 05/STAB-19 — the adapter that carries dependency values from the per-node
+/// dependency mux into an (today: the Calculation driver).
+///
+/// The defect these pin. The adapter forwarded a hard-coded statusCode: 0u behind
+/// a comment claiming "quality is stripped at the mux boundary". That was true once; since #478/#519
+/// carries the source driver's full status code,
+/// sub-code included. The consequence was that a comms-lost input reached the Calculation driver as
+/// arrived, Good, null — indistinguishable from a real Good reading of null, which is how a
+/// calc output kept publishing Good over a dead input.
+///
+public sealed class DependencyConsumerMuxAdapterTests : RuntimeActorTestBase
+{
+ private const string TagA = "Plant/Modbus/dev1/a";
+ private const string TagB = "Plant/Modbus/dev1/b";
+
+ /// Single delivery: the status code reaches the consumer verbatim, sub-code and all.
+ [Fact]
+ public void A_single_dependency_value_carries_its_status_code_to_the_consumer()
+ {
+ var (consumer, adapter) = Spawn();
+
+ adapter.Tell(new VirtualTagActor.DependencyValueChanged(
+ TagA, Value: null, DateTime.UtcNow, OpcUaStatus.BadNoCommunication));
+
+ AwaitCondition(() => consumer.Received.Count == 1, PresenceBudget);
+ consumer.Received[0].Ref.ShouldBe(TagA);
+ consumer.Received[0].StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
+ consumer.Received[0].StatusCode.ShouldNotBe(OpcUaStatus.Good,
+ "a Bad input forwarded as Good is the laundering 05/STAB-19 is about");
+ }
+
+ ///
+ /// Batched delivery goes through the same seam, so it must carry status too — the batch path is
+ /// what a real driver burst uses, and a fix applied only to the single-value path would be inert
+ /// in production.
+ ///
+ [Fact]
+ public void A_batched_delivery_carries_each_items_status_code()
+ {
+ var (consumer, adapter) = Spawn();
+ var ts = DateTime.UtcNow;
+
+ adapter.Tell(new VirtualTagActor.DependencyValueBatch(new[]
+ {
+ new VirtualTagActor.DependencyValueChanged(TagA, 1.0, ts, OpcUaStatus.Good),
+ new VirtualTagActor.DependencyValueChanged(TagB, null, ts, OpcUaStatus.BadNoCommunication),
+ }));
+
+ AwaitCondition(() => consumer.Received.Count == 2, PresenceBudget);
+ consumer.Received[0].StatusCode.ShouldBe(OpcUaStatus.Good);
+ consumer.Received[1].StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
+ }
+
+ private (RecordingConsumer Consumer, IActorRef Adapter) Spawn()
+ {
+ var consumer = new RecordingConsumer();
+ var mux = CreateTestProbe();
+ var adapter = Sys.ActorOf(DependencyConsumerMuxAdapter.Props(consumer, mux.Ref));
+ mux.ExpectMsg(PresenceBudget);
+ return (consumer, adapter);
+ }
+
+ private sealed class RecordingConsumer : IDependencyConsumer
+ {
+ private readonly List<(string Ref, object? Value, uint StatusCode)> _received = new();
+ private readonly object _gate = new();
+
+ public IReadOnlyList<(string Ref, object? Value, uint StatusCode)> Received
+ {
+ get { lock (_gate) return _received.ToArray(); }
+ }
+
+ public IReadOnlyCollection DependencyRefs { get; } = [TagA, TagB];
+
+ public void OnDependencyValue(string fullReference, object? value, uint statusCode, DateTime timestampUtc)
+ {
+ lock (_gate) _received.Add((fullReference, value, statusCode));
+ }
+ }
+}