From 234e07fcc0f2577f2a6967f13470e8700f3ff765 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 14:02:35 +0000 Subject: [PATCH] refactor(process): extract command env resolution helper --- src/process/exec.test.ts | 44 +++++++++++++++++++++++----------------- src/process/exec.ts | 32 +++++++++++++++++++---------- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/process/exec.test.ts b/src/process/exec.test.ts index d8d621abc56..d3e9e9dde6b 100644 --- a/src/process/exec.test.ts +++ b/src/process/exec.test.ts @@ -2,9 +2,8 @@ import type { ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; import process from "node:process"; import { describe, expect, it, vi } from "vitest"; -import { withEnvAsync } from "../test-utils/env.js"; import { attachChildProcessBridge } from "./child-process-bridge.js"; -import { runCommandWithTimeout, shouldSpawnWithShell } from "./exec.js"; +import { resolveCommandEnv, runCommandWithTimeout, shouldSpawnWithShell } from "./exec.js"; describe("runCommandWithTimeout", () => { it("never enables shell execution (Windows cmd.exe injection hardening)", () => { @@ -16,24 +15,31 @@ describe("runCommandWithTimeout", () => { ).toBe(false); }); - it("merges custom env with process.env", async () => { - await withEnvAsync({ OPENCLAW_BASE_ENV: "base" }, async () => { - const result = await runCommandWithTimeout( - [ - process.execPath, - "-e", - 'process.stdout.write((process.env.OPENCLAW_BASE_ENV ?? "") + "|" + (process.env.OPENCLAW_TEST_ENV ?? ""))', - ], - { - timeoutMs: 80, - env: { OPENCLAW_TEST_ENV: "ok" }, - }, - ); - - expect(result.code).toBe(0); - expect(result.stdout).toBe("base|ok"); - expect(result.termination).toBe("exit"); + it("merges custom env with base env and drops undefined values", async () => { + const resolved = resolveCommandEnv({ + argv: ["node", "script.js"], + baseEnv: { + OPENCLAW_BASE_ENV: "base", + OPENCLAW_TO_REMOVE: undefined, + }, + env: { + OPENCLAW_TEST_ENV: "ok", + }, }); + + expect(resolved.OPENCLAW_BASE_ENV).toBe("base"); + expect(resolved.OPENCLAW_TEST_ENV).toBe("ok"); + expect(resolved.OPENCLAW_TO_REMOVE).toBeUndefined(); + }); + + it("suppresses npm fund prompts for npm argv", async () => { + const resolved = resolveCommandEnv({ + argv: ["npm", "--version"], + baseEnv: {}, + }); + + expect(resolved.NPM_CONFIG_FUND).toBe("false"); + expect(resolved.npm_config_fund).toBe("false"); }); it("kills command when no output timeout elapses", async () => { diff --git a/src/process/exec.ts b/src/process/exec.ts index d3801e486af..ef6b707fbe6 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -174,16 +174,13 @@ export type CommandOptions = { noOutputTimeoutMs?: number; }; -export async function runCommandWithTimeout( - argv: string[], - optionsOrTimeout: number | CommandOptions, -): Promise { - const options: CommandOptions = - typeof optionsOrTimeout === "number" ? { timeoutMs: optionsOrTimeout } : optionsOrTimeout; - const { timeoutMs, cwd, input, env, noOutputTimeoutMs } = options; - const { windowsVerbatimArguments } = options; - const hasInput = input !== undefined; - +export function resolveCommandEnv(params: { + argv: string[]; + env?: NodeJS.ProcessEnv; + baseEnv?: NodeJS.ProcessEnv; +}): NodeJS.ProcessEnv { + const baseEnv = params.baseEnv ?? process.env; + const argv = params.argv; const shouldSuppressNpmFund = (() => { const cmd = path.basename(argv[0] ?? ""); if (cmd === "npm" || cmd === "npm.cmd" || cmd === "npm.exe") { @@ -196,7 +193,7 @@ export async function runCommandWithTimeout( return false; })(); - const mergedEnv = env ? { ...process.env, ...env } : { ...process.env }; + const mergedEnv = params.env ? { ...baseEnv, ...params.env } : { ...baseEnv }; const resolvedEnv = Object.fromEntries( Object.entries(mergedEnv) .filter(([, value]) => value !== undefined) @@ -210,6 +207,19 @@ export async function runCommandWithTimeout( resolvedEnv.npm_config_fund = "false"; } } + return resolvedEnv; +} + +export async function runCommandWithTimeout( + argv: string[], + optionsOrTimeout: number | CommandOptions, +): Promise { + const options: CommandOptions = + typeof optionsOrTimeout === "number" ? { timeoutMs: optionsOrTimeout } : optionsOrTimeout; + const { timeoutMs, cwd, input, env, noOutputTimeoutMs } = options; + const { windowsVerbatimArguments } = options; + const hasInput = input !== undefined; + const resolvedEnv = resolveCommandEnv({ argv, env }); const stdio = resolveCommandStdio({ hasInput, preferInherit: true }); const finalArgv = process.platform === "win32" ? (resolveNpmArgvForWindows(argv) ?? argv) : argv;