mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
* 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.
160 lines
5.0 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
});
|
|
});
|