diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/TopologyDisableConfirmLabelTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/TopologyDisableConfirmLabelTests.cs
new file mode 100644
index 00000000..6590cf9f
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/TopologyDisableConfirmLabelTests.cs
@@ -0,0 +1,179 @@
+using System.Security.Claims;
+using Bunit;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using NSubstitute;
+using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
+using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
+using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
+using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
+using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
+using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
+using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
+using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
+using ZB.MOM.WW.ScadaBridge.Communication;
+using ZB.MOM.WW.ScadaBridge.DeploymentManager;
+using ZB.MOM.WW.ScadaBridge.Security;
+using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services;
+using TopologyPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment.Topology;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
+
+///
+/// Pins the verbs on the Topology instance-action confirmations (deferred-work
+/// register row 28): Disable must read "Disable", not the red button's
+/// historical "Delete" default, while a genuine Delete must still read "Delete".
+/// Like FailoverConfirmLabelTests these run the real
+/// behind a real DialogHost, so they pin what the user reads rather than
+/// the argument a substitute received.
+///
+public class TopologyDisableConfirmLabelTests : BunitContext
+{
+ private readonly ITemplateEngineRepository _repo = Substitute.For();
+ private readonly ISiteRepository _siteRepo = Substitute.For();
+ private readonly IDeploymentManagerRepository _deployRepo = Substitute.For();
+ private readonly IFlatteningPipeline _pipeline = Substitute.For();
+ private readonly IAuditService _audit = Substitute.For();
+ private readonly DialogService _dialog = new();
+
+ public TopologyDisableConfirmLabelTests()
+ {
+ Services.AddSingleton(_repo);
+ Services.AddSingleton(_siteRepo);
+ Services.AddSingleton(_deployRepo);
+ Services.AddSingleton(_pipeline);
+ Services.AddSingleton(_audit);
+
+ // DeploymentService has non-mockable concrete deps; instantiate them directly
+ // (the same DI block TopologyPageTests uses to render this page).
+ Services.AddSingleton(new CommunicationService(
+ Options.Create(new CommunicationOptions()),
+ NullLogger.Instance));
+ Services.AddSingleton(new OperationLockManager());
+ Services.AddSingleton(Options.Create(new DeploymentManagerOptions
+ {
+ OperationLockTimeout = TimeSpan.FromSeconds(5)
+ }));
+ Services.AddSingleton(Options.Create(new CommunicationOptions()));
+ Services.AddSingleton>(NullLogger.Instance);
+ Services.AddScoped();
+ Services.AddScoped();
+ Services.AddSingleton(
+ new DeploymentStatusNotifier(NullLogger.Instance));
+ Services.AddSingleton(Substitute.For());
+ Services.AddScoped();
+ Services.AddScoped();
+ Services.AddScoped();
+ Services.AddScoped();
+
+ // The concrete service registered as the interface, so the page and the
+ // host share one instance (DialogHost injects IDialogService and casts).
+ Services.AddSingleton(_dialog);
+
+ var claims = new[]
+ {
+ new Claim(JwtTokenService.UsernameClaimType, "tester"),
+ new Claim(JwtTokenService.RoleClaimType, Roles.Deployer)
+ };
+ Services.AddSingleton(
+ new TestAuthStateProvider(new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"))));
+ Services.AddAuthorizationCore();
+
+ JSInterop.Mode = JSRuntimeMode.Loose;
+ }
+
+ /// One site with one enabled instance, so both Disable and Delete are offered.
+ private void SeedOneEnabledInstance()
+ {
+ _siteRepo.GetAllSitesAsync(Arg.Any())
+ .Returns(Task.FromResult>(
+ new List { new("Plant-A", "plant-a") { Id = 1 } }));
+ _repo.GetAllTemplatesAsync(Arg.Any())
+ .Returns(Task.FromResult>(new List()));
+ _repo.GetAllInstancesAsync(Arg.Any())
+ .Returns(Task.FromResult>(new List
+ {
+ new("Pump-001") { Id = 100, SiteId = 1, State = InstanceState.Enabled }
+ }));
+ _repo.GetAreasBySiteIdAsync(Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult>(new List()));
+ }
+
+ /// Renders Topology next to a live DialogHost, as MainLayout does.
+ private IRenderedComponent RenderPageWithHost() => Render();
+
+ /// Wrapper so the page and the dialog host share one render tree.
+ public sealed class TestHostWrapper : ComponentBase
+ {
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenComponent(0);
+ builder.CloseComponent();
+ builder.OpenComponent(1);
+ builder.CloseComponent();
+ }
+ }
+
+ ///
+ /// Expands to the instance row, opens its context menu and clicks the named item,
+ /// then returns the confirmation modal's footer buttons.
+ ///
+ ///
+ /// The menu click is deliberately not awaited: with a real DialogService the page's
+ /// handler parks on await ConfirmAsync(...) until the dialog is answered.
+ ///
+ private (IRenderedComponent cut, IReadOnlyList footer)
+ OpenInstanceConfirm(string menuItem)
+ {
+ SeedOneEnabledInstance();
+ var cut = RenderPageWithHost();
+
+ cut.FindAll(".tv-row").First(r => r.QuerySelector(".tv-label")?.TextContent == "Plant-A")
+ .QuerySelector(".tv-toggle")!.Click();
+
+ var row = cut.FindAll(".tv-row").First(r => r.QuerySelector(".tv-label")?.TextContent == "Pump-001");
+ row.ContextMenu();
+
+ _ = cut.Find(".dropdown-menu.show")
+ .QuerySelectorAll("button.dropdown-item")
+ .First(b => b.TextContent.Trim() == menuItem)
+ .ClickAsync(new());
+
+ cut.WaitForState(() => cut.FindAll("div.modal-footer button").Count > 0);
+ return (cut, cut.FindAll("div.modal-footer button").ToList());
+ }
+
+ [Fact]
+ public void Disable_confirm_button_says_disable_not_delete()
+ {
+ var (_, footer) = OpenInstanceConfirm("Disable");
+ var confirm = footer.Last();
+
+ Assert.Equal("Disable", confirm.TextContent.Trim());
+ Assert.Contains("btn-danger", confirm.ClassList);
+ Assert.Contains("Cancel", footer.Select(b => b.TextContent.Trim()));
+ }
+
+ [Fact]
+ public void Disable_confirm_body_names_the_instance()
+ {
+ var (cut, _) = OpenInstanceConfirm("Disable");
+ Assert.Contains("Disable instance 'Pump-001'?", cut.Find(".modal-body").TextContent);
+ }
+
+ [Fact]
+ public void Delete_confirm_button_still_says_delete()
+ {
+ // The negative twin: relabelling Disable must not have changed the genuine
+ // delete path, which relies on the danger-confirm default.
+ var confirm = OpenInstanceConfirm("Delete").footer.Last();
+
+ Assert.Equal("Delete", confirm.TextContent.Trim());
+ Assert.Contains("btn-danger", confirm.ClassList);
+ }
+}