diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/DriverHealthReportingNodeWireTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/DriverHealthReportingNodeWireTests.cs
new file mode 100644
index 000000000..f0d096ce8
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/DriverHealthReportingNodeWireTests.cs
@@ -0,0 +1,126 @@
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;
+using ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry;
+using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Telemetry.Pre847;
+
+namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Telemetry;
+
+///
+/// Mixed-version wire tests for DriverHealth.reporting_node_id (proto field 15, added by lmxopcua#847;
+/// tests lmxopcua#851). A rolling upgrade runs old and new nodes against old and new centrals, so the field
+/// must cross the wire in all three directions: new→new round-trips, old→new parses as ABSENT (not an empty
+/// node id), and new→old is ignored as an unknown field without disturbing the rest of the message. "Old" is
+/// the frozen pre-#847 schema in Protos/telemetry_pre847.proto — a real generated parser/serializer, not
+/// a hand-built byte string.
+///
+[Trait("Category", "Unit")]
+public sealed class DriverHealthReportingNodeWireTests
+{
+ private static readonly DateTime PublishedUtc = new(2026, 11, 11, 12, 0, 0, DateTimeKind.Utc);
+ private static readonly DateTime LastReadUtc = new(2026, 11, 11, 11, 59, 30, DateTimeKind.Utc);
+
+ private static DriverHealth NewHealth(string? reportingNodeId)
+ {
+ var msg = new DriverHealth
+ {
+ ClusterId = "MAIN",
+ DriverInstanceId = "drv-1",
+ State = "Faulted",
+ LastSuccessfulReadUtc = Timestamp.FromDateTime(LastReadUtc),
+ LastError = "timeout",
+ ErrorCount5Min = 4,
+ PublishedUtc = Timestamp.FromDateTime(PublishedUtc),
+ HasDiagnostics = true,
+ };
+ msg.Diagnostics["pdu"] = "480";
+ if (reportingNodeId is not null) msg.ReportingNodeId = reportingNodeId;
+ return msg;
+ }
+
+ [Fact]
+ public void New_sender_to_new_receiver_round_trips_the_reporting_node_id()
+ {
+ var bytes = NewHealth("central-1").ToByteArray();
+
+ var parsed = DriverHealth.Parser.ParseFrom(bytes);
+ parsed.HasReportingNodeId.ShouldBeTrue();
+
+ var e = TelemetryProtoMapCentral.ToHealth(parsed);
+ e.ReportingNodeId.ShouldBe("central-1");
+ e.DriverInstanceId.ShouldBe("drv-1");
+ e.State.ShouldBe("Faulted");
+ e.PublishedUtc.ShouldBe(PublishedUtc);
+ }
+
+ [Fact]
+ public void New_sender_without_a_node_id_omits_field_15_and_maps_to_null()
+ {
+ // A harness / node with no role info leaves the field unset: it must not appear on the wire at all, so an
+ // explicit "" would not be confused with "absent" either way.
+ var msg = NewHealth(reportingNodeId: null);
+ msg.HasReportingNodeId.ShouldBeFalse();
+
+ var e = TelemetryProtoMapCentral.ToHealth(DriverHealth.Parser.ParseFrom(msg.ToByteArray()));
+ e.ReportingNodeId.ShouldBeNull();
+ }
+
+ [Fact]
+ public void Old_sender_to_new_receiver_parses_as_absent_node_id_and_keeps_every_other_field()
+ {
+ var old = new DriverHealthPre847
+ {
+ ClusterId = "MAIN",
+ DriverInstanceId = "drv-1",
+ State = "Healthy",
+ LastSuccessfulReadUtc = Timestamp.FromDateTime(LastReadUtc),
+ ErrorCount5Min = 0,
+ PublishedUtc = Timestamp.FromDateTime(PublishedUtc),
+ RediscoveryReason = "schema changed",
+ HasDiagnostics = true,
+ };
+ old.Diagnostics["pdu"] = "240";
+
+ var parsed = DriverHealth.Parser.ParseFrom(old.ToByteArray());
+
+ // Explicit presence: absent, NOT the proto3 default "" — central keys the snapshot under the
+ // driver-only (unlabelled) node and the AdminUI renders it as a pre-#847 row.
+ parsed.HasReportingNodeId.ShouldBeFalse();
+ var e = TelemetryProtoMapCentral.ToHealth(parsed);
+ e.ReportingNodeId.ShouldBeNull();
+ e.ClusterId.ShouldBe("MAIN");
+ e.DriverInstanceId.ShouldBe("drv-1");
+ e.State.ShouldBe("Healthy");
+ e.LastSuccessfulReadUtc.ShouldBe(LastReadUtc);
+ e.LastError.ShouldBeNull();
+ e.PublishedUtc.ShouldBe(PublishedUtc);
+ e.RediscoveryReason.ShouldBe("schema changed");
+ e.Diagnostics.ShouldNotBeNull();
+ e.Diagnostics!["pdu"].ShouldBe("240");
+ }
+
+ [Fact]
+ public void New_sender_to_old_receiver_ignores_field_15_and_keeps_every_other_field()
+ {
+ var bytes = NewHealth("central-2").ToByteArray();
+
+ // An old central's parser: field 15 is unknown to it. It must neither throw nor shift any known field.
+ var old = DriverHealthPre847.Parser.ParseFrom(bytes);
+
+ old.ClusterId.ShouldBe("MAIN");
+ old.DriverInstanceId.ShouldBe("drv-1");
+ old.State.ShouldBe("Faulted");
+ old.LastSuccessfulReadUtc.ToDateTime().ShouldBe(LastReadUtc);
+ old.LastError.ShouldBe("timeout");
+ old.ErrorCount5Min.ShouldBe(4);
+ old.PublishedUtc.ToDateTime().ShouldBe(PublishedUtc);
+ old.HasDiagnostics.ShouldBeTrue();
+ old.Diagnostics["pdu"].ShouldBe("480");
+
+ // The old parser keeps field 15 as an unknown field rather than corrupting it: re-serialised by an old
+ // process, a new reader still recovers the node id.
+ DriverHealth.Parser.ParseFrom(old.ToByteArray()).ReportingNodeId.ShouldBe("central-2");
+ }
+}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/Protos/telemetry_pre847.proto b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/Protos/telemetry_pre847.proto
new file mode 100644
index 000000000..87bda4139
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/Telemetry/Protos/telemetry_pre847.proto
@@ -0,0 +1,45 @@
+syntax = "proto3";
+
+// TEST-ONLY frozen copy of telemetry.v1.DriverHealth as it stood BEFORE lmxopcua#847 added
+// `optional string reporting_node_id = 15`. It stands in for an old node / an old central in the mixed-version
+// wire tests (lmxopcua#851): the same field numbers and wire types as the real message, minus field 15. Never edit
+// it to track telemetry.proto — its whole point is to stay old.
+package telemetry.tests.pre847;
+
+option csharp_namespace = "ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Telemetry.Pre847";
+
+import "google/protobuf/timestamp.proto";
+
+message DriverHealthPre847 {
+ string cluster_id = 1;
+ string driver_instance_id = 2;
+ string state = 3;
+ google.protobuf.Timestamp last_successful_read_utc = 4;
+ optional string last_error = 5;
+ int32 error_count_5min = 6;
+ google.protobuf.Timestamp published_utc = 7;
+ google.protobuf.Timestamp rediscovery_needed_utc = 8;
+ optional string rediscovery_reason = 9;
+ bool has_host_statuses = 10;
+ repeated HostConnectivityPre847 host_statuses = 11;
+ SkippedTagsPre847 skipped_tags = 12;
+ bool has_diagnostics = 13;
+ map diagnostics = 14;
+}
+
+message SkippedTagsPre847 {
+ int32 authored_count = 1;
+ int32 skipped_count = 2;
+ repeated SkippedTagPre847 sample = 3;
+}
+
+message SkippedTagPre847 {
+ string raw_path = 1;
+ string reason = 2;
+}
+
+message HostConnectivityPre847 {
+ string host_name = 1;
+ string state = 2;
+ google.protobuf.Timestamp last_changed_utc = 3;
+}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.csproj b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.csproj
index 9b1252e6b..9608a8300 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.csproj
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.csproj
@@ -11,6 +11,12 @@
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
all
@@ -18,6 +24,10 @@
+
+
+
+