mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
fix: preserve pinned Boat credentials on environment edits
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
eaf6be127b
commit
e450faf402
2 files changed
+41
-7
No files matched your search
@@ -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<HTMLInputElement>("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 } } });
|
||||
|
||||
@@ -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)
|
||||
<Input value={environmentForm.boatId} placeholder="bx_…"
|
||||
onChange={(event) => setEnvironmentForm(current => ({ ...current, boatId: event.target.value }))} />
|
||||
</Field>
|
||||
<Field label="Boat API key" hint={environmentForm.boatApiKeySecretId ? "A key is saved. Leave blank to keep it." : "Stored as an encrypted secret."}>
|
||||
<Field label="Boat API key" hint={environmentForm.boatApiKeySecretRef ? "A key is saved. Leave blank to keep it." : "Stored as an encrypted secret."}>
|
||||
<Input type="password" autoComplete="new-password" value={environmentForm.boatApiKey}
|
||||
onChange={(event) => setEnvironmentForm(current => ({ ...current, boatApiKey: event.target.value }))} />
|
||||
</Field>
|
||||
|
||||
Reference in new issue
Block a user