fix(environments): pass company context to sandbox probes

This commit is contained in:
Devin Foley committed 2026-07-09 16:19:09 -07:00
1 parent 4d898aa7af
commit 6aed879611
5 files changed
+148 -4

No files matched your search

@@ -419,6 +419,46 @@ describeEmbeddedPostgres("environmentCustomImageService", () => {
})).rejects.toThrow("Setup template must be the active template");
});
it("turns missing provider template setup failures into conflicts", async () => {
const { environmentId } = await seed();
const workerManager = createWorkerManager();
workerManager.call.mockImplementation(async (_pluginId, method) => {
if (method === "environmentStartInteractiveSetup") {
throw new Error("Snapshot not found");
}
throw new Error(`Unexpected plugin call: ${method}`);
});
const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager });
const [template] = await db.insert(environmentCustomImageTemplates).values({
environmentId,
provider: "fake-plugin",
templateKind: "snapshot",
templateRef: "snapshot-missing",
status: "active",
}).returning();
await expect(service.startSetupSession({
environmentId,
templateId: template!.id,
actor: { userId: "user-1" },
})).rejects.toMatchObject({
status: 409,
message: expect.stringContaining("customImage template no longer exists"),
});
const [session] = await db
.select({
status: environmentCustomImageSetupSessions.status,
failureReason: environmentCustomImageSetupSessions.failureReason,
})
.from(environmentCustomImageSetupSessions)
.where(eq(environmentCustomImageSetupSessions.templateId, template!.id));
expect(session).toEqual({
status: "failed",
failureReason: "Selected provider template is missing.",
});
});
it("applies the active template regardless of base-config changes and falls back when none exists", async () => {
const { companyId, environmentId } = await seed();
const environment = await db.select().from(environments).where(eq(environments.id, environmentId)).then((rows) => rows[0]!);
@@ -185,6 +185,24 @@ function isActiveSetupStatus(status: EnvironmentCustomImageSetupSessionStatus):
return (ACTIVE_SETUP_STATUSES as readonly string[]).includes(status);
}
function providerErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function isProviderMissingTemplateError(error: unknown): boolean {
const message = providerErrorMessage(error).toLowerCase();
return (
(message.includes("snapshot") || message.includes("template") || message.includes("image"))
&& (message.includes("not found") || message.includes("missing"))
);
}
function missingProviderTemplateConflict() {
return conflict(
"The selected environment customImage template no longer exists in the sandbox provider. Disable this custom image or roll back to a previous template, then start image setup again.",
);
}
function templateConfigBindingFromDriver(input: {
templateRefKind?: string | null | undefined;
templateConfigBinding?: unknown;
@@ -700,11 +718,17 @@ export function environmentCustomImageService(
connectionPayload: providerSession.connectionPayload ?? null,
};
} catch (error) {
const failureReason = selectedTemplate && isProviderMissingTemplateError(error)
? "Selected provider template is missing."
: providerErrorMessage(error);
await markSessionStatus({
sessionId,
status: "failed",
failureReason: error instanceof Error ? error.message : String(error),
failureReason,
});
if (selectedTemplate && isProviderMissingTemplateError(error)) {
throw missingProviderTemplateConflict();
}
throw error;
}
},
+4 -1
View File
@@ -65,7 +65,10 @@ export const environmentsApi = {
config?: Record<string, unknown>;
metadata?: Record<string, unknown> | null;
}) => api.patch<Environment>(`/environments/${environmentId}`, body),
probe: (environmentId: string) => api.post<EnvironmentProbeResult>(`/environments/${environmentId}/probe`, {}),
probe: (environmentId: string, companyId?: string | null) => {
const query = companyId ? `?companyId=${encodeURIComponent(companyId)}` : "";
return api.post<EnvironmentProbeResult>(`/environments/${environmentId}/probe${query}`, {});
},
probeConfig: (companyId: string, body: {
name?: string;
driver: "local" | "ssh" | "sandbox" | "plugin";
+44 -1
View File
@@ -485,7 +485,50 @@ describe("CompanyEnvironments — test provider button", () => {
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");
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(
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<CompanyEnvironments />
</TooltipProvider>
</QueryClientProvider>,
);
});
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("Verified temporary daytona sandbox paperclip-probe (473167E9).");
expect(container.textContent).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 () => {
+35 -1
View File
@@ -199,6 +199,36 @@ function readEnvironmentSandboxProvider(environment: Environment): string | null
: null;
}
function readRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function readProbeMetadata(probe: EnvironmentProbeResult): Record<string, unknown> {
const details = readRecord(probe.details);
return readRecord(details?.metadata) ?? {};
}
function readStringValue(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function sandboxProbeLifecycleNote(probe: EnvironmentProbeResult): string | null {
if (probe.driver !== "sandbox") return null;
const metadata = readProbeMetadata(probe);
const provider = readStringValue(metadata.provider) ?? readStringValue(readRecord(probe.details)?.provider) ?? "sandbox";
const sandboxName = readStringValue(metadata.sandboxName);
const sandboxId = readStringValue(metadata.sandboxId);
const sandboxLabel = sandboxName && sandboxId
? `${sandboxName} (${sandboxId})`
: sandboxName ?? sandboxId;
const base = sandboxLabel
? `Verified temporary ${provider} sandbox ${sandboxLabel}.`
: `Verified a temporary ${provider} sandbox.`;
return `${base} Test probes clean up the validation sandbox after the check, so it may not remain visible in the provider dashboard.`;
}
function formatDateTime(value: string | Date | null | undefined): string | null {
if (!value) return null;
const date = value instanceof Date ? value : new Date(value);
@@ -1150,7 +1180,7 @@ export function CompanyEnvironments() {
});
const environmentProbeMutation = useMutation({
mutationFn: async (environmentId: string) => await environmentsApi.probe(environmentId),
mutationFn: async (environmentId: string) => await environmentsApi.probe(environmentId, selectedCompanyId),
onMutate: (environmentId) => {
setTestingEnvironmentId(environmentId);
},
@@ -1409,6 +1439,7 @@ export function CompanyEnvironments() {
</div>
{savedEnvironments.map((environment) => {
const probe = probeResults[environment.id] ?? null;
const sandboxLifecycleNote = probe ? sandboxProbeLifecycleNote(probe) : null;
const isEditing = editingEnvironmentId === environment.id;
const sandboxProvider = readEnvironmentSandboxProvider(environment);
const sandboxProviderCapability = sandboxProvider
@@ -1478,6 +1509,9 @@ export function CompanyEnvironments() {
}
>
<div className="font-medium">{probe.summary}</div>
{sandboxLifecycleNote ? (
<div className="mt-1 text-muted-foreground">{sandboxLifecycleNote}</div>
) : null}
{probe.details?.error && typeof probe.details.error === "string" ? (
<div className="mt-1 font-mono text-(length:--text-micro)">{probe.details.error}</div>
) : null}