fix(qa-lab): bound Node binary lookup (#109428)

This commit is contained in:
Alix-007
2026-07-18 16:35:16 +08:00
committed by GitHub
parent 764999c755
commit cc7bd4d95a
2 changed files with 43 additions and 2 deletions
+34 -1
View File
@@ -1,6 +1,6 @@
// Qa Lab tests cover node exec plugin behavior.
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { runExecMock } = vi.hoisted(() => ({ runExecMock: vi.fn() }));
@@ -13,6 +13,10 @@ describe("resolveQaNodeExecPath", () => {
runExecMock.mockReset();
});
afterEach(() => {
vi.useRealTimers();
});
it("reuses the current exec path when already running under Node", async () => {
await expect(
resolveQaNodeExecPath({
@@ -66,6 +70,7 @@ describe("resolveQaNodeExecPath", () => {
expect(runExecMock).toHaveBeenCalledWith("which", ["node"], {
baseEnv: env,
logOutput: false,
timeoutMs: 5_000,
});
});
@@ -82,6 +87,7 @@ describe("resolveQaNodeExecPath", () => {
expect(options).toEqual({
encoding: "utf8",
env: { SystemRoot: String.raw`D:\Windows` },
timeoutMs: 5_000,
});
return {
stdout: String.raw`D:\nodejs\node.exe` + "\r\n",
@@ -92,6 +98,33 @@ describe("resolveQaNodeExecPath", () => {
).resolves.toBe(String.raw`D:\nodejs\node.exe`);
});
it("fails after the lookup timeout when the PATH probe stalls", async () => {
vi.useFakeTimers();
runExecMock.mockImplementationOnce(
(_file: string, _args: string[], options: { timeoutMs: number }): Promise<never> =>
new Promise((_resolve, reject) => {
setTimeout(() => reject(new Error("timed out")), options.timeoutMs);
}),
);
const lookup = resolveQaNodeExecPath({
execPath: "/opt/homebrew/bin/bun",
platform: "darwin",
versions: { ...process.versions, bun: "1.2.3" },
});
const rejection = lookup.catch((error: unknown) => error);
await vi.advanceTimersByTimeAsync(5_000);
await expect(rejection).resolves.toEqual(
expect.objectContaining({ message: expect.stringContaining("Node not found in PATH") }),
);
expect(runExecMock).toHaveBeenCalledWith("which", ["node"], {
baseEnv: undefined,
logOutput: false,
timeoutMs: 5_000,
});
});
it("throws a clear error when node is unavailable", async () => {
await expect(
resolveQaNodeExecPath({
+9 -1
View File
@@ -9,11 +9,18 @@ type ExecFileAsync = (
options: {
encoding: "utf8";
env?: NodeJS.ProcessEnv;
timeoutMs: number;
},
) => Promise<{ stdout: string; stderr: string }>;
const NODE_BINARY_LOOKUP_TIMEOUT_MS = 5_000;
const execFileAsync: ExecFileAsync = async (file, args, options) =>
await runExec(file, [...args], { baseEnv: options.env, logOutput: false });
await runExec(file, [...args], {
baseEnv: options.env,
logOutput: false,
timeoutMs: options.timeoutMs,
});
function isNodeExecPath(execPath: string, platform: NodeJS.Platform): boolean {
const pathModule = platform === "win32" ? path.win32 : path.posix;
@@ -48,6 +55,7 @@ export async function resolveQaNodeExecPath(params?: {
({ stdout } = await execFileImpl(locator, ["node"], {
encoding: "utf8",
env: params?.env,
timeoutMs: NODE_BINARY_LOOKUP_TIMEOUT_MS,
}));
} catch {
throw new Error(