paperclip/ui/src/components/EnvVarEditor.test.tsx

128 lines
4.1 KiB
TypeScript

// @vitest-environment jsdom
import { createRoot, type Root } from "react-dom/client";
import { flushSync } from "react-dom";
import type { CompanySecret, EnvBinding, UserSecretDefinition } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { EnvironmentVariablesEditor } from "./environment-variables-editor";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
const definition: UserSecretDefinition = {
id: "def-1",
companyId: "c1",
key: "PERSONAL_GH_TOKEN",
name: "Personal GitHub token",
description: null,
status: "active",
provider: "local_encrypted",
managedMode: "paperclip_managed",
providerConfigId: null,
providerMetadata: null,
usageGuidance: null,
createdByAgentId: null,
createdByUserId: null,
updatedByAgentId: null,
updatedByUserId: null,
deletedAt: null,
createdAt: new Date(),
updatedAt: new Date(),
};
async function act(callback: () => void | Promise<void>) {
let result: void | Promise<void> = undefined;
flushSync(() => {
result = callback();
});
await result;
}
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
function render(props: Partial<React.ComponentProps<typeof EnvironmentVariablesEditor>>) {
root = createRoot(container);
return act(() => {
root.render(
<EnvironmentVariablesEditor
value={props.value ?? {}}
secrets={props.secrets ?? []}
userSecretDefinitions={props.userSecretDefinitions}
onCreateSecret={props.onCreateSecret ?? (vi.fn() as never)}
onChange={props.onChange ?? vi.fn()}
/>,
);
});
}
describe("EnvironmentVariablesEditor user secret binding", () => {
it("renders an existing user_secret_ref as a User secret row with the definition and requirement", async () => {
const value: Record<string, EnvBinding> = {
GH_TOKEN: { type: "user_secret_ref", key: "PERSONAL_GH_TOKEN", required: true },
};
await render({ value, userSecretDefinitions: [definition] });
const keyInput = container.querySelector<HTMLInputElement>('input[placeholder="KEY"]');
expect(keyInput?.value).toBe("GH_TOKEN");
// Radix Select triggers render the selected label as text.
expect(container.textContent).toContain("User secret");
expect(container.textContent).toContain("Personal GitHub token");
expect(container.textContent).toContain("Required");
});
it("explains user-secret bindings when a user secret row is present", async () => {
await render({
value: { GH_TOKEN: { type: "user_secret_ref", key: "PERSONAL_GH_TOKEN", required: true } },
userSecretDefinitions: [definition],
});
expect(container.textContent).toContain("Personal GitHub token");
expect(container.textContent).toContain("User secrets resolve from the user responsible for the run.");
});
it("keeps working for company secrets when no user definitions are provided", async () => {
const secret: CompanySecret = {
id: "sec-1",
companyId: "c1",
scope: "company",
ownerUserId: null,
userSecretDefinitionId: null,
key: "api_key",
name: "API key",
provider: "local_encrypted",
status: "active",
managedMode: "paperclip_managed",
externalRef: null,
providerConfigId: null,
providerMetadata: null,
latestVersion: 2,
description: null,
lastResolvedAt: null,
lastRotatedAt: null,
deletedAt: null,
createdByAgentId: null,
createdByUserId: null,
createdAt: new Date(),
updatedAt: new Date(),
};
const value: Record<string, EnvBinding> = {
API_KEY: { type: "secret_ref", secretId: "sec-1", version: "latest" },
};
await render({ value, secrets: [secret] });
const keyInput = container.querySelector<HTMLInputElement>('input[placeholder="KEY"]');
expect(keyInput?.value).toBe("API_KEY");
expect(container.textContent).toContain("API key");
});
});