diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/ServiceLevelEndToEndTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/ServiceLevelEndToEndTests.cs
new file mode 100644
index 0000000..cafa479
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/ServiceLevelEndToEndTests.cs
@@ -0,0 +1,147 @@
+using Akka.Actor;
+using Microsoft.Extensions.Logging.Abstractions;
+using Opc.Ua.Server;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Commons.Messages.Redundancy;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
+using ZB.MOM.WW.OtOpcUa.Commons.Types;
+using ZB.MOM.WW.OtOpcUa.OpcUaServer;
+using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
+using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
+
+namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.OpcUa;
+
+///
+/// Task 60 / #81 — verifies the full path from cluster redundancy state to OPC UA
+/// Server.ServiceLevel visible on the wire. Boots a real ,
+/// wires into a
+/// (the production binding pattern), spawns against the
+/// deferred publisher, and sends a snapshot. Asserts
+/// ServerObject.ServiceLevel.Value reflects the role-derived byte.
+///
+public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
+{
+ private static CancellationToken Ct => CancellationToken.None;
+
+ [Fact]
+ public async Task Primary_leader_drives_Server_ServiceLevel_to_240()
+ {
+ var pkiRoot = AllocatePkiRoot();
+ try
+ {
+ var server = new StandardServer();
+ await using var host = new OpcUaApplicationHost(
+ BuildOptions("PrimaryLeader", pkiRoot),
+ NullLogger.Instance);
+ await host.StartAsync(server, Ct);
+
+ var deferred = new DeferredServiceLevelPublisher();
+ deferred.SetInner(new SdkServiceLevelPublisher(
+ server.CurrentInstance,
+ NullLogger.Instance));
+
+ var localNode = NodeId.Parse("node-A");
+ var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
+ serviceLevel: deferred,
+ subscribeRedundancyTopic: false,
+ localNode: localNode));
+
+ actor.Tell(new RedundancyStateChanged(
+ Nodes: new[]
+ {
+ new NodeRedundancyState(localNode, RedundancyRole.Primary, IsClusterLeader: true, IsRoleLeaderForDriver: true, AsOfUtc: DateTime.UtcNow),
+ },
+ CorrelationId: CorrelationId.NewId()));
+
+ AwaitAssertion(() =>
+ server.CurrentInstance.ServerObject.ServiceLevel.Value.ShouldBe((byte)240));
+ }
+ finally
+ {
+ DeletePkiRoot(pkiRoot);
+ }
+ }
+
+ [Fact]
+ public async Task Secondary_drives_Server_ServiceLevel_to_100()
+ {
+ var pkiRoot = AllocatePkiRoot();
+ try
+ {
+ var server = new StandardServer();
+ await using var host = new OpcUaApplicationHost(
+ BuildOptions("Secondary", pkiRoot),
+ NullLogger.Instance);
+ await host.StartAsync(server, Ct);
+
+ var deferred = new DeferredServiceLevelPublisher();
+ deferred.SetInner(new SdkServiceLevelPublisher(
+ server.CurrentInstance,
+ NullLogger.Instance));
+
+ var localNode = NodeId.Parse("node-B");
+ var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
+ serviceLevel: deferred,
+ subscribeRedundancyTopic: false,
+ localNode: localNode));
+
+ actor.Tell(new RedundancyStateChanged(
+ Nodes: new[]
+ {
+ new NodeRedundancyState(localNode, RedundancyRole.Secondary, IsClusterLeader: false, IsRoleLeaderForDriver: false, AsOfUtc: DateTime.UtcNow),
+ },
+ CorrelationId: CorrelationId.NewId()));
+
+ AwaitAssertion(() =>
+ server.CurrentInstance.ServerObject.ServiceLevel.Value.ShouldBe((byte)100));
+ }
+ finally
+ {
+ DeletePkiRoot(pkiRoot);
+ }
+ }
+
+ private static OpcUaApplicationHostOptions BuildOptions(string name, string pkiRoot) =>
+ new()
+ {
+ ApplicationName = $"OtOpcUa.E2E.{name}",
+ ApplicationUri = $"urn:OtOpcUa.E2E.{name}:{Guid.NewGuid():N}",
+ OpcUaPort = AllocateFreePort(),
+ PublicHostname = "localhost",
+ PkiStoreRoot = pkiRoot,
+ };
+
+ private static string AllocatePkiRoot() =>
+ Path.Combine(Path.GetTempPath(), $"otopcua-pki-{Guid.NewGuid():N}");
+
+ private static void DeletePkiRoot(string root)
+ {
+ if (Directory.Exists(root))
+ {
+ try { Directory.Delete(root, recursive: true); }
+ catch { /* best-effort */ }
+ }
+ }
+
+ private static int AllocateFreePort()
+ {
+ using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
+ listener.Start();
+ var port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
+ listener.Stop();
+ return port;
+ }
+
+ private void AwaitAssertion(Action assertion)
+ {
+ var deadline = DateTime.UtcNow.AddSeconds(3);
+ Exception? last = null;
+ while (DateTime.UtcNow < deadline)
+ {
+ try { assertion(); return; }
+ catch (Exception ex) { last = ex; Thread.Sleep(30); }
+ }
+ if (last is not null) throw last;
+ }
+}