Fix ranges issue

This commit is contained in:
Reinier Criel 2025-10-24 13:14:57 -07:00
parent 15785fad73
commit 6b2db6dace
6 changed files with 128 additions and 4 deletions

View file

@ -43,3 +43,35 @@ export async function safeSpawn(command, args, options = {}) {
});
});
}
/**
* To avoid any regression issues on the JS ecosystem,
* a py-friendly safeSpawn that avoids shell interpolation
* issues (e.g., '<', '>' in version specs).
*
* TL;DR: add support for shell::false
*/
export async function safeSpawnPy(command, args, options = {}) {
return new Promise((resolve) => {
const child = spawn(command, args, { ...options, shell: false });
let stdout = "";
let stderr = "";
child.stdout?.on("data", (data) => {
stdout += data.toString();
});
child.stderr?.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
resolve({ status: code, stdout, stderr });
});
child.on("error", (error) => {
resolve({ status: 1, stdout: "", stderr: error.message || String(error) });
});
});
}

View file

@ -87,3 +87,73 @@ describe("safeSpawn", () => {
assert.strictEqual(spawnCalls[0].options.shell, true);
});
});
describe("safeSpawnPy", () => {
let safeSpawnPy;
let spawnCalls = [];
beforeEach(async () => {
spawnCalls = [];
// Mock child_process for argument-array spawn signature
mock.module("child_process", {
namedExports: {
spawn: (command, args = [], options = {}) => {
spawnCalls.push({ command, args, options });
const stdoutListeners = [];
const stderrListeners = [];
const stdout = { on: (event, cb) => { if (event === "data") stdoutListeners.push(cb); } };
const stderr = { on: (event, cb) => { if (event === "data") stderrListeners.push(cb); } };
const obj = {
stdout,
stderr,
on: (event, callback) => {
if (event === 'close') {
// Emit one chunk to stdout and stderr to verify piping works, then close with success
setTimeout(() => {
stdoutListeners.forEach((cb) => cb(Buffer.from("STDOUT-TEST")));
stderrListeners.forEach((cb) => cb(Buffer.from("")));
callback(0);
}, 0);
}
}
};
return obj;
},
},
});
// Import after mocking; use a query to avoid ESM cache collisions with previous import
const safeSpawnModule = await import("./safeSpawn.js?py");
safeSpawnPy = safeSpawnModule.safeSpawnPy;
});
afterEach(() => {
mock.reset();
});
it("spawns without a shell and preserves args (inherit)", async () => {
const result = await safeSpawnPy("pip3", ["install", "Jinja2>=3.1,<3.2"], { stdio: "inherit" });
// Verifies no throw and status 0
assert.strictEqual(result.status, 0);
// Verify spawn signature
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, "pip3");
assert.deepStrictEqual(spawnCalls[0].args, ["install", "Jinja2>=3.1,<3.2"]);
assert.strictEqual(spawnCalls[0].options.shell, false);
assert.strictEqual(spawnCalls[0].options.stdio, "inherit");
});
it("captures stdout when stdio=pipe", async () => {
const result = await safeSpawnPy("pip3", ["install", "idna!=3.5,>=3.0", "--dry-run"], { stdio: "pipe" });
assert.strictEqual(result.status, 0);
assert.match(result.stdout || "", /STDOUT-TEST/);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].options.shell, false);
assert.strictEqual(spawnCalls[0].options.stdio, "pipe");
});
});