fix(gateway): skip worker bootstrap during teardown replay (#128745)

Preserve replay-safe lease adoption, then route destroy-requested node and SSH environments directly through canonical teardown without bundle installation or credential minting.\n\nRefs #128744
This commit is contained in:
Peter Steinberger
2026-08-24 06:58:25 -07:00
committed by GitHub
parent 2b8fe12f0b
commit 84fd10db2a
2 changed files with 116 additions and 14 deletions
@@ -283,34 +283,29 @@ export function createWorkerProviderLifecycle(options: WorkerProviderLifecycleOp
leaseId: lease.leaseId,
sharedHost: lease.sharedHost === true,
desktop: lease.desktop ?? null,
...(lease.node
? { nodeDeviceId: lease.node.deviceId, sshEndpoint: null }
: { nodeDeviceId: null, sshEndpoint: lease.ssh }),
};
const leaseModeError = resolveWorkerLeaseModeError(provider, lease);
if (leaseModeError) {
const leasePatch = {
...patch,
...(lease.node
? { nodeDeviceId: lease.node.deviceId, sshEndpoint: null }
: { nodeDeviceId: null, sshEndpoint: lease.ssh }),
};
return await failBootstrap(
record,
lease.leaseId,
provider,
leaseModeError,
"invalid_profile",
leasePatch,
patch,
);
}
if (record.destroyRequestedAtMs !== null) {
// Replay must recover its exact lease, but teardown intent forbids transport bootstrap.
return move(record, "draining", patch);
}
if (lease.node) {
return await finishNodeProvisioning(record, lease, provider, patch);
}
const bootstrapping = move(record, "bootstrapping", {
...patch,
sshEndpoint: lease.ssh,
});
if (record.destroyRequestedAtMs !== null) {
return bootstrapping;
}
const bootstrapping = move(record, "bootstrapping", patch);
let installation = preparedInstallation;
if (!installation) {
try {
@@ -76,6 +76,113 @@ describe("node worker provider provisioning", () => {
);
});
it("destroys a replayed node lease without installing or admitting its worker", async () => {
const leaseId = "cloud-lease-destroy-replay";
const deviceId = "cloud-device-destroy-replay";
const operationIds: string[] = [];
const ensureNodeWorkerBundle = vi.fn(async () => structuredClone(support.BOOTSTRAP_RECEIPT));
const generateWorkerCredential = vi.fn(() => support.CREDENTIAL);
const retireNodeEnrollment = vi.fn(async () => {});
const destroy = vi.fn(async () => {});
const transitions = vi.spyOn(support.testState.store, "transition");
const prepareNodeEnrollment = vi.fn(async (record) => {
const enrolled = support.testState.store.ensureNodeEnrollment(record.environmentId);
if (!enrolled.nodeSetupId) {
throw new Error("expected persisted cloud enrollment ownership");
}
return {
mode: "connect" as const,
setupCode: "setup-code",
setupId: enrolled.nodeSetupId,
openclawVersion: "2026.8.1",
packageSpecs: ["openclaw@2026.8.1"],
displayName: "Cloud worker destroy replay",
waitForDeviceId: async () => deviceId,
};
});
const workerService = support.createService(
support.createProvider({
supportedExecutionModes: ["worker-turn"],
provisionBeforeInstallation: true,
requiresNodeEnrollment: true,
provision: async (_profile, operationId, options) => {
operationIds.push(operationId);
if (operationIds.length === 1) {
await options?.beginNodeEnrollment?.();
throw new Error("provider response was lost after node allocation");
}
return {
leaseId,
node: { deviceId },
sharedHost: false,
desktop: support.DESKTOP,
};
},
destroy,
}),
{
prepareNodeEnrollment,
retireNodeEnrollment,
ensureNodeWorkerBundle,
generateWorkerCredential,
},
);
await expect(
workerService.create("development", "request-node-destroy-replay"),
).rejects.toMatchObject({ code: "provider_failure" });
const provisioning = support.testState.store.list()[0]!;
expect(provisioning).toMatchObject({
state: "provisioning",
leaseId: null,
nodeSetupId: expect.any(String),
});
await expect(workerService.destroy(provisioning.environmentId)).resolves.toMatchObject({
state: "destroyed",
leaseId,
nodeDeviceId: deviceId,
sharedHost: false,
desktop: support.DESKTOP,
});
expect(operationIds).toEqual([
provisioning.provisionOperationId,
provisioning.provisionOperationId,
]);
expect(ensureNodeWorkerBundle).not.toHaveBeenCalled();
expect(support.testState.prepareInstallation).not.toHaveBeenCalled();
expect(support.testState.bootstrapWorker).not.toHaveBeenCalled();
expect(generateWorkerCredential).not.toHaveBeenCalled();
expect(support.testState.store.getCredential(provisioning.environmentId)).toBeUndefined();
expect(transitions).not.toHaveBeenCalledWith(expect.objectContaining({ to: "ready" }));
expect(destroy).toHaveBeenCalledExactlyOnceWith({ leaseId, profile: { region: "test" } });
expect(retireNodeEnrollment).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
state: "destroying",
leaseId,
nodeSetupId: provisioning.nodeSetupId,
nodeDeviceId: deviceId,
sharedHost: false,
desktop: support.DESKTOP,
bootstrapReceipt: null,
ownerEpoch: 0,
}),
);
expect(support.testState.store.get(provisioning.environmentId)).toMatchObject({
state: "destroyed",
bootstrapReceipt: null,
ownerEpoch: 0,
});
expect(
workerService.takeMintedCredential({
environmentId: provisioning.environmentId,
ownerEpoch: 0,
sessionId: null,
}),
).toBeUndefined();
});
it("keeps paired-device roles when a node lease has no cloud enrollment owner", async () => {
const retireNodeEnrollment = vi.fn(async () => {});
const workerService = support.createService(