diff --git a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs index 3e405507..321d6fe6 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs @@ -563,6 +563,37 @@ public class DeploymentService instance.UniqueName, _options.OperationLockTimeout, cancellationToken); var commandId = Guid.NewGuid().ToString("N"); + + // A NotDeployed instance has no live Instance Actor at any site, so Delete is + // a pure central-side record cleanup -- there is nothing to tear down at the + // site and no reason to require the site be reachable. Transport-imported + // instances always land NotDeployed (often against an uncommissioned or + // unreachable site), so a site round-trip here would make them undeletable + // until their site comes online. Skip the site entirely and remove the record. + if (instance.State == InstanceState.NotDeployed) + { + try + { + await _repository.DeleteInstanceAsync(instanceId, cancellationToken); + await _repository.SaveChangesAsync(cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Failed to delete NotDeployed instance {Instance} centrally", instance.UniqueName); + return Result.Failure( + $"Delete failed: the central record for '{instance.UniqueName}' could not be removed: {ex.Message}."); + } + + await TryLogAuditAsync(user, "Delete", "Instance", instanceId.ToString(), + instance.UniqueName, + new { CommandId = commandId, Success = true, SiteRoundTripSkipped = true }, + cancellationToken); + + return Result.Success(new InstanceLifecycleResponse( + commandId, instance.UniqueName, Success: true, ErrorMessage: null, DateTimeOffset.UtcNow)); + } + var siteId = await ResolveSiteIdentifierAsync(instance.SiteId, cancellationToken); var command = new DeleteInstanceCommand(commandId, instance.UniqueName, DateTimeOffset.UtcNow); diff --git a/tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/DeploymentServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/DeploymentServiceTests.cs index ddcb4801..7cc6467f 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/DeploymentServiceTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/DeploymentServiceTests.cs @@ -454,6 +454,25 @@ public class DeploymentServiceTests : TestKit Assert.Contains("not found", result.Error); } + [Fact] + public async Task DeleteInstanceAsync_NotDeployed_RemovesRecordWithoutSiteRoundTrip() + { + // #05-T18: a NotDeployed instance has no live actor at any site (Transport- + // imported instances always land NotDeployed, often against an uncommissioned + // or unreachable site). Delete must succeed as a central-side record cleanup + // WITHOUT contacting the site. The default _service's CommunicationService has + // no actor wired, so any attempted site round-trip throws uncaught -- a Success + // result therefore proves the site was never contacted (Times.Never, enforced + // structurally). + var instance = new Instance("ImportedInst") { Id = 70, SiteId = 1, State = InstanceState.NotDeployed }; + _repo.GetInstanceByIdAsync(70, Arg.Any()).Returns(instance); + + var result = await _service.DeleteInstanceAsync(70, "admin"); + + Assert.True(result.IsSuccess); + await _repo.Received(1).DeleteInstanceAsync(70, Arg.Any()); + } + // ── DeploymentManager-004: site-success but central-delete-failure must not escape uncaught ── [Fact]