Files
reverse-engineering-anything/src/domain/javascript/childProcessOptionsRegression.test.ts
T
吴佳浩Alben f1da7f7670 fix(javascript): read the spawn env option as Node reads it (#7713)
* fix(javascript): read spawn env as Node reads it

Node's normalizeSpawnArguments starts with `const env = options.env ||
{ ...process.env }`, so a falsy env is replaced by a copy of the parent
environment. A child started with env: null, undefined, 0, "" or false
therefore inherits everything, exactly as if no env were passed.

REA tested the key's presence alone, so every one of those reported
environment_supplied: true and the graph emitted a resolved supplies-env
relation. The presence test also missed the opposite direction: an env
supplied through an inline object spread reported false although Node uses
it.

Read the declared value the way stdioMode already reads stdio: an explicit
property whose statically known value is falsy does not supply an
environment, and an inline spread that carries env does.

Verified on Node v24.22.1 with a child that prints a parent marker variable:
no options, env: null, undefined, void 0, 0, "" and false all print the
marker, while env: {} prints nothing and env: { A: "1" } prints only that.

* fix(javascript): read falsy numeric literals as Node does

greptile flagged that environmentSupplied still reported a supplied environment for
`env: -0` and `env: 0n`: staticJavaScriptTruthiness recognized neither form, so it
returned undefined and `!== false` turned that into true. Node's
normalizeSpawnArguments uses `options.env || { ...process.env }`, so both inherit.

Extend the shared truthiness reader to cover a BigInt literal and a negated numeric
literal, and add regression cases for `-0`, `0n` and `0.0`.

The reader is shared with the route scan and the state tracker; both treat
`undefined` as "unknown", so a more precise `false` only strengthens them.
2026-10-29 07:45:22 +08:00

160 lines
5.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
describe("child-process options detection", () => {
it("detects options without an args array", () => {
const ir = analyzeJavaScriptSemantics(`
import { spawn, execFile, fork } from "node:child_process";
spawn("tool", {
env: { A: "1" },
stdio: "pipe"
});
execFile("tool", {
env: { A: "1" }
});
fork("worker.js", {
env: { A: "1" },
stdio: ["pipe", "pipe", "pipe", "ipc"]
});
`);
expect(
ir.childProcessSpawns.map(
({ method, environmentSupplied, stdioMode, resolution }) => ({
method,
environmentSupplied,
stdioMode,
resolution,
}),
),
).toEqual([
{
method: "spawn",
environmentSupplied: true,
stdioMode: "pipe",
resolution: "complete",
},
{
method: "execFile",
environmentSupplied: true,
stdioMode: "default",
resolution: "complete",
},
{
method: "fork",
environmentSupplied: true,
stdioMode: "array",
resolution: "complete",
},
]);
});
it("reads fork's silent option when stdio is not a string or array", () => {
const ir = analyzeJavaScriptSemantics(`
import { fork, spawn } from "node:child_process";
fork("piped.js", { silent: true });
fork("inherited.js", [], { silent: false });
fork("nullish-stdio.js", { silent: 1, stdio: null });
fork("explicit-stdio.js", { silent: true, stdio: "inherit" });
fork("dynamic.js", { silent: process.env.QUIET });
spawn("tool", { silent: true });
fork("boolean-stdio.js", { stdio: false, silent: true });
fork("object-stdio.js", { stdio: {}, silent: false });
fork("unresolved-stdio.js", { stdio: streams, silent: true });
spawn("tool", { stdio: false });
`);
expect(ir.childProcessSpawns.map(({ stdioMode }) => stdioMode)).toEqual([
"pipe",
"default",
"pipe",
"inherit",
"dynamic",
"default",
"pipe",
"default",
"dynamic",
"dynamic",
]);
});
it("preserves behavior with explicit args arrays", () => {
const ir = analyzeJavaScriptSemantics(`
import { spawn, execFile, fork } from "node:child_process";
spawn("tool", [], { env: { A: "1" }, stdio: "pipe" });
execFile("tool", [], { env: { A: "1" } });
fork("worker.js", [], {
env: { A: "1" },
stdio: ["pipe", "pipe", "pipe", "ipc"]
});
`);
expect(
ir.childProcessSpawns.map(({ environmentSupplied, stdioMode }) => ({
environmentSupplied,
stdioMode,
})),
).toEqual([
{ environmentSupplied: true, stdioMode: "pipe" },
{ environmentSupplied: true, stdioMode: "default" },
{ environmentSupplied: true, stdioMode: "array" },
]);
});
it("does not invent options when omitted", () => {
const ir = analyzeJavaScriptSemantics(`
import { spawn, execFile, fork } from "node:child_process";
spawn("tool");
execFile("tool");
fork("worker.js");
`);
expect(
ir.childProcessSpawns.every(
({ environmentSupplied, stdioMode }) =>
environmentSupplied === false && stdioMode === "default",
),
).toBe(true);
expect(ir.childProcessSpawns).toHaveLength(3);
});
it("reads env as Node does, where a falsy value inherits the parent environment", () => {
// Node's normalizeSpawnArguments does `const env = options.env || { ...process.env }`,
// so a nullish or otherwise falsy env supplies nothing and the child inherits.
const cases = [
["absent", `spawn("tool")`, false],
["null", `spawn("tool", [], { env: null })`, false],
["undefined", `spawn("tool", [], { env: undefined })`, false],
["void", `spawn("tool", [], { env: void 0 })`, false],
["zero", `spawn("tool", [], { env: 0 })`, false],
["negative-zero", `spawn("tool", [], { env: -0 })`, false],
["bigint-zero", `spawn("tool", [], { env: 0n })`, false],
["zero-float", `spawn("tool", [], { env: 0.0 })`, false],
["nan", `spawn("tool", [], { env: NaN })`, true],
["empty-string", `spawn("tool", [], { env: "" })`, false],
["false", `spawn("tool", [], { env: false })`, false],
["empty-object", `spawn("tool", [], { env: {} })`, true],
["with-key", `spawn("tool", [], { env: { A: "1" } })`, true],
["inline-spread", `spawn("tool", [], { ...{ env: { A: "1" } } })`, true],
["unresolved-value", `spawn("tool", [], { env: input })`, true],
["unresolved-options", `spawn("tool", [], input)`, false],
] as const;
for (const [label, call, supplied] of cases) {
const ir = analyzeJavaScriptSemantics(
`import { spawn } from "node:child_process"; ${call};`,
);
expect(ir.childProcessSpawns[0]?.environmentSupplied, label).toBe(
supplied,
);
}
});
});