mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
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:
committed by
GitHub
parent
2b8fe12f0b
commit
84fd10db2a
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user