mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:03:27 +01:00
Harden custom image setup terminal fallback
This commit is contained in:
1 parent
b97afa57aa
commit
f9188e1976
14 files changed
+1611
-48
No files matched your search
@@ -172,6 +172,27 @@ function createTemplate(overrides: Record<string, unknown> = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -601,6 +622,84 @@ describe("CompanyEnvironments — test provider button", () => {
|
||||
expect(getOpenDialog()?.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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<CompanyEnvironments />
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
|
||||
await act(async () => click(editButtons(container)[0]));
|
||||
await waitForAssertion(() => {
|
||||
expect(getOpenDialog()?.textContent).toContain("Setup connection details could not be refreshed.");
|
||||
});
|
||||
|
||||
const dialog = getOpenDialog()!;
|
||||
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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<CompanyEnvironments />
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
|
||||
await act(async () => click(editButtons(container)[0]));
|
||||
await waitForAssertion(() => {
|
||||
expect(getOpenDialog()?.textContent).toContain("Browser terminal is not available for this provider connection.");
|
||||
});
|
||||
|
||||
const dialog = getOpenDialog()!;
|
||||
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 } } });
|
||||
|
||||
@@ -199,6 +199,27 @@ function readConnectionCommand(payload: EnvironmentCustomImageConnectionPayload
|
||||
: null;
|
||||
}
|
||||
|
||||
function setupConnectionFallbackMessage(input: {
|
||||
payload: EnvironmentCustomImageConnectionPayload | null;
|
||||
refreshError: unknown;
|
||||
isLoading: boolean;
|
||||
}): string | null {
|
||||
if (input.refreshError) {
|
||||
return "Setup connection details could not be refreshed. You can still finish or cancel this setup.";
|
||||
}
|
||||
if (input.isLoading) return null;
|
||||
if (!input.payload) {
|
||||
return "Connection details are not available yet. You can still finish or cancel this setup.";
|
||||
}
|
||||
if (input.payload.type !== "ssh") {
|
||||
return "Browser terminal is not available for this provider connection. Use the provider setup instructions, then finish or cancel here.";
|
||||
}
|
||||
if (!readConnectionCommand(input.payload)) {
|
||||
return "Connection details are not available yet. You can still finish or cancel this setup.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function capabilityState(capability: EnvironmentProviderCapability | null | undefined) {
|
||||
if (!capability || capability.status !== "supported" || !capability.supportsInteractiveSetup) {
|
||||
return {
|
||||
@@ -451,6 +472,13 @@ function EnvironmentImageTemplatePanel({
|
||||
? sessionQuery.data?.connectionPayload ?? null
|
||||
: null;
|
||||
const connectionCommand = readConnectionCommand(connectionPayload);
|
||||
const connectionFallbackMessage = session?.status === "waiting_for_user"
|
||||
? setupConnectionFallbackMessage({
|
||||
payload: connectionPayload,
|
||||
refreshError: sessionQuery.isError ? sessionQuery.error : null,
|
||||
isLoading: sessionQuery.isLoading,
|
||||
})
|
||||
: null;
|
||||
const sessionExpiresAt = formatDateTime(connectionPayload?.expiresAt ?? session?.expiresAt ?? null);
|
||||
const capturedAt = formatDateTime(activeTemplate?.capturedAt ?? activeTemplate?.createdAt ?? null);
|
||||
const lastUsedAt = formatDateTime(activeTemplate?.lastUsedAt ?? null);
|
||||
@@ -500,9 +528,9 @@ function EnvironmentImageTemplatePanel({
|
||||
</code>
|
||||
</div>
|
||||
) : null}
|
||||
{session.status === "waiting_for_user" && !connectionCommand ? (
|
||||
{session.status === "waiting_for_user" && connectionFallbackMessage ? (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
Connection details are not available yet.
|
||||
{connectionFallbackMessage}
|
||||
</div>
|
||||
) : null}
|
||||
{session.failureReason ? (
|
||||
|
||||
Reference in new issue
Block a user