fix: fence runtime removal races and retries

This commit is contained in:
2026-09-13 02:54:01 +09:00
parent 6c609808c9
commit d2cb50d081
6 changed files with 416 additions and 21 deletions
@@ -178,7 +178,9 @@ Deno.test("Runtime detail keeps trust controls owner-only and conflict-safe", as
"Workspace trust only; this does not delete the Runtime process, Workers, or Workdirs.",
"await revokeRuntimeTrustKey(",
"await removeRemoteRuntime(",
"operation_id: crypto.randomUUID()",
"new RuntimeRemovalAttempt()",
"runtimeRemovalAttempt.operationId()",
"operation_id: operationId",
"expected_binding_revision: trust.revision",
"The Backend removes Workspace trust and this Runtime registration as one guarded operation.",
"deleteRuntimeConfirmation.trim() !== data.runtimeId",
@@ -12,6 +12,7 @@ import {
previewRuntimePublicKeyFingerprint,
removeRemoteRuntime,
revokeRuntimeTrustKey,
RuntimeRemovalAttempt,
RuntimeTrustConflictError,
RuntimeTrustRouteFence,
updateRemoteRuntime,
@@ -314,6 +315,58 @@ Deno.test("Runtime metadata update never sends public key authority", async () =
assert(!("public_key" in body), "metadata update sent public_key");
});
Deno.test("Runtime removal attempt retains its id across response-loss retry", async () => {
const attempt = new RuntimeRemovalAttempt();
const operationIds: string[] = [];
let calls = 0;
const submit = async () => {
const operationId = attempt.operationId(() => "stable-removal-operation");
operationIds.push(operationId);
const operation = await removeRemoteRuntime(
"workspace-a",
"runtime-a",
{ operation_id: operationId, expected_binding_revision: 4 },
() => {
calls += 1;
if (calls === 1) return Promise.reject(new Error("response lost"));
return Promise.resolve(Response.json({
operation_id: operationId,
workspace_id: "workspace-a",
runtime_id: "runtime-a",
state: "succeeded",
binding_removed: true,
runtime_registration_removed: true,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:01Z",
completed_at: "2026-01-01T00:00:01Z",
}));
},
);
attempt.complete(operation.operation_id);
};
try {
await submit();
throw new Error("expected response-loss retry to fail");
} catch (error) {
assert(
error instanceof Error && error.message.includes("response lost"),
`unexpected response-loss error: ${String(error)}`,
);
}
await submit();
assert(
operationIds.length === 2 &&
operationIds.every((id) => id === "stable-removal-operation"),
`response-loss retry changed operation id: ${operationIds.join(",")}`,
);
assert(
attempt.operationId(() => "next-removal-operation") ===
"next-removal-operation",
"authoritative success did not clear the completed operation id",
);
});
Deno.test("Runtime removal uses the Workspace-scoped operation route", async () => {
let requestedUrl = "";
let requestedMethod = "";