From e450faf402dd1f4bdb3f335626750fb83a09f176 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 10 Oct 2026 09:50:07 -0500 Subject: [PATCH] fix: preserve pinned Boat credentials on environment edits Co-Authored-By: Paperclip --- ui/src/pages/CompanyEnvironments.test.tsx | 31 +++++++++++++++++++++++ ui/src/pages/CompanyEnvironments.tsx | 17 ++++++++----- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/ui/src/pages/CompanyEnvironments.test.tsx b/ui/src/pages/CompanyEnvironments.test.tsx index 1b4137624d4..40c30301f1d 100644 --- a/ui/src/pages/CompanyEnvironments.test.tsx +++ b/ui/src/pages/CompanyEnvironments.test.tsx @@ -777,6 +777,37 @@ describe("CompanyEnvironments — test provider button", () => { }); }); + it("preserves the pinned Boat key when saving an unrelated environment edit", async () => { + root = createRoot(container); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + const apiKeySecretRef = { + type: "secret_ref", + secretId: "75c6e3a4-1e77-4ce1-af91-205509c06966", + version: 2, + }; + mockEnvironmentsApi.list.mockResolvedValue([{ + id: "env-boat", name: "Boat", driver: "computer", description: null, + config: { provider: "boat", sandboxId: "bx_existing", apiKeySecretRef }, + }]); + await act(async () => { + root!.render(renderCompanyEnvironments(queryClient, `${ENVIRONMENTS_PATH}/env-boat/edit`)); + }); + await waitForAssertion(() => { + expect(getEnvironmentFormPage()?.textContent).toContain("Boat API key"); + }); + const page = getEnvironmentFormPage()!; + const nameInput = Array.from(page.querySelectorAll("input")) + .find((input) => input.value === "Boat")!; + await act(async () => setInputValue(nameInput, "Renamed Boat")); + await act(async () => click(findButton(page, "Save environment"))); + await waitForAssertion(() => { + expect(mockEnvironmentsApi.update).toHaveBeenCalledExactlyOnceWith("env-boat", expect.objectContaining({ + name: "Renamed Boat", + config: { provider: "boat", sandboxId: "bx_existing", apiKeySecretRef }, + }), "company-1"); + }); + }); + it("confirms before cancelling the edit page with unsaved environment variable drafts", async () => { root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); diff --git a/ui/src/pages/CompanyEnvironments.tsx b/ui/src/pages/CompanyEnvironments.tsx index 2021b92fb83..a8a9d644434 100644 --- a/ui/src/pages/CompanyEnvironments.tsx +++ b/ui/src/pages/CompanyEnvironments.tsx @@ -11,7 +11,9 @@ import { FitAddon } from "@xterm/addon-fit"; import { Terminal as XTermTerminal } from "@xterm/xterm"; import "@xterm/xterm/css/xterm.css"; import { + envBindingSecretRefSchema, type EnvBinding, + type EnvSecretRefBinding, type Environment, type EnvironmentDeleteBlastRadius, type EnvironmentProviderCapability, @@ -71,7 +73,7 @@ type EnvironmentFormState = { driver: "local" | "ssh" | "sandbox" | "computer"; boatId: string; boatApiKey: string; - boatApiKeySecretId: string; + boatApiKeySecretRef: EnvSecretRefBinding | null; sshHost: string; sshPort: string; sshUsername: string; @@ -127,7 +129,7 @@ function buildEnvironmentPayload(form: EnvironmentFormState) { provider: "boat", sandboxId: form.boatId.trim(), ...(form.boatApiKey.trim() ? { apiKey: form.boatApiKey.trim() } : { - apiKeySecretRef: { type: "secret_ref" as const, secretId: form.boatApiKeySecretId, version: "latest" as const }, + apiKeySecretRef: form.boatApiKeySecretRef, }), } : form.driver === "ssh" ? { @@ -159,7 +161,7 @@ function createEmptyEnvironmentForm(): EnvironmentFormState { driver: "ssh", boatId: "", boatApiKey: "", - boatApiKeySecretId: "", + boatApiKeySecretRef: null, sshHost: "", sshPort: "22", sshUsername: "", @@ -229,11 +231,12 @@ function readSandboxConfig(environment: Environment) { function createEnvironmentFormFromEnvironment(environment: Environment): EnvironmentFormState { if (environment.driver === "computer") { - const ref = environment.config.apiKeySecretRef; + const parsedRef = envBindingSecretRefSchema.safeParse(environment.config.apiKeySecretRef); + const ref = parsedRef.success ? parsedRef.data : null; return { ...createEmptyEnvironmentForm(), name: environment.name, description: environment.description ?? "", driver: "computer", boatId: typeof environment.config.sandboxId === "string" ? environment.config.sandboxId : "", - boatApiKeySecretId: ref && typeof ref === "object" && "secretId" in ref && typeof ref.secretId === "string" ? ref.secretId : "", + boatApiKeySecretRef: ref, envVars: environment.envVars ?? {} }; } if (environment.driver === "ssh") { @@ -1857,7 +1860,7 @@ export function CompanyEnvironments({ mode = "list" }: CompanyEnvironmentsProps) environmentForm.name.trim().length > 0 && (environmentForm.driver !== "computer" || /^bx_[a-zA-Z0-9]+$/.test(environmentForm.boatId.trim()) && - Boolean(environmentForm.boatApiKey.trim() || environmentForm.boatApiKeySecretId)) && + Boolean(environmentForm.boatApiKey.trim() || environmentForm.boatApiKeySecretRef)) && (environmentForm.driver !== "ssh" || ( environmentForm.sshHost.trim().length > 0 && @@ -2295,7 +2298,7 @@ export function CompanyEnvironments({ mode = "list" }: CompanyEnvironmentsProps) setEnvironmentForm(current => ({ ...current, boatId: event.target.value }))} /> - + setEnvironmentForm(current => ({ ...current, boatApiKey: event.target.value }))} />