diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index e1908c5f40b..cf0c144a1a3 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -1993,13 +1993,18 @@ describe("resolveShell (shell fallback)", () => { }); it("returns process.env.SHELL when set", () => { - process.env.SHELL = "/usr/bin/zsh"; - expect(resolveShell()).toBe("/usr/bin/zsh"); + process.env.SHELL = process.execPath; + expect(resolveShell()).toBe(process.execPath); }); it("trims whitespace from SHELL env var", () => { - process.env.SHELL = " /usr/bin/fish "; - expect(resolveShell()).toBe("/usr/bin/fish"); + process.env.SHELL = ` ${process.execPath} `; + expect(resolveShell()).toBe(process.execPath); + }); + + it("preserves non-absolute shell names so PATH lookup still works", () => { + process.env.SHELL = "zsh"; + expect(resolveShell()).toBe("zsh"); }); it("falls back to /bin/sh on non-Windows when SHELL is unset", () => { @@ -2031,6 +2036,12 @@ describe("resolveShell (shell fallback)", () => { Object.defineProperty(process, "platform", { value: "win32" }); expect(resolveShell()).toBe("sh"); }); + + it("falls back when SHELL points to a missing absolute path", () => { + process.env.SHELL = "/definitely/missing/zsh"; + Object.defineProperty(process, "platform", { value: "linux" }); + expect(resolveShell()).toBe("/bin/sh"); + }); }); describeEmbeddedPostgres("workspace runtime startup reconciliation", () => { diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index 846af64f340..dce5a258ba7 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -26,7 +26,11 @@ import { readExecutionWorkspaceConfig } from "./execution-workspaces.js"; import { readProjectWorkspaceRuntimeConfig } from "./project-workspace-runtime-config.js"; export function resolveShell(): string { - return process.env.SHELL?.trim() || (process.platform === "win32" ? "sh" : "/bin/sh"); + const fallback = process.platform === "win32" ? "sh" : "/bin/sh"; + const shell = process.env.SHELL?.trim(); + if (!shell) return fallback; + if (path.isAbsolute(shell) && !existsSync(shell)) return fallback; + return shell; } export interface ExecutionWorkspaceInput { @@ -1735,6 +1739,11 @@ async function startLocalRuntimeService(input: { detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], }); + const spawnErrorPromise = new Promise((_, reject) => { + child.once("error", (err) => { + reject(err); + }); + }); let stderrExcerpt = ""; let stdoutExcerpt = ""; child.stdout?.on("data", async (chunk) => { @@ -1749,7 +1758,10 @@ async function startLocalRuntimeService(input: { }); try { - await waitForReadiness({ service: input.service, url }); + await Promise.race([ + waitForReadiness({ service: input.service, url }), + spawnErrorPromise, + ]); } catch (err) { terminateChildProcess(child); throw new Error(