diff --git a/server/src/__tests__/environment-custom-images-service.test.ts b/server/src/__tests__/environment-custom-images-service.test.ts index ae0e10dd8bf..86c8d487d0e 100644 --- a/server/src/__tests__/environment-custom-images-service.test.ts +++ b/server/src/__tests__/environment-custom-images-service.test.ts @@ -459,6 +459,43 @@ describeEmbeddedPostgres("environmentCustomImageService", () => { }); }); + it("does not rewrite generic missing image setup failures as template conflicts", async () => { + const { environmentId } = await seed(); + const workerManager = createWorkerManager(); + workerManager.call.mockImplementation(async (_pluginId, method) => { + if (method === "environmentStartInteractiveSetup") { + throw new Error("Base image not found in registry"); + } + 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-valid", + status: "active", + }).returning(); + + await expect(service.startSetupSession({ + environmentId, + templateId: template!.id, + actor: { userId: "user-1" }, + })).rejects.toThrow("Base image not found in registry"); + + 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: "Base image not found in registry", + }); + }); + 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]!); diff --git a/server/src/services/environment-custom-images.ts b/server/src/services/environment-custom-images.ts index f27bf8a1ffb..4191a64a929 100644 --- a/server/src/services/environment-custom-images.ts +++ b/server/src/services/environment-custom-images.ts @@ -192,7 +192,7 @@ function providerErrorMessage(error: unknown): string { function isProviderMissingTemplateError(error: unknown): boolean { const message = providerErrorMessage(error).toLowerCase(); return ( - (message.includes("snapshot") || message.includes("template") || message.includes("image")) + (message.includes("snapshot") || message.includes("template")) && (message.includes("not found") || message.includes("missing")) ); } diff --git a/ui/src/pages/CompanyEnvironments.test.tsx b/ui/src/pages/CompanyEnvironments.test.tsx index 4b576c9c522..734e30b7985 100644 --- a/ui/src/pages/CompanyEnvironments.test.tsx +++ b/ui/src/pages/CompanyEnvironments.test.tsx @@ -531,6 +531,50 @@ describe("CompanyEnvironments — test provider button", () => { expect(container.textContent).toContain("Test probes clean up the validation sandbox after the check"); }); + 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( + + + + + , + ); + }); + 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 } } }); diff --git a/ui/src/pages/CompanyEnvironments.tsx b/ui/src/pages/CompanyEnvironments.tsx index 6229d9b2820..c601a393900 100644 --- a/ui/src/pages/CompanyEnvironments.tsx +++ b/ui/src/pages/CompanyEnvironments.tsx @@ -215,7 +215,7 @@ function readStringValue(value: unknown): string | null { } function sandboxProbeLifecycleNote(probe: EnvironmentProbeResult): string | null { - if (probe.driver !== "sandbox") return null; + if (probe.driver !== "sandbox" || !probe.ok) return null; const metadata = readProbeMetadata(probe); const provider = readStringValue(metadata.provider) ?? readStringValue(readRecord(probe.details)?.provider) ?? "sandbox"; const sandboxName = readStringValue(metadata.sandboxName);