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);
|
||||
|
||||
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 command = new DeleteInstanceCommand(commandId, instance.UniqueName, DateTimeOffset.UtcNow);
|
||||
|
||||
|
||||
@@ -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<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 ──
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user