diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DeploymentsRealtimeTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DeploymentsRealtimeTests.cs
new file mode 100644
index 00000000..65cd5e8b
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DeploymentsRealtimeTests.cs
@@ -0,0 +1,111 @@
+using Microsoft.Playwright;
+using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
+using Xunit;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment;
+
+///
+/// E2E coverage for the Deployment Status page's real-time, push-based update
+/// model (Tier 2). The page (/deployment/deployments) is NOT polled: in
+/// OnInitializedAsync it subscribes to
+/// IDeploymentStatusNotifier.StatusChanged — a process singleton the
+/// DeploymentManager raises on every deployment-record status write — and reloads
+/// the table on each notification, which Blazor Server pushes to the browser over
+/// its SignalR circuit. So with the page already open, a CLI instance deploy
+/// makes the instance's row surface with no manual Refresh: the row simply
+/// appearing IS the proof the push path works.
+///
+///
+/// Each fact mints a fresh ephemeral instance on the real site-a (via
+/// ) and deletes it in a
+/// finally. The minted uniqueName is unique, so its row can never
+/// pre-exist — the row's appearance is attributable solely to the deploy under test.
+///
+///
+///
+/// Why Fact B's negative is deterministic: the Pause button toggles
+/// _autoRefresh, and OnDeploymentStatusChanged early-returns on
+/// !_autoRefresh, so a paused page deterministically ignores the
+/// StatusChanged notification. The pause click round-trips over the SignalR
+/// circuit (and we wait for the button to flip to "Resume" before deploying), so
+/// _autoRefresh is committed false server-side BEFORE the deploy fires
+/// — the suppression is not a race. Refresh calls LoadDataAsync directly,
+/// bypassing the pause guard, so the row surfaces on the explicit reload.
+///
+///
+[Collection("Playwright")]
+public class DeploymentsRealtimeTests : IClassFixture
+{
+ private readonly PlaywrightFixture _pw;
+ private readonly DeploymentFixture _cluster;
+
+ public DeploymentsRealtimeTests(PlaywrightFixture pw, DeploymentFixture cluster)
+ {
+ _pw = pw;
+ _cluster = cluster;
+ }
+
+ [SkippableFact]
+ public async Task DeployingInstance_PushesRowWithoutManualRefresh()
+ {
+ Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);
+
+ var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
+ try
+ {
+ // Open the page FIRST so the StatusChanged subscription is live before we deploy.
+ var page = await _pw.NewAuthenticatedPageAsync();
+ await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/deployments");
+ await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+ await Assertions.Expect(page.Locator("h4:has-text('Deployment Status')")).ToBeVisibleAsync();
+
+ // Deploy over the CLI — this writes deployment records and raises StatusChanged,
+ // which the page reloads on (push). No Refresh click here: the row appearing IS
+ // the proof the SignalR push path works.
+ await CliRunner.DeployInstanceAsync(instanceId);
+
+ var row = page.Locator("table tbody tr", new() { HasText = uniqueName });
+ await Assertions.Expect(row).ToBeVisibleAsync(new() { Timeout = 25_000 });
+ }
+ finally
+ {
+ await CliRunner.DeleteInstanceAsync(instanceId);
+ }
+ }
+
+ [SkippableFact]
+ public async Task PausedUpdates_SuppressPush_RefreshRestoresRow()
+ {
+ Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);
+
+ var (instanceId, uniqueName) = await _cluster.CreateInstanceAsync();
+ try
+ {
+ var page = await _pw.NewAuthenticatedPageAsync();
+ await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/deployment/deployments");
+ await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+
+ // Pause — the click round-trips over the circuit, so _autoRefresh is committed
+ // false before the deploy below. The button flipping to "Resume" proves it.
+ await page.Locator("button[aria-label='Pause auto-refresh']").ClickAsync();
+ await Assertions.Expect(page.Locator("button[aria-label='Resume auto-refresh']"))
+ .ToBeVisibleAsync(new() { Timeout = 5_000 });
+
+ await CliRunner.DeployInstanceAsync(instanceId);
+
+ // Paused: StatusChanged is ignored, so the row is NOT auto-added. Settle briefly to
+ // give any (erroneous) push time to manifest, then assert absence.
+ var row = page.Locator("table tbody tr", new() { HasText = uniqueName });
+ await page.WaitForTimeoutAsync(2_000);
+ await Assertions.Expect(row).ToHaveCountAsync(0);
+
+ // Refresh bypasses the pause (LoadDataAsync) -> the row surfaces.
+ await page.Locator("button[aria-label='Refresh deployments']").ClickAsync();
+ await Assertions.Expect(row).ToBeVisibleAsync(new() { Timeout = 15_000 });
+ }
+ finally
+ {
+ await CliRunner.DeleteInstanceAsync(instanceId);
+ }
+ }
+}