Files
PaperClipAI/ui/src/pages/CompanyEnvironments.test.tsx

1829 lines
69 KiB
TypeScript

// @vitest-environment jsdom
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TooltipProvider } from "@/components/ui/tooltip";
import { CompanyEnvironments } from "./CompanyEnvironments";
import { ApiError } from "@/api/client";
const xtermMocks = vi.hoisted(() => {
class MockTerminal {
readonly options: Record<string, unknown>;
cols: number;
rows: number;
writes: string[] = [];
focused = false;
disposed = false;
openedElement: HTMLElement | null = null;
private readonly dataHandlers: Array<(data: string) => void> = [];
constructor(options: Record<string, unknown> = {}) {
this.options = options;
this.cols = typeof options.cols === "number" ? options.cols : 80;
this.rows = typeof options.rows === "number" ? options.rows : 24;
xtermMocks.terminalInstances.push(this);
}
loadAddon(addon: { activate?: (terminal: MockTerminal) => void }) {
addon.activate?.(this);
}
open(element: HTMLElement) {
this.openedElement = element;
element.dataset.mockXtermOpen = "true";
}
onData(handler: (data: string) => void) {
this.dataHandlers.push(handler);
return {
dispose: () => {
const index = this.dataHandlers.indexOf(handler);
if (index >= 0) this.dataHandlers.splice(index, 1);
},
};
}
emitData(data: string) {
for (const handler of this.dataHandlers) handler(data);
}
write(data: string) {
this.writes.push(data);
if (!this.openedElement) return;
const chunk = document.createElement("span");
// Keep test DOM readable without reimplementing xterm's ANSI parser.
chunk.textContent = data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
this.openedElement.appendChild(chunk);
}
resize(cols: number, rows: number) {
this.cols = cols;
this.rows = rows;
}
focus() {
this.focused = true;
this.openedElement?.focus();
}
reset() {
this.writes = [];
if (this.openedElement) this.openedElement.textContent = "";
}
clear() {
if (this.openedElement) this.openedElement.textContent = "";
}
dispose() {
this.disposed = true;
}
}
class MockFitAddon {
fitCalls = 0;
terminal: MockTerminal | null = null;
constructor() {
xtermMocks.fitAddonInstances.push(this);
}
activate(terminal: MockTerminal) {
this.terminal = terminal;
}
fit() {
this.fitCalls += 1;
this.terminal?.resize(120, 32);
}
}
return {
MockTerminal,
MockFitAddon,
terminalInstances: [] as MockTerminal[],
fitAddonInstances: [] as MockFitAddon[],
reset() {
this.terminalInstances.length = 0;
this.fitAddonInstances.length = 0;
},
};
});
vi.mock("@xterm/xterm", () => ({
Terminal: xtermMocks.MockTerminal,
}));
vi.mock("@xterm/addon-fit", () => ({
FitAddon: xtermMocks.MockFitAddon,
}));
vi.mock("@xterm/xterm/css/xterm.css", () => ({}));
const mockEnvironmentsApi = vi.hoisted(() => ({
list: vi.fn(),
capabilities: vi.fn(),
secretRefs: vi.fn(),
probe: vi.fn(),
probeConfig: vi.fn(),
create: vi.fn(),
update: vi.fn(),
remove: vi.fn(),
deleteBlastRadius: vi.fn(),
setDefault: vi.fn(),
customImageTemplate: vi.fn(),
startCustomImageSetupSession: vi.fn(),
customImageSetupSession: vi.fn(),
createCustomImageTerminalSessionToken: vi.fn(),
finishCustomImageSetupSession: vi.fn(),
cancelCustomImageSetupSession: vi.fn(),
rollbackCustomImageTemplate: vi.fn(),
relinkCustomImageTemplate: vi.fn(),
disableCustomImageTemplate: vi.fn(),
}));
const mockInstanceSettingsApi = vi.hoisted(() => ({
get: vi.fn(),
getExperimental: vi.fn(),
}));
const mockSecretsApi = vi.hoisted(() => ({
list: vi.fn(),
}));
const mockAgentsApi = vi.hoisted(() => ({
list: vi.fn(),
update: vi.fn(),
}));
vi.mock("@/context/CompanyContext", () => ({
useCompany: () => ({
selectedCompanyId: "company-1",
selectedCompany: null,
}),
}));
vi.mock("@/context/BreadcrumbContext", () => ({
useBreadcrumbs: () => ({ setBreadcrumbs: vi.fn() }),
}));
vi.mock("@/context/ToastContext", () => ({
useToast: () => ({ pushToast: vi.fn() }),
useToastActions: () => ({ pushToast: vi.fn() }),
useOptionalToastActions: () => ({ pushToast: vi.fn() }),
}));
vi.mock("@/api/environments", () => ({
environmentsApi: mockEnvironmentsApi,
}));
vi.mock("@/api/instanceSettings", () => ({
instanceSettingsApi: mockInstanceSettingsApi,
}));
vi.mock("@/api/secrets", () => ({
secretsApi: mockSecretsApi,
}));
vi.mock("@/api/agents", () => ({
agentsApi: mockAgentsApi,
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
// Minimal browser APIs for jsdom.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
class FakeWebSocket {
static CONNECTING = 0;
static OPEN = 1;
static CLOSING = 2;
static CLOSED = 3;
static instances: FakeWebSocket[] = [];
readonly url: string;
readyState = FakeWebSocket.CONNECTING;
sent: string[] = [];
onopen: ((event: Event) => void) | null = null;
onmessage: ((event: MessageEvent) => void) | null = null;
onclose: ((event: CloseEvent) => void) | null = null;
onerror: ((event: Event) => void) | null = null;
constructor(url: string) {
this.url = url;
FakeWebSocket.instances.push(this);
}
send(data: string) {
this.sent.push(data);
}
close() {
this.readyState = FakeWebSocket.CLOSED;
this.onclose?.(new Event("close") as CloseEvent);
}
open() {
this.readyState = FakeWebSocket.OPEN;
this.onopen?.(new Event("open"));
}
emitMessage(data: string) {
this.onmessage?.(new MessageEvent("message", { data }));
}
}
async function act(callback: () => void | Promise<void>) {
await callback();
await Promise.resolve();
await new Promise((resolve) => window.setTimeout(resolve, 0));
}
async function flushReact() {
for (let i = 0; i < 3; i += 1) {
await Promise.resolve();
await new Promise((resolve) => window.setTimeout(resolve, 0));
}
}
async function waitForAssertion(assertion: () => void) {
let lastError: unknown;
for (let i = 0; i < 20; i += 1) {
await flushReact();
try {
assertion();
return;
} catch (error) {
lastError = error;
}
}
throw lastError;
}
function testProviderButtons(container: HTMLElement): HTMLButtonElement[] {
return Array.from(container.querySelectorAll("button")).filter((button) => {
const label = button.textContent?.trim();
return label === "Test provider" || label === "Testing...";
});
}
function findButton(root: ParentNode, label: string): HTMLButtonElement | undefined {
return Array.from(root.querySelectorAll("button")).find((button) => button.textContent?.trim() === label);
}
function findAction(root: ParentNode, label: string): HTMLElement | undefined {
return Array.from(root.querySelectorAll<HTMLElement>("button,a")).find((element) => element.textContent?.trim() === label);
}
function editButtons(root: ParentNode): HTMLElement[] {
return Array.from(root.querySelectorAll<HTMLElement>("button,a")).filter((element) => element.textContent?.trim() === "Edit");
}
function click(element: Element | null | undefined) {
element?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
setter?.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
}
function setSelectValue(select: HTMLSelectElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value")?.set;
setter?.call(select, value);
select.dispatchEvent(new Event("change", { bubbles: true }));
}
const ENVIRONMENTS_PATH = "/company/settings/instance/environments";
function getEnvironmentFormPage(): HTMLElement | null {
return document.body.querySelector("[data-testid='environment-form-page']");
}
// Opens the edit page for the environment at `index`. The environment list
// depends on an async query. Under a loaded test worker that query can resolve
// after the first flush, so a direct click can miss the Edit control and never
// open the form page. This helper first waits for the control, then clicks it,
// then waits for the form page.
async function openEnvironmentEditPage(container: HTMLElement, index = 0) {
await waitForAssertion(() => {
expect(editButtons(container)[index]).toBeTruthy();
});
await act(async () => click(editButtons(container)[index]));
await waitForAssertion(() => {
expect(getEnvironmentFormPage()).not.toBeNull();
});
}
function renderCompanyEnvironments(queryClient: QueryClient, initialPath = ENVIRONMENTS_PATH) {
return (
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[initialPath]}>
<TooltipProvider>
<Routes>
<Route path={ENVIRONMENTS_PATH} element={<CompanyEnvironments />} />
<Route path={`${ENVIRONMENTS_PATH}/new`} element={<CompanyEnvironments mode="create" />} />
<Route path={`${ENVIRONMENTS_PATH}/:environmentId/edit`} element={<CompanyEnvironments mode="edit" />} />
</Routes>
</TooltipProvider>
</MemoryRouter>
</QueryClientProvider>
);
}
function createSession(overrides: Record<string, unknown> = {}) {
return {
id: "session-1",
environmentId: "env-1",
templateId: null,
promotedTemplateId: null,
provider: "daytona",
providerLeaseId: "lease-redacted",
environmentLeaseId: null,
status: "waiting_for_user",
startedByUserId: "user-1",
startedByAgentId: null,
baseTemplateRef: null,
expiresAt: "2026-06-25T21:00:00.000Z",
finishedAt: null,
failureReason: null,
connectionSummary: {
type: "ssh",
username: "sandbox",
hostRedacted: true,
portRedacted: true,
},
connectionSecretRef: "secret-redacted",
metadata: null,
createdAt: "2026-06-25T20:00:00.000Z",
updatedAt: "2026-06-25T20:00:00.000Z",
...overrides,
};
}
function createTemplate(overrides: Record<string, unknown> = {}) {
return {
id: "template-1",
environmentId: "env-1",
provider: "daytona",
templateKind: "snapshot",
templateRef: "redacted-template-ref",
sourceTemplateRef: null,
sourceEnvironmentConfigFingerprint: "fingerprint",
status: "active",
createdByUserId: "user-1",
createdByAgentId: null,
capturedAt: "2026-06-25T20:00:00.000Z",
lastUsedAt: null,
supersededByTemplateId: null,
metadata: null,
createdAt: "2026-06-25T20:00:00.000Z",
updatedAt: "2026-06-25T20:00:00.000Z",
...overrides,
};
}
function createDeleteBlastRadius(overrides: {
canDelete?: boolean;
deleteBlockedReasons?: string[];
staticReferences?: Record<string, unknown>;
activeRuntimeUse?: Record<string, unknown>;
pendingCleanupLeaseCount?: number;
reusableSandboxLeaseCount?: number;
reusableSandboxLeaseHolders?: Array<Record<string, unknown>>;
} = {}) {
return {
environmentId: "env-1",
canDelete: overrides.canDelete ?? true,
deleteBlockedReasons: overrides.deleteBlockedReasons ?? [],
reusableSandboxLeaseHolders: overrides.reusableSandboxLeaseHolders ?? [],
staticReferences: {
isManagedLocal: false,
isInstanceDefault: false,
agentDefaultCount: 0,
executionWorkspaceSelectionCount: 0,
issueSelectionCount: 0,
projectSelectionCount: 0,
secretBindingCount: 0,
...overrides.staticReferences,
},
activeRuntimeUse: {
activeLeaseCount: 0,
activeCustomImageSetupSessionCount: 0,
hasActiveRuntimeUse: false,
...overrides.activeRuntimeUse,
},
pendingCleanupLeaseCount: overrides.pendingCleanupLeaseCount ?? 0,
reusableSandboxLeaseCount: overrides.reusableSandboxLeaseCount ?? 0,
};
}
function supportedDaytonaCapabilities() {
return {
adapters: [],
drivers: { local: "supported", ssh: "supported", sandbox: "supported", plugin: "unsupported" },
sandboxProviders: {
daytona: {
status: "supported",
supportsSavedProbe: true,
supportsUnsavedProbe: true,
supportsRunExecution: true,
supportsReusableLeases: true,
supportsInteractiveSetup: true,
interactiveSetupConnectionTypes: ["ssh"],
supportsTemplateCapture: true,
supportsTemplateDelete: true,
displayName: "Daytona",
},
},
};
}
describe("CompanyEnvironments — test provider button", () => {
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot> | null;
let probeResolvers: Map<string, () => void>;
let originalWebSocket: typeof WebSocket | undefined;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = null;
probeResolvers = new Map();
originalWebSocket = globalThis.WebSocket;
FakeWebSocket.instances = [];
xtermMocks.reset();
globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket;
mockInstanceSettingsApi.get.mockResolvedValue({ defaultEnvironmentId: null });
mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableEnvironments: true });
mockEnvironmentsApi.capabilities.mockResolvedValue({ adapters: [], sandboxProviders: {} });
mockEnvironmentsApi.secretRefs.mockResolvedValue({ refs: [] });
mockSecretsApi.list.mockResolvedValue([]);
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: null,
activeSession: null,
latestSession: null,
});
mockEnvironmentsApi.startCustomImageSetupSession.mockResolvedValue({
session: createSession(),
connectionPayload: { type: "ssh", command: "ssh sandbox@setup.example.invalid" },
});
mockEnvironmentsApi.customImageSetupSession.mockResolvedValue({
session: createSession(),
connectionPayload: { type: "ssh", command: "ssh sandbox@setup.example.invalid" },
});
mockEnvironmentsApi.createCustomImageTerminalSessionToken.mockResolvedValue({
id: "terminal-session-1",
token: "terminal-token-terminal-token-123456",
expiresAt: "2026-06-25T20:05:00.000Z",
setupSessionId: "session-1",
environmentId: "env-1",
connectionType: "ssh",
websocketPath:
"/api/environment-custom-image-setup-sessions/session-1/terminal/ws?terminalSessionId=terminal-session-1",
});
mockEnvironmentsApi.finishCustomImageSetupSession.mockResolvedValue({
session: createSession({ status: "promoted", promotedTemplateId: "template-1", finishedAt: "2026-06-25T20:10:00.000Z" }),
template: createTemplate(),
connectionPayload: null,
});
mockEnvironmentsApi.cancelCustomImageSetupSession.mockResolvedValue(
createSession({ status: "cancelled", finishedAt: "2026-06-25T20:10:00.000Z" }),
);
mockEnvironmentsApi.rollbackCustomImageTemplate.mockResolvedValue({
activeTemplate: createTemplate({ id: "template-previous" }),
supersededTemplate: createTemplate({ id: "template-current", status: "superseded" }),
});
mockEnvironmentsApi.relinkCustomImageTemplate.mockResolvedValue({
template: createTemplate({ id: "template-relinked" }),
classification: "knob_only",
});
mockEnvironmentsApi.disableCustomImageTemplate.mockResolvedValue(
createTemplate({ status: "revoked" }),
);
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Alpha", driver: "sandbox", description: null, config: { provider: "e2b" } },
{ id: "env-2", name: "Beta", driver: "sandbox", description: null, config: { provider: "e2b" } },
]);
mockEnvironmentsApi.create.mockImplementation(async (_companyId: string, body: { name: string }) => ({
id: "env-new",
name: body.name,
driver: "ssh",
description: null,
config: {},
}));
mockEnvironmentsApi.update.mockImplementation(async (environmentId: string, body: { name: string }) => ({
id: environmentId,
name: body.name,
driver: "sandbox",
description: null,
config: { provider: "e2b" },
}));
mockEnvironmentsApi.deleteBlastRadius.mockResolvedValue(createDeleteBlastRadius());
mockEnvironmentsApi.remove.mockImplementation(async (environmentId: string) => ({
id: environmentId,
name: environmentId === "env-1" ? "Alpha" : "Beta",
driver: "sandbox",
description: null,
config: { provider: "e2b" },
}));
mockAgentsApi.list.mockResolvedValue([]);
mockAgentsApi.update.mockResolvedValue({});
// Each probe stays pending until its resolver is called, so the testing
// state remains observable and can be settled per environment.
mockEnvironmentsApi.probe.mockImplementation(
(environmentId: string) =>
new Promise<{ ok: boolean; driver: string; summary: string; details: null }>((resolve) => {
probeResolvers.set(environmentId, () =>
resolve({ ok: true, driver: "sandbox", summary: "ok", details: null }),
);
}),
);
});
afterEach(() => {
root?.unmount();
root = null;
container.remove();
document.body.innerHTML = "";
if (originalWebSocket) {
globalThis.WebSocket = originalWebSocket;
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (globalThis as any).WebSocket;
}
vi.restoreAllMocks();
vi.clearAllMocks();
});
it("shows the testing state only on the clicked environment's button", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
const buttonsBefore = testProviderButtons(container);
expect(buttonsBefore).toHaveLength(2);
expect(buttonsBefore.every((button) => button.textContent?.trim() === "Test provider")).toBe(true);
expect(buttonsBefore.every((button) => !button.disabled)).toBe(true);
await act(async () => {
buttonsBefore[0].dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
const buttonsAfter = testProviderButtons(container);
expect(buttonsAfter).toHaveLength(2);
expect(buttonsAfter[0].textContent?.trim()).toBe("Testing...");
expect(buttonsAfter[0].disabled).toBe(true);
expect(buttonsAfter[1].textContent?.trim()).toBe("Test provider");
expect(buttonsAfter[1].disabled).toBe(false);
expect(mockEnvironmentsApi.probe).toHaveBeenCalledExactlyOnceWith("env-1", "company-1");
});
it("explains that successful sandbox provider tests use a temporary sandbox", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.probe.mockResolvedValue({
ok: true,
driver: "sandbox",
summary: "Connected to Daytona sandbox paperclip-probe.",
details: {
provider: "daytona",
diagnostics: [],
metadata: {
provider: "daytona",
sandboxId: "473167E9",
sandboxName: "paperclip-probe",
},
},
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await act(async () => {
testProviderButtons(container)[0].dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
expect(mockEnvironmentsApi.probe).toHaveBeenCalledExactlyOnceWith("env-1", "company-1");
expect(container.textContent).toContain("Connected to Daytona sandbox paperclip-probe.");
expect(container.textContent).not.toContain("Verified temporary daytona sandbox");
expect(container.textContent).not.toContain("Test probes clean up the validation sandbox after the check");
expect(container.textContent).not.toContain("provider dashboard");
});
it("does not show sandbox lifecycle success copy for failed sandbox provider tests", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.probe.mockResolvedValue({
ok: false,
driver: "sandbox",
summary: "Daytona sandbox probe failed.",
details: {
provider: "daytona",
error: "Sandbox image was not found.",
metadata: {
provider: "daytona",
sandboxId: "473167E9",
sandboxName: "paperclip-probe",
},
},
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await act(async () => {
testProviderButtons(container)[0].dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
expect(container.textContent).toContain("Daytona sandbox probe failed.");
expect(container.textContent).toContain("Sandbox image was not found.");
expect(container.textContent).not.toContain("Verified temporary daytona sandbox");
expect(container.textContent).not.toContain("Test probes clean up the validation sandbox after the check");
});
it("keeps the second environment's testing state when an earlier probe settles", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
// Click both rows in quick succession while both probes are still pending.
await act(async () => {
testProviderButtons(container)[0].dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
await act(async () => {
testProviderButtons(container)[1].dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
// Settle only the first environment's probe.
await act(async () => {
probeResolvers.get("env-1")?.();
});
await flushReact();
const buttons = testProviderButtons(container);
expect(buttons[1].textContent?.trim()).toBe("Testing...");
expect(buttons[1].disabled).toBe(true);
});
it("opens the add-environment form on a standalone page and closes it on cancel", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
const options = Array.from(container.querySelectorAll("option"));
expect(options.some((option) => option.textContent?.trim() === "Local")).toBe(true);
await act(async () => {
click(container.querySelector('[aria-label="Add environment"]'));
});
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Add environment");
});
await act(async () => {
findButton(document.body, "Cancel")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
expect(getEnvironmentFormPage()).toBeNull();
});
it("opens the edit form on a standalone page with existing values and closes after save", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await act(async () => {
click(findAction(container, "Edit"));
});
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Edit environment");
});
const page = getEnvironmentFormPage();
expect(document.body.querySelector("[role='dialog']")).toBeNull();
expect(
Array.from(page?.querySelectorAll("input") ?? []).some((input) => (input as HTMLInputElement).value === "Alpha"),
).toBe(true);
await act(async () => click(findButton(page!, "Add variable")));
await flushReact();
const variableName = page!.querySelector<HTMLInputElement>('input[aria-label="Variable name"]')!;
const variableValue = page!.querySelector<HTMLInputElement>('input[aria-label="Variable value"]')!;
await act(async () => {
setInputValue(variableName, "API_TOKEN");
setInputValue(variableValue, "draft-token");
});
await flushReact();
await act(async () => {
findButton(document.body, "Save environment")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
expect(mockEnvironmentsApi.update).toHaveBeenCalledExactlyOnceWith(
"env-1",
expect.objectContaining({
name: "Alpha",
driver: "sandbox",
envVars: { API_TOKEN: { type: "plain", value: "draft-token" } },
}),
// The secret-context company must ride along so the server can scope
// bindings even when the environment has none yet.
"company-1",
);
// Saving awaits query invalidation before navigating away. A single flush
// can observe the API call before that asynchronous success path settles.
await waitForAssertion(() => {
expect(getEnvironmentFormPage()).toBeNull();
});
});
it("confirms before cancelling the edit page with unsaved environment variable drafts", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false);
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await act(async () => {
click(findAction(container, "Edit"));
});
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Edit environment");
});
const page = getEnvironmentFormPage()!;
await act(async () => click(findButton(page, "Add variable")));
await flushReact();
const variableName = page.querySelector<HTMLInputElement>('input[aria-label="Variable name"]')!;
const variableValue = page.querySelector<HTMLInputElement>('input[aria-label="Variable value"]')!;
await act(async () => {
setInputValue(variableName, "API_TOKEN");
setInputValue(variableValue, "draft-token");
});
await flushReact();
await act(async () => click(findButton(document.body, "Cancel")));
await flushReact();
expect(confirmSpy).toHaveBeenCalledWith("Discard unsaved environment changes?");
expect(getEnvironmentFormPage()).not.toBeNull();
confirmSpy.mockReturnValue(true);
await act(async () => click(findButton(document.body, "Cancel")));
await flushReact();
expect(getEnvironmentFormPage()).toBeNull();
});
it("keeps unload and in-app link warnings after env var changes are staged into the form", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false);
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await act(async () => {
click(findAction(container, "Edit"));
});
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Edit environment");
});
const page = getEnvironmentFormPage()!;
await act(async () => click(findButton(page, "Add variable")));
await flushReact();
const variableName = page.querySelector<HTMLInputElement>('input[aria-label="Variable name"]')!;
const variableValue = page.querySelector<HTMLInputElement>('input[aria-label="Variable value"]')!;
await act(async () => {
setInputValue(variableName, "API_TOKEN");
setInputValue(variableValue, "draft-token");
});
await flushReact();
await act(async () => click(findButton(page, "Save")));
await flushReact();
expect(page.textContent).not.toContain("Unsaved changes");
const beforeUnload = new Event("beforeunload", { cancelable: true });
window.dispatchEvent(beforeUnload);
expect(beforeUnload.defaultPrevented).toBe(true);
const link = document.createElement("a");
link.href = "/agents/dashboard";
document.body.appendChild(link);
const clickEvent = new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 });
link.dispatchEvent(clickEvent);
expect(confirmSpy).toHaveBeenCalledWith("Discard unsaved environment changes?");
expect(clickEvent.defaultPrevented).toBe(true);
expect(getEnvironmentFormPage()).not.toBeNull();
link.remove();
});
it("shows image setup controls only for providers advertising setup and capture support", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
{ id: "env-2", name: "E2B", driver: "sandbox", description: null, config: { provider: "e2b" } },
{ id: "env-3", name: "Policy", driver: "sandbox", description: null, config: { provider: "policy" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue({
adapters: [],
drivers: { local: "supported", ssh: "supported", sandbox: "supported", plugin: "unsupported" },
sandboxProviders: {
daytona: supportedDaytonaCapabilities().sandboxProviders.daytona,
e2b: {
status: "supported",
supportsSavedProbe: true,
supportsUnsavedProbe: true,
supportsRunExecution: true,
supportsReusableLeases: true,
supportsInteractiveSetup: false,
interactiveSetupConnectionTypes: [],
supportsTemplateCapture: false,
supportsTemplateDelete: false,
displayName: "E2B",
},
policy: {
status: "supported",
supportsSavedProbe: true,
supportsUnsavedProbe: true,
supportsRunExecution: true,
supportsReusableLeases: true,
supportsInteractiveSetup: true,
interactiveSetupConnectionTypes: ["ssh"],
supportsTemplateCapture: false,
supportsTemplateDelete: false,
displayName: "Policy Sandbox",
},
},
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
// Daytona supports setup + capture -> "Configure image" on its edit page.
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Configure image");
});
expect(mockEnvironmentsApi.customImageTemplate).toHaveBeenCalledExactlyOnceWith("env-1", "company-1");
await act(async () => click(findButton(document.body, "Cancel")));
await waitForAssertion(() => expect(getEnvironmentFormPage()).toBeNull());
// E2B does not advertise interactive setup.
await openEnvironmentEditPage(container, 1);
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Unsupported provider");
});
expect(getEnvironmentFormPage()?.textContent).not.toContain("Configure image");
await act(async () => click(findButton(document.body, "Cancel")));
await waitForAssertion(() => expect(getEnvironmentFormPage()).toBeNull());
// Provider advertises setup but cannot capture an image.
await openEnvironmentEditPage(container, 2);
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Setup capture unavailable");
});
expect(getEnvironmentFormPage()?.textContent).not.toContain("Configure image");
});
it("shows a live connect command and removes it after cancellation", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const command = "ssh sandbox@setup.example.invalid -p 2222";
let activeSession: ReturnType<typeof createSession> | null = createSession();
let latestSession: ReturnType<typeof createSession> | null = activeSession;
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockImplementation(async () => ({
activeTemplate: null,
activeSession,
latestSession,
}));
mockEnvironmentsApi.customImageSetupSession.mockResolvedValue({
session: createSession(),
connectionPayload: { type: "ssh", command },
});
mockEnvironmentsApi.cancelCustomImageSetupSession.mockImplementation(async () => {
activeSession = null;
latestSession = createSession({ status: "cancelled", finishedAt: "2026-06-25T20:10:00.000Z" });
return latestSession;
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain(command);
});
await act(async () => click(findButton(getEnvironmentFormPage()!, "Cancel")));
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Setup cancelled");
});
expect(mockEnvironmentsApi.cancelCustomImageSetupSession).toHaveBeenCalledExactlyOnceWith(
"session-1",
{ reason: "operator cancelled" },
);
expect(getEnvironmentFormPage()?.textContent).not.toContain(command);
});
it("opens an embedded browser terminal automatically while preserving the SSH command fallback", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const command = "ssh sandbox@setup.example.invalid -p 2222";
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: null,
activeSession: createSession(),
latestSession: createSession(),
});
mockEnvironmentsApi.customImageSetupSession.mockResolvedValue({
session: createSession(),
connectionPayload: { type: "ssh", command },
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain(command);
expect(getEnvironmentFormPage()?.textContent).toContain("Browser terminal");
expect(getEnvironmentFormPage()?.textContent).toContain("SSH command fallback");
expect(mockEnvironmentsApi.createCustomImageTerminalSessionToken).toHaveBeenCalledExactlyOnceWith("session-1", {});
expect(FakeWebSocket.instances).toHaveLength(1);
});
expect(FakeWebSocket.instances[0].url).toContain(
"/api/environment-custom-image-setup-sessions/session-1/terminal/ws?terminalSessionId=terminal-session-1",
);
expect(FakeWebSocket.instances[0].url).not.toContain("token=");
expect(FakeWebSocket.instances[0].url).not.toContain("terminal-token-terminal-token-123456");
expect(FakeWebSocket.instances[0].url).toContain("cols=120");
expect(FakeWebSocket.instances[0].url).toContain("rows=32");
expect(xtermMocks.terminalInstances[0].options.cursorBlink).toBe(true);
expect(xtermMocks.terminalInstances[0].options.cursorInactiveStyle).toBe("bar");
expect(xtermMocks.terminalInstances[0].options.cursorStyle).toBe("bar");
expect(xtermMocks.terminalInstances[0].options.cursorWidth).toBe(2);
expect(xtermMocks.terminalInstances[0].options.customGlyphs).toBe(true);
expect(xtermMocks.terminalInstances[0].options.letterSpacing).toBe(0);
expect(xtermMocks.terminalInstances[0].options.theme).toMatchObject({
cursor: "#22d3ee",
cursorAccent: "#020617",
});
expect(String(xtermMocks.terminalInstances[0].options.fontFamily)).toContain("Nerd Font");
await act(async () => {
FakeWebSocket.instances[0].open();
FakeWebSocket.instances[0].emitMessage(JSON.stringify({ type: "ready" }));
FakeWebSocket.instances[0].emitMessage(JSON.stringify({ type: "output", data: "\u001b[?2004hsetup shell\r\n$ " }));
});
const terminalScreen = getEnvironmentFormPage()?.querySelector<HTMLElement>(
"[data-testid='custom-image-terminal-screen-session-1']",
);
await waitForAssertion(() => {
expect(terminalScreen?.dataset.mockXtermOpen).toBe("true");
expect(xtermMocks.terminalInstances[0].writes.join("")).toContain("setup shell");
expect(xtermMocks.terminalInstances[0].focused).toBe(true);
expect(document.activeElement).toBe(terminalScreen);
expect(getEnvironmentFormPage()?.textContent).toContain(command);
});
expect(FakeWebSocket.instances[0].sent).toContain(JSON.stringify({
type: "auth",
token: "terminal-token-terminal-token-123456",
}));
expect(FakeWebSocket.instances[0].sent).toContain(JSON.stringify({ type: "resize", cols: 120, rows: 32 }));
await act(async () => {
xtermMocks.terminalInstances[0].emitData("l");
xtermMocks.terminalInstances[0].emitData("\r");
});
expect(FakeWebSocket.instances[0].sent).toContain(JSON.stringify({ type: "input", data: "l" }));
expect(FakeWebSocket.instances[0].sent).toContain(JSON.stringify({ type: "input", data: "\r" }));
});
it("keeps the environment edit page open when Escape is pressed in setup terminal", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const command = "ssh sandbox@setup.example.invalid -p 2222";
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: null,
activeSession: createSession(),
latestSession: createSession(),
});
mockEnvironmentsApi.customImageSetupSession.mockResolvedValue({
session: createSession(),
connectionPayload: { type: "ssh", command },
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
let terminalScreen: HTMLElement | null = null;
await waitForAssertion(() => {
terminalScreen = getEnvironmentFormPage()?.querySelector<HTMLElement>(
"[data-testid='custom-image-terminal-screen-session-1']",
) ?? null;
expect(terminalScreen).toBeTruthy();
expect(document.body.querySelector("[role='dialog']")).toBeNull();
});
await act(async () => {
terminalScreen?.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
});
await flushReact();
expect(getEnvironmentFormPage()?.textContent).toContain(command);
expect(getEnvironmentFormPage()?.textContent).toContain("Edit environment");
});
it("does not render connect details when an active session refreshes as expired", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const command = "ssh sandbox@setup.example.invalid -p 2222";
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: null,
activeSession: createSession(),
latestSession: createSession(),
});
mockEnvironmentsApi.customImageSetupSession.mockResolvedValue({
session: createSession({ status: "timed_out", finishedAt: "2026-06-25T20:10:00.000Z" }),
connectionPayload: { type: "ssh", command },
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Setup expired");
});
expect(getEnvironmentFormPage()?.textContent).not.toContain(command);
});
it("shows a setup connection refresh fallback without breaking finish or cancel", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: null,
activeSession: createSession(),
latestSession: createSession(),
});
mockEnvironmentsApi.customImageSetupSession.mockRejectedValue(new Error("proxy unavailable"));
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Setup connection details could not be refreshed.");
});
const dialog = getEnvironmentFormPage()!;
expect(findButton(dialog, "Finished")?.disabled).toBe(false);
expect(findButton(dialog, "Cancel")?.disabled).toBe(false);
await act(async () => click(findButton(dialog, "Finished")));
await flushReact();
expect(mockEnvironmentsApi.finishCustomImageSetupSession).toHaveBeenCalledExactlyOnceWith("session-1", {});
});
it("shows provider fallback messaging for unsupported setup connection payloads", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: null,
activeSession: createSession(),
latestSession: createSession(),
});
mockEnvironmentsApi.customImageSetupSession.mockResolvedValue({
session: createSession(),
connectionPayload: { type: "browser_terminal", command: null },
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Browser terminal is not available for this provider connection.");
});
const dialog = getEnvironmentFormPage()!;
expect(findButton(dialog, "Finished")?.disabled).toBe(false);
expect(findButton(dialog, "Cancel")?.disabled).toBe(false);
});
it("shows active template controls for refresh, rollback, and disable", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const activeTemplateId = "12345678-90ab-cdef-1234-567890abcdef";
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: createTemplate({ id: activeTemplateId }),
activeSession: null,
latestSession: null,
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
const dialog = getEnvironmentFormPage();
expect(dialog?.textContent).toContain("Active template");
expect(dialog?.textContent).toContain("redacted-template-ref");
expect(dialog?.textContent).not.toContain("id 12345678-90a");
expect(
dialog?.querySelector(
"[title='Provider snapshot ref redacted-template-ref (Paperclip template 12345678-90ab-cdef-1234-567890abcdef)']",
),
).toBeTruthy();
expect(findButton(dialog!, "Refresh")).toBeTruthy();
expect(findButton(dialog!, "Rollback")).toBeTruthy();
expect(findButton(dialog!, "Disable")).toBeTruthy();
});
await act(async () => click(findButton(getEnvironmentFormPage()!, "Refresh")));
await flushReact();
expect(mockEnvironmentsApi.startCustomImageSetupSession).toHaveBeenCalledWith(
"env-1",
"company-1",
{ templateId: activeTemplateId },
);
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Browser terminal");
expect(getEnvironmentFormPage()?.textContent).toContain("SSH command fallback");
expect(mockEnvironmentsApi.createCustomImageTerminalSessionToken).toHaveBeenCalledExactlyOnceWith("session-1", {});
expect(FakeWebSocket.instances).toHaveLength(1);
});
});
it("allows stale capturing setup sessions to be cancelled back to active template controls", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const activeTemplate = createTemplate({ id: "template-active" });
let activeSession: ReturnType<typeof createSession> | null = createSession({ status: "capturing" });
let latestSession: ReturnType<typeof createSession> | null = activeSession;
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockImplementation(async () => ({
activeTemplate,
activeSession,
latestSession,
}));
mockEnvironmentsApi.customImageSetupSession.mockResolvedValue({
session: activeSession,
connectionPayload: null,
});
mockEnvironmentsApi.cancelCustomImageSetupSession.mockImplementation(async () => {
activeSession = null;
latestSession = createSession({ status: "cancelled", finishedAt: "2026-06-25T20:10:00.000Z" });
return latestSession;
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
const dialog = getEnvironmentFormPage()!;
expect(dialog.textContent).toContain("Capturing template");
expect(dialog.textContent).toContain("Capture is in progress.");
expect(findButton(dialog, "Finished")?.disabled).toBe(true);
expect(findButton(dialog, "Cancel")?.disabled).toBe(false);
});
await act(async () => click(findButton(getEnvironmentFormPage()!, "Cancel")));
await waitForAssertion(() => {
const dialog = getEnvironmentFormPage()!;
expect(dialog.textContent).toContain("Active template");
expect(findButton(dialog, "Refresh")).toBeTruthy();
});
expect(mockEnvironmentsApi.cancelCustomImageSetupSession).toHaveBeenCalledExactlyOnceWith(
"session-1",
{ reason: "operator cancelled" },
);
});
it("shows an out-of-sync warning when the active template no longer matches the saved config", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: createTemplate({ id: "template-active" }),
activeTemplateMatchesConfig: false,
activeSession: null,
latestSession: null,
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
const dialog = getEnvironmentFormPage()!;
expect(dialog.textContent).toContain("Active template");
expect(dialog.textContent).toContain("Not in use — the environment configuration changed");
});
});
it("names the changed boot-source field in the out-of-sync banner for a boot-source drift", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: createTemplate({ id: "template-active" }),
activeTemplateMatchesConfig: false,
activeTemplateDrift: {
classification: "boot_source_drift",
driftedPaths: [{ path: "snapshot", from: "a", to: "b" }],
},
activeSession: null,
latestSession: null,
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
const dialog = getEnvironmentFormPage()!;
expect(dialog.textContent).toContain("Not in use — Base image changed: snapshot `a` -> `b`");
expect(dialog.textContent).not.toContain("the environment configuration changed");
});
});
it("keeps the generic out-of-sync banner for an unclassified drift", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: createTemplate({ id: "template-active" }),
activeTemplateMatchesConfig: false,
activeTemplateDrift: { classification: "unclassified", driftedPaths: [{ path: "apiUrl" }] },
activeSession: null,
latestSession: null,
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
const dialog = getEnvironmentFormPage()!;
expect(dialog.textContent).toContain("Not in use — the environment configuration changed");
expect(dialog.textContent).not.toContain("Base image changed");
});
});
it("does not show the out-of-sync warning when the active template matches the saved config", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: createTemplate({ id: "template-active" }),
activeTemplateMatchesConfig: true,
activeSession: null,
latestSession: null,
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
const dialog = getEnvironmentFormPage()!;
expect(dialog.textContent).toContain("Active template");
expect(dialog.textContent).not.toContain("Not in use");
});
});
it("passes company context when rolling back and disabling an active template", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: createTemplate({ id: "template-active" }),
activeSession: null,
latestSession: null,
});
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
const dialog = getEnvironmentFormPage();
expect(dialog?.textContent).toContain("Active template");
expect(findButton(dialog!, "Rollback")).toBeTruthy();
expect(findButton(dialog!, "Disable")).toBeTruthy();
});
await act(async () => click(findButton(getEnvironmentFormPage()!, "Rollback")));
await waitForAssertion(() => {
expect(mockEnvironmentsApi.rollbackCustomImageTemplate).toHaveBeenCalledExactlyOnceWith("env-1", "company-1");
});
await act(async () => click(findButton(getEnvironmentFormPage()!, "Disable")));
await waitForAssertion(() => {
expect(mockEnvironmentsApi.disableCustomImageTemplate).toHaveBeenCalledExactlyOnceWith("env-1", "company-1");
});
});
function setupOutOfSyncTemplatePanel() {
mockEnvironmentsApi.list.mockResolvedValue([
{ id: "env-1", name: "Daytona", driver: "sandbox", description: null, config: { provider: "daytona" } },
]);
mockEnvironmentsApi.capabilities.mockResolvedValue(supportedDaytonaCapabilities());
mockEnvironmentsApi.customImageTemplate.mockResolvedValue({
activeTemplate: createTemplate({ id: "template-active" }),
activeTemplateMatchesConfig: false,
activeSession: null,
latestSession: null,
});
}
it("relinks an out-of-sync template and names both remedies in the copy", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
setupOutOfSyncTemplatePanel();
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
const dialog = getEnvironmentFormPage()!;
expect(dialog.textContent).toContain("relink this image or capture a new one");
expect(findButton(dialog, "Relink")).toBeTruthy();
});
await act(async () => click(findButton(getEnvironmentFormPage()!, "Relink")));
await waitForAssertion(() => {
expect(mockEnvironmentsApi.relinkCustomImageTemplate).toHaveBeenCalledExactlyOnceWith("env-1", "company-1");
});
});
it("confirms boot-source drift before re-sending the relink with the flag", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
setupOutOfSyncTemplatePanel();
mockEnvironmentsApi.relinkCustomImageTemplate
.mockRejectedValueOnce(new ApiError("Confirm the relink.", 409, {
error: "Confirm the relink.",
details: {
classification: "boot_source_drift",
driftedPaths: [{ path: "image", from: "fake:base", to: "fake:other" }],
},
}))
.mockResolvedValueOnce({
template: createTemplate({ id: "template-relinked" }),
classification: "boot_source_drift",
});
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
try {
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
expect(findButton(getEnvironmentFormPage()!, "Relink")).toBeTruthy();
});
await act(async () => click(findButton(getEnvironmentFormPage()!, "Relink")));
await waitForAssertion(() => {
expect(mockEnvironmentsApi.relinkCustomImageTemplate).toHaveBeenNthCalledWith(1, "env-1", "company-1");
expect(mockEnvironmentsApi.relinkCustomImageTemplate).toHaveBeenNthCalledWith(
2,
"env-1",
"company-1",
{ confirmBootSourceDrift: true },
);
});
expect(confirmSpy).toHaveBeenCalledTimes(1);
expect(confirmSpy.mock.calls[0]![0]).toContain("image fake:base -> fake:other");
} finally {
confirmSpy.mockRestore();
}
});
it("does not re-send the relink when the operator declines the confirmation", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
setupOutOfSyncTemplatePanel();
mockEnvironmentsApi.relinkCustomImageTemplate.mockRejectedValueOnce(new ApiError("Cannot verify.", 409, {
error: "Cannot verify.",
details: { classification: "unclassified", driftedPaths: [{ path: "apiUrl" }] },
}));
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false);
try {
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
await openEnvironmentEditPage(container);
await waitForAssertion(() => {
expect(findButton(getEnvironmentFormPage()!, "Relink")).toBeTruthy();
});
await act(async () => click(findButton(getEnvironmentFormPage()!, "Relink")));
await waitForAssertion(() => {
expect(confirmSpy).toHaveBeenCalledTimes(1);
});
expect(confirmSpy.mock.calls[0]![0]).toContain("cannot verify the boot source");
expect(mockEnvironmentsApi.relinkCustomImageTemplate).toHaveBeenCalledTimes(1);
expect(mockEnvironmentsApi.relinkCustomImageTemplate).toHaveBeenCalledWith("env-1", "company-1");
} finally {
confirmSpy.mockRestore();
}
});
it("hides the implicit Local option in the default picker under managed-sandbox-only", async () => {
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
enableEnvironments: true,
enableManagedSandboxOnly: true,
});
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient));
});
await flushReact();
const options = Array.from(container.querySelectorAll("option"));
expect(options.some((option) => option.textContent?.trim() === "Local")).toBe(false);
// Saved non-local environments remain selectable defaults.
expect(options.some((option) => option.textContent?.includes("Alpha"))).toBe(true);
});
it("reassigns this company's active agents to the chosen environment before deleting", async () => {
mockAgentsApi.list.mockResolvedValue([
{ id: "agent-1", name: "Iris", status: "active", defaultEnvironmentId: "env-1" },
{ id: "agent-2", name: "Miles", status: "active", defaultEnvironmentId: "env-2" },
{ id: "agent-3", name: "Retired", status: "terminated", defaultEnvironmentId: "env-1" },
]);
mockEnvironmentsApi.deleteBlastRadius.mockResolvedValue(
createDeleteBlastRadius({ staticReferences: { agentDefaultCount: 2 } }),
);
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(getEnvironmentFormPage()?.textContent).toContain("Edit environment");
});
const deleteButton = document.body.querySelector<HTMLButtonElement>("[data-testid='environment-delete-button']");
expect(deleteButton).not.toBeNull();
await act(async () => click(deleteButton));
await waitForAssertion(() => {
expect(
document.body.querySelector("[data-testid='environment-delete-dialog']")?.textContent,
).toContain("1 agent uses this environment as their default");
});
const dialog = document.body.querySelector<HTMLElement>("[data-testid='environment-delete-dialog']")!;
// Only this company's active agents are reassignable; the terminated one is
// left to the FK fallback and surfaces through the impact note instead.
expect(dialog.textContent).toContain("Iris");
expect(dialog.textContent).not.toContain("Retired");
expect(dialog.textContent).toContain("Other references to this environment");
const reassignSelect = dialog.querySelector<HTMLSelectElement>(
"[data-testid='environment-delete-reassign-select']",
)!;
const optionLabels = Array.from(reassignSelect.querySelectorAll("option")).map((option) =>
option.textContent?.trim(),
);
expect(optionLabels).toEqual(["Default: Local", "Beta · sandbox"]);
await act(async () => setSelectValue(reassignSelect, "env-2"));
await act(async () =>
click(document.body.querySelector("[data-testid='environment-delete-confirm']")),
);
await flushReact();
expect(mockAgentsApi.update).toHaveBeenCalledExactlyOnceWith(
"agent-1",
{ defaultEnvironmentId: "env-2" },
"company-1",
);
expect(mockEnvironmentsApi.remove).toHaveBeenCalledExactlyOnceWith("env-1");
expect(mockAgentsApi.update.mock.invocationCallOrder[0]).toBeLessThan(
mockEnvironmentsApi.remove.mock.invocationCallOrder[0],
);
await waitForAssertion(() => {
expect(getEnvironmentFormPage()).toBeNull();
});
});
it("deletes without any reassignment when no agents use the environment", async () => {
mockAgentsApi.list.mockResolvedValue([
{ id: "agent-2", name: "Miles", status: "active", defaultEnvironmentId: "env-2" },
]);
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(getEnvironmentFormPage()?.textContent).toContain("Edit environment");
});
await act(async () =>
click(document.body.querySelector("[data-testid='environment-delete-button']")),
);
await waitForAssertion(() => {
expect(
document.body.querySelector("[data-testid='environment-delete-dialog']")?.textContent,
).toContain("permanently deleted");
});
expect(
document.body.querySelector("[data-testid='environment-delete-reassign-select']"),
).toBeNull();
await act(async () =>
click(document.body.querySelector("[data-testid='environment-delete-confirm']")),
);
await flushReact();
expect(mockAgentsApi.update).not.toHaveBeenCalled();
expect(mockEnvironmentsApi.remove).toHaveBeenCalledExactlyOnceWith("env-1");
await waitForAssertion(() => {
expect(getEnvironmentFormPage()).toBeNull();
});
});
it("disables the delete confirmation while the server would block the delete", async () => {
mockEnvironmentsApi.deleteBlastRadius.mockResolvedValue(
createDeleteBlastRadius({
canDelete: false,
deleteBlockedReasons: ["instance_default"],
staticReferences: { isInstanceDefault: true },
}),
);
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(getEnvironmentFormPage()?.textContent).toContain("Edit environment");
});
await act(async () =>
click(document.body.querySelector("[data-testid='environment-delete-button']")),
);
await waitForAssertion(() => {
expect(
document.body.querySelector("[data-testid='environment-delete-dialog']")?.textContent,
).toContain("Cannot delete the current instance default environment");
});
const confirm = document.body.querySelector<HTMLButtonElement>(
"[data-testid='environment-delete-confirm']",
)!;
expect(confirm.disabled).toBe(true);
expect(
document.body.querySelector("[data-testid='environment-delete-reassign-select']"),
).toBeNull();
expect(mockEnvironmentsApi.remove).not.toHaveBeenCalled();
});
it("links the workspaces holding blocking sandbox leases in the delete dialog", async () => {
mockEnvironmentsApi.deleteBlastRadius.mockResolvedValue(
createDeleteBlastRadius({
canDelete: false,
deleteBlockedReasons: ["reusable_sandbox_lease"],
reusableSandboxLeaseCount: 3,
reusableSandboxLeaseHolders: [
{
leaseId: "lease-1",
executionWorkspaceId: "ws-1",
executionWorkspaceName: "ACME-7-fix-the-thing",
issueId: "issue-1",
issueIdentifier: "ACME-7",
issueTitle: "Fix the thing",
},
{
leaseId: "lease-2",
executionWorkspaceId: "ws-1",
executionWorkspaceName: "ACME-7-fix-the-thing",
issueId: "issue-2",
issueIdentifier: "ACME-9",
issueTitle: "Follow-up",
},
{
leaseId: "lease-3",
executionWorkspaceId: null,
executionWorkspaceName: null,
issueId: null,
issueIdentifier: null,
issueTitle: null,
},
],
}),
);
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(getEnvironmentFormPage()?.textContent).toContain("Edit environment");
});
await act(async () =>
click(document.body.querySelector("[data-testid='environment-delete-button']")),
);
await waitForAssertion(() => {
expect(
document.body.querySelector("[data-testid='environment-delete-lease-holders']"),
).not.toBeNull();
});
const holders = document.body.querySelector<HTMLElement>(
"[data-testid='environment-delete-lease-holders']",
)!;
const workspaceLink = holders.querySelector<HTMLAnchorElement>("a[href='/execution-workspaces/ws-1']");
expect(workspaceLink?.textContent).toBe("ACME-7-fix-the-thing");
// The two leases on ws-1 collapse into one row naming both issues.
expect(holders.querySelectorAll("li")).toHaveLength(2);
expect(holders.textContent).toContain("2 sandbox leases");
expect(holders.textContent).toContain("ACME-7, ACME-9");
expect(holders.textContent).toContain("Workspace no longer on record");
// Leases as the only blocker are consentable: the confirm stays enabled
// and destroys the sandboxes as part of the delete.
const confirm = document.body.querySelector<HTMLButtonElement>(
"[data-testid='environment-delete-confirm']",
)!;
expect(confirm.disabled).toBe(false);
expect(confirm.textContent?.trim()).toBe("Destroy 3 sandboxes and delete");
await act(async () => click(confirm));
await flushReact();
expect(mockEnvironmentsApi.remove).toHaveBeenCalledExactlyOnceWith("env-1", {
destroyReusableSandboxLeases: true,
});
await waitForAssertion(() => {
expect(getEnvironmentFormPage()).toBeNull();
});
});
it("does not offer delete on the add-environment page", async () => {
root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
await act(async () => {
root!.render(renderCompanyEnvironments(queryClient, `${ENVIRONMENTS_PATH}/new`));
});
await waitForAssertion(() => {
expect(getEnvironmentFormPage()?.textContent).toContain("Add environment");
});
expect(document.body.querySelector("[data-testid='environment-delete-button']")).toBeNull();
});
});