From 83ec986f599ff3bc1846d8198c727a8dd992f5fe Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 11 Oct 2026 00:43:12 -0500 Subject: [PATCH] Allow Boat disconnect while cleanup remains pending --- ui/src/pages/CompanyEnvironments.test.tsx | 42 +++++++++++++++++++++++ ui/src/pages/CompanyEnvironments.tsx | 8 +++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/ui/src/pages/CompanyEnvironments.test.tsx b/ui/src/pages/CompanyEnvironments.test.tsx index 1b4c39d3f28..bfa09b5b2b9 100644 --- a/ui/src/pages/CompanyEnvironments.test.tsx +++ b/ui/src/pages/CompanyEnvironments.test.tsx @@ -1871,6 +1871,48 @@ describe("CompanyEnvironments — test provider button", () => { expect(mockEnvironmentsApi.remove).not.toHaveBeenCalled(); }); + it.each([ + { driver: "computer", instanceDefault: false, allowed: true }, + { driver: "computer", instanceDefault: true, allowed: false }, + { driver: "sandbox", instanceDefault: false, allowed: false }, + ])("matches pending-cleanup removal policy for $driver (instance default=$instanceDefault)", async ({ driver, instanceDefault, allowed }) => { + mockEnvironmentsApi.list.mockResolvedValue([{ + id: "env-1", name: "Cleanup environment", driver, status: "active", + config: driver === "computer" ? { provider: "boat", sandboxId: "bx_existing" } : { provider: "daytona" }, + }]); + mockEnvironmentsApi.deleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({ + canDelete: false, + deleteBlockedReasons: instanceDefault ? ["instance_default", "pending_cleanup"] : ["pending_cleanup"], + staticReferences: { isInstanceDefault: instanceDefault }, + pendingCleanupLeaseCount: 1, + })); + root = createRoot(container); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + await act(async () => { + root!.render(renderCompanyEnvironments(queryClient, `${ENVIRONMENTS_PATH}/env-1/edit`)); + }); + await waitForAssertion(() => { + expect(document.body.querySelector("[data-testid='environment-delete-button']")).not.toBeNull(); + }); + await act(async () => click(document.body.querySelector("[data-testid='environment-delete-button']"))); + await waitForAssertion(() => { + expect(mockEnvironmentsApi.deleteBlastRadius).toHaveBeenCalledWith("env-1"); + const confirm = document.body.querySelector("[data-testid='environment-delete-confirm']"); + expect(confirm).not.toBeNull(); + expect(confirm!.disabled).toBe(!allowed); + }); + if (allowed) { + expect(document.body.textContent).toContain("Disconnect computer"); + await act(async () => click(document.body.querySelector("[data-testid='environment-delete-confirm']"))); + await waitForAssertion(() => expect(mockEnvironmentsApi.remove).toHaveBeenCalledExactlyOnceWith("env-1")); + } else { + expect(mockEnvironmentsApi.remove).not.toHaveBeenCalled(); + expect(document.body.textContent).toContain(instanceDefault + ? "Choose another default environment before disconnecting this computer." + : "Cannot delete this environment while a sandbox cleanup is pending."); + } + }); + it("links the workspaces holding blocking sandbox leases in the delete dialog", async () => { mockEnvironmentsApi.deleteBlastRadius.mockResolvedValue( createDeleteBlastRadius({ diff --git a/ui/src/pages/CompanyEnvironments.tsx b/ui/src/pages/CompanyEnvironments.tsx index 38fe6b5a20b..cca6dd8e3ac 100644 --- a/ui/src/pages/CompanyEnvironments.tsx +++ b/ui/src/pages/CompanyEnvironments.tsx @@ -103,19 +103,21 @@ function environmentEditPath(environmentId: string) { // the server enforces these gates with a 409; this copy lets the modal explain // the block before the user hits it. function environmentDeleteBlockMessage(impact: EnvironmentDeleteBlastRadius, isComputer = false): string | null { + if (isComputer) { + return impact.staticReferences.isInstanceDefault + ? "Choose another default environment before disconnecting this computer." + : null; + } if (impact.staticReferences.isManagedLocal) { return "Cannot delete the managed local environment."; } if (impact.staticReferences.isInstanceDefault) { - if (isComputer) return "Choose another default environment before disconnecting this computer."; return "Cannot delete the current instance default environment. Set a new default environment before deleting this one."; } if (impact.pendingCleanupLeaseCount > 0) { - if (isComputer) return "Computer cleanup is pending. Wait for it to finish, then retry disconnecting. Its files will remain on Boat."; return "Cannot delete this environment while a sandbox cleanup is pending. Wait for the cleanup sweep to destroy the orphan sandbox, then retry."; } if (impact.reusableSandboxLeaseCount > 0) { - if (isComputer) return "This computer still has saved runner connections. Disconnecting stops its runners and retains its files on Boat."; return "Cannot delete this environment while it has a reusable sandbox lease. Remove the associated execution workspace or issue so Paperclip can destroy the sandbox, then retry."; } return null;