fix(deployment): delete NotDeployed instances centrally without a site round-trip
A NotDeployed instance has no live Instance Actor at any site, so Delete is a pure central-side record cleanup. Transport-imported instances always land NotDeployed (often against an uncommissioned/unreachable site); a site round-trip made them undeletable until the site came online. Short-circuit the site call for NotDeployed. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -563,6 +563,37 @@ public class DeploymentService
|
|||||||
instance.UniqueName, _options.OperationLockTimeout, cancellationToken);
|
instance.UniqueName, _options.OperationLockTimeout, cancellationToken);
|
||||||
|
|
||||||
var commandId = Guid.NewGuid().ToString("N");
|
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<InstanceLifecycleResponse>.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<InstanceLifecycleResponse>.Success(new InstanceLifecycleResponse(
|
||||||
|
commandId, instance.UniqueName, Success: true, ErrorMessage: null, DateTimeOffset.UtcNow));
|
||||||
|
}
|
||||||
|
|
||||||
var siteId = await ResolveSiteIdentifierAsync(instance.SiteId, cancellationToken);
|
var siteId = await ResolveSiteIdentifierAsync(instance.SiteId, cancellationToken);
|
||||||
var command = new DeleteInstanceCommand(commandId, instance.UniqueName, DateTimeOffset.UtcNow);
|
var command = new DeleteInstanceCommand(commandId, instance.UniqueName, DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
|
|||||||
@@ -454,6 +454,25 @@ public class DeploymentServiceTests : TestKit
|
|||||||
Assert.Contains("not found", result.Error);
|
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<CancellationToken>()).Returns(instance);
|
||||||
|
|
||||||
|
var result = await _service.DeleteInstanceAsync(70, "admin");
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
await _repo.Received(1).DeleteInstanceAsync(70, Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
// ── DeploymentManager-004: site-success but central-delete-failure must not escape uncaught ──
|
// ── DeploymentManager-004: site-success but central-delete-failure must not escape uncaught ──
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user