diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index 3f93ccf6..8bf008bd 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -24,6 +24,7 @@ import { realizeExecutionWorkspace, releaseRuntimeServicesForRun, resetRuntimeServicesForTests, + resolveShell, sanitizeRuntimeServiceBaseEnv, stopRuntimeServicesForExecutionWorkspace, type RealizedExecutionWorkspace, @@ -1345,6 +1346,60 @@ describe("ensureRuntimeServicesForRun", () => { }); }); +describe("resolveShell (shell fallback)", () => { + const originalShell = process.env.SHELL; + const originalPlatform = process.platform; + + afterEach(() => { + if (originalShell !== undefined) { + process.env.SHELL = originalShell; + } else { + delete process.env.SHELL; + } + Object.defineProperty(process, "platform", { value: originalPlatform }); + }); + + it("returns process.env.SHELL when set", () => { + process.env.SHELL = "/usr/bin/zsh"; + expect(resolveShell()).toBe("/usr/bin/zsh"); + }); + + it("trims whitespace from SHELL env var", () => { + process.env.SHELL = " /usr/bin/fish "; + expect(resolveShell()).toBe("/usr/bin/fish"); + }); + + it("falls back to /bin/sh on non-Windows when SHELL is unset", () => { + delete process.env.SHELL; + Object.defineProperty(process, "platform", { value: "linux" }); + expect(resolveShell()).toBe("/bin/sh"); + }); + + it("falls back to sh (bare) on Windows when SHELL is unset", () => { + delete process.env.SHELL; + Object.defineProperty(process, "platform", { value: "win32" }); + expect(resolveShell()).toBe("sh"); + }); + + it("falls back to /bin/sh on darwin when SHELL is unset", () => { + delete process.env.SHELL; + Object.defineProperty(process, "platform", { value: "darwin" }); + expect(resolveShell()).toBe("/bin/sh"); + }); + + it("treats empty SHELL as unset and uses platform fallback", () => { + process.env.SHELL = ""; + Object.defineProperty(process, "platform", { value: "linux" }); + expect(resolveShell()).toBe("/bin/sh"); + }); + + it("treats whitespace-only SHELL as unset and uses platform fallback", () => { + process.env.SHELL = " "; + Object.defineProperty(process, "platform", { value: "win32" }); + expect(resolveShell()).toBe("sh"); + }); +}); + describeEmbeddedPostgres("workspace runtime startup reconciliation", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index f191ece4..a100242e 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -24,6 +24,10 @@ import type { WorkspaceOperationRecorder } from "./workspace-operations.js"; 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"); +} + export interface ExecutionWorkspaceInput { baseCwd: string; source: "project_primary" | "task_session" | "agent_home"; @@ -379,7 +383,7 @@ async function runWorkspaceCommand(input: { env: NodeJS.ProcessEnv; label: string; }) { - const shell = process.env.SHELL?.trim() || "/bin/sh"; + const shell = resolveShell(); const proc = await executeProcess({ command: shell, args: ["-c", input.command], @@ -475,7 +479,7 @@ async function recordWorkspaceCommandOperation( cwd: input.cwd, metadata: input.metadata ?? null, run: async () => { - const shell = process.env.SHELL?.trim() || "/bin/sh"; + const shell = resolveShell(); const result = await executeProcess({ command: shell, args: ["-c", input.command], @@ -1285,6 +1289,7 @@ async function startLocalRuntimeService(input: { const portEnvKey = asString(portConfig.envKey, "PORT"); env[portEnvKey] = String(port); } + const expose = parseObject(input.service.expose); const readiness = parseObject(input.service.readiness); const urlTemplate = @@ -1359,7 +1364,8 @@ async function startLocalRuntimeService(input: { ); } } - const shell = process.env.SHELL?.trim() || "/bin/sh"; + + const shell = resolveShell(); const child = spawn(shell, ["-lc", command], { cwd: serviceCwd, env,