openclaw / src /process /exec.windows.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
3144483 verified
Raw
History Blame Contribute Delete
29.1 kB
// Windows exec tests cover trusted command wrapping, tree termination, and output decoding.
import { EventEmitter } from "node:events";
import fs from "node:fs";
import path from "node:path";
import { PassThrough } from "node:stream";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { withTempDir } from "../test-utils/temp-dir.js";
import { withMockedWindowsPlatform } from "../test-utils/vitest-spies.js";
const execaMock = vi.fn();
const isRegularFileMock = vi.fn();
const resolveExecutableFromPathEnvMock = vi.fn();
const resolveExecutablePathCandidateMock = vi.fn();
const spawnSyncMock = vi.fn();
type MockResult = {
cause?: unknown;
code?: string;
exitCode?: number;
failed: boolean;
isCanceled?: boolean;
isMaxBuffer?: boolean;
isTerminated: boolean;
signal?: NodeJS.Signals;
stderr?: Buffer;
stdout?: Buffer;
timedOut?: boolean;
};
type MockSubprocess = EventEmitter & {
nodeChildProcess: MockSubprocess;
exitCode: number | null;
finish: (result?: Partial<MockResult>) => void;
kill: ReturnType<typeof vi.fn>;
killed: boolean;
pid: number;
signalCode: NodeJS.Signals | null;
stderr: PassThrough;
stdout: PassThrough;
catch: Promise<MockResult>["catch"];
finally: Promise<MockResult>["finally"];
then: Promise<MockResult>["then"];
};
type ExecaCall = [string, string[], Record<string, unknown>];
function createMockSubprocess(params?: {
autoFinish?: boolean;
exitCode?: number;
reject?: boolean;
signal?: NodeJS.Signals;
stderr?: Buffer;
stderrChunks?: Buffer[];
stdout?: Buffer;
stdoutChunks?: Buffer[];
}): MockSubprocess {
const child = new EventEmitter() as MockSubprocess;
child.nodeChildProcess = child;
child.pid = 1234;
child.exitCode = null;
child.signalCode = null;
child.killed = false;
child.stdout = new PassThrough();
child.stderr = new PassThrough();
child.kill = vi.fn(() => {
child.killed = true;
return true;
});
let resolve!: (result: MockResult) => void;
let reject!: (error: Error) => void;
const completion = new Promise<MockResult>((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
// oxlint-disable-next-line unicorn/no-thenable -- Stub combines Execa's promise with its exposed Node child.
child.then = completion.then.bind(completion);
child.catch = completion.catch.bind(completion);
child.finally = completion.finally.bind(completion);
child.finish = (overrides = {}) => {
for (const chunk of params?.stdoutChunks ?? []) {
child.stdout.write(chunk);
}
for (const chunk of params?.stderrChunks ?? []) {
child.stderr.write(chunk);
}
const exitCode = Object.hasOwn(overrides, "exitCode")
? overrides.exitCode
: (params?.exitCode ?? 0);
const signal = overrides.signal ?? params?.signal;
child.exitCode = signal ? null : (exitCode ?? null);
child.signalCode = signal ?? null;
child.emit("exit", child.exitCode, child.signalCode);
const result = {
exitCode: signal ? undefined : exitCode,
failed: signal !== undefined || exitCode !== 0,
isTerminated: signal !== undefined,
signal,
stderr: params?.stderr ?? Buffer.concat(params?.stderrChunks ?? []),
stdout: params?.stdout ?? Buffer.concat(params?.stdoutChunks ?? []),
...overrides,
};
if (params?.reject) {
reject(Object.assign(new Error("command failed"), result));
} else {
resolve(result);
}
};
if (params?.autoFinish !== false) {
queueMicrotask(() => child.finish());
}
return child;
}
function requireExecaCall(index: number): ExecaCall {
const call = execaMock.mock.calls[index];
if (!call) {
throw new Error(`expected execa call ${index}`);
}
return call as ExecaCall;
}
function expectedTrustedCmdExe(): string {
return path.win32.join(getWindowsInstallRoots().systemRoot, "System32", "cmd.exe");
}
function expectCmdWrappedInvocation(call: ExecaCall, commandFragment = "pnpm.cmd") {
expect(call[0]).toBe(expectedTrustedCmdExe());
expect(call[1].slice(0, 3)).toEqual(["/d", "/s", "/c"]);
expect(call[1][3]).toContain(commandFragment);
expect(call[1][3]).toContain("--version");
expect(call[2]).toMatchObject({
shell: false,
windowsHide: true,
windowsVerbatimArguments: true,
});
}
let runCommandWithTimeout: typeof import("./exec.js").runCommandWithTimeout;
let runCommandBuffered: typeof import("./exec.js").runCommandBuffered;
let runCommandBuffersWithTimeout: typeof import("./exec-runner.js").runCommandBuffersWithTimeout;
let runUtf8CommandWithTimeout: typeof import("./exec.js").runUtf8CommandWithTimeout;
let runExec: typeof import("./exec.js").runExec;
let spawnCommand: typeof import("./exec.js").spawnCommand;
let withCommandProcessScope: typeof import("./exec-spawn.js").withCommandProcessScope;
let getWindowsInstallRoots: typeof import("../infra/windows-install-roots.js").getWindowsInstallRoots;
let getWindowsSystem32ExePath: typeof import("../infra/windows-install-roots.js").getWindowsSystem32ExePath;
describe("Windows command execution", () => {
beforeEach(async () => {
vi.resetModules();
const accessSync = fs.accessSync.bind(fs);
vi.spyOn(fs, "accessSync").mockImplementation((filePath, mode) => {
if (String(filePath).toLowerCase() === "c:\\windows\\system32\\reg.exe") {
throw new Error("registry lookup disabled for test");
}
return accessSync(filePath, mode);
});
vi.doMock("execa", () => ({ execa: execaMock }));
vi.doMock("../infra/executable-path.js", async () => {
const actual = await vi.importActual<typeof import("../infra/executable-path.js")>(
"../infra/executable-path.js",
);
return {
...actual,
isRegularFile: isRegularFileMock,
resolveExecutableFromPathEnv: resolveExecutableFromPathEnvMock,
resolveExecutablePathCandidate: resolveExecutablePathCandidateMock,
};
});
vi.doMock("node:child_process", async () => {
const actual =
await vi.importActual<typeof import("node:child_process")>("node:child_process");
return { ...actual, spawnSync: spawnSyncMock };
});
({ getWindowsInstallRoots, getWindowsSystem32ExePath } =
await import("../infra/windows-install-roots.js"));
({
runCommandBuffered,
runCommandWithTimeout,
runExec,
runUtf8CommandWithTimeout,
spawnCommand,
} = await import("./exec.js"));
({ runCommandBuffersWithTimeout } = await import("./exec-runner.js"));
({ withCommandProcessScope } = await import("./exec-spawn.js"));
});
afterAll(() => {
vi.doUnmock("execa");
vi.doUnmock("../infra/executable-path.js");
vi.doUnmock("node:child_process");
vi.resetModules();
});
beforeEach(() => {
execaMock.mockReset();
execaMock.mockImplementation(() => createMockSubprocess());
isRegularFileMock.mockReset();
isRegularFileMock.mockReturnValue(true);
resolveExecutableFromPathEnvMock.mockReset();
resolveExecutableFromPathEnvMock.mockImplementation((command: string) => {
const basename = path.win32.basename(command).toLowerCase();
if (["corepack", "pnpm", "yarn"].includes(basename)) {
return undefined;
}
if (command.includes("\\")) {
return command;
}
const extension = path.extname(command) || path.win32.extname(command);
return path.win32.join(
"C:\\openclaw-test-bin",
extension ? command : `${path.win32.basename(command)}.exe`,
);
});
resolveExecutablePathCandidateMock.mockReset();
resolveExecutablePathCandidateMock.mockImplementation((command: string) => command);
spawnSyncMock.mockReset();
spawnSyncMock.mockReturnValue({ stdout: "Active code page: 936", stderr: "" });
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it("wraps .cmd commands through trusted cmd.exe", async () => {
await withMockedWindowsPlatform(async () => {
await runCommandWithTimeout(["pnpm", "--version"], { timeoutMs: 1_000 });
expectCmdWrappedInvocation(requireExecaCall(0));
});
});
it("ignores ComSpec when selecting the Windows command wrapper", async () => {
const previousComSpec = process.env.ComSpec;
const previousSystemRoot = process.env.SystemRoot;
process.env.ComSpec = "C:\\workspace\\evil\\cmd.exe";
process.env.SystemRoot = "C:\\Windows";
try {
await withMockedWindowsPlatform(async () => {
await runCommandWithTimeout(["pnpm", "--version"], { timeoutMs: 1_000 });
expect(requireExecaCall(0)[0].toLowerCase()).toBe("c:\\windows\\system32\\cmd.exe");
});
} finally {
if (previousComSpec === undefined) {
delete process.env.ComSpec;
} else {
process.env.ComSpec = previousComSpec;
}
if (previousSystemRoot === undefined) {
delete process.env.SystemRoot;
} else {
process.env.SystemRoot = previousSystemRoot;
}
}
});
it("resolves implicit batch shims before Execa can consult ComSpec", async () => {
await withTempDir("openclaw-execa-windows-shim-", async (binDir) => {
const shimPath = path.join(binDir, "custom-shim.cmd");
fs.writeFileSync(shimPath, "@echo off\r\n", "utf8");
resolveExecutableFromPathEnvMock.mockReturnValueOnce(shimPath);
await withMockedWindowsPlatform(async () => {
void spawnCommand(["custom-shim", "--version"], {
baseEnv: {
ComSpec: "C:\\workspace\\evil\\cmd.exe",
PATH: binDir,
PATHEXT: ".CMD",
},
});
const call = requireExecaCall(0);
expect(call[0]).toBe(expectedTrustedCmdExe());
expect(call[1][3]).toContain(`"${shimPath}" "--version"`);
});
});
});
it("rejects unresolved commands before Execa can consult ambient ComSpec", async () => {
resolveExecutableFromPathEnvMock.mockReturnValueOnce(undefined);
await withMockedWindowsPlatform(async () => {
expect(() =>
spawnCommand(["missing\r\ncalc.exe"], {
baseEnv: {
ComSpec: "C:\\workspace\\evil\\cmd.exe",
PATH: "C:\\openclaw-test-bin",
PATHEXT: ".EXE;.CMD;.BAT;.COM",
},
}),
).toThrow("ENOENT");
expect(execaMock).not.toHaveBeenCalled();
});
});
it("rejects unsupported Windows command types before Execa", async () => {
resolveExecutableFromPathEnvMock.mockReturnValueOnce("C:\\tools\\script.ps1");
await withMockedWindowsPlatform(async () => {
expect(() => spawnCommand(["script.ps1"])).toThrow("Unsupported Windows command extension");
expect(execaMock).not.toHaveBeenCalled();
});
});
it("quotes carets inside the trusted cmd.exe wrapper", async () => {
await withMockedWindowsPlatform(async () => {
await runCommandWithTimeout(["pnpm", "run", "value^with^carets"], { timeoutMs: 1_000 });
const commandLine = String(requireExecaCall(0)[1][3]);
expect(commandLine).toContain('"value^with^carets"');
});
});
it("spawns node plus npm-cli.js instead of npm.cmd when available", async () => {
vi.spyOn(fs, "existsSync").mockReturnValue(true);
vi.spyOn(process, "execPath", "get").mockReturnValue("C:\\Program Files\\nodejs\\node.exe");
await withMockedWindowsPlatform(async () => {
void spawnCommand(["npm", "--version"]);
const [command, args, options] = requireExecaCall(0);
expect(path.win32.basename(command).toLowerCase()).toBe("node.exe");
expect(args[0]).toContain(path.join("node_modules", "npm", "bin", "npm-cli.js"));
expect(args[1]).toBe("--version");
expect(options.shell).toBe(false);
});
});
it("falls back to a trusted npm.cmd wrapper when npm-cli.js is unavailable", async () => {
vi.spyOn(fs, "existsSync").mockReturnValue(false);
await withMockedWindowsPlatform(async () => {
void spawnCommand(["npm", "--version"]);
expectCmdWrappedInvocation(requireExecaCall(0), "npm.cmd");
});
});
it("sets windowsHide and disables shell on direct commands", async () => {
await withMockedWindowsPlatform(async () => {
void spawnCommand(["node", "script.js"]);
const [command, , options] = requireExecaCall(0);
expect(path.win32.basename(command).toLowerCase()).toBe("node.exe");
expect(options).toMatchObject({ shell: false, windowsHide: true });
});
});
it("infers success when a spawned Windows shim has no exit state", async () => {
vi.useFakeTimers();
const command = createMockSubprocess({ autoFinish: false });
execaMock.mockReturnValueOnce(command);
await withMockedWindowsPlatform(async () => {
const resultPromise = runCommandWithTimeout(["pnpm", "--version"], {
timeoutMs: 1_000,
});
command.finish({ exitCode: undefined, failed: true });
await vi.advanceTimersByTimeAsync(251);
await expect(resultPromise).resolves.toMatchObject({ code: 0, termination: "exit" });
});
});
it("preserves a delayed nonzero exit code from a Windows shim", async () => {
vi.useFakeTimers();
const command = createMockSubprocess({ autoFinish: false });
execaMock.mockReturnValueOnce(command);
await withMockedWindowsPlatform(async () => {
const resultPromise = runCommandWithTimeout(["pnpm", "--version"], {
timeoutMs: 1_000,
});
command.finish({ exitCode: undefined, failed: true });
setTimeout(() => {
command.exitCode = 7;
}, 20);
await vi.advanceTimersByTimeAsync(30);
await expect(resultPromise).resolves.toMatchObject({ code: 7, termination: "exit" });
});
});
it("sanitizes a Windows shim launch error without an exit state", async () => {
const command = createMockSubprocess({ autoFinish: false });
execaMock.mockReturnValueOnce(command);
await withMockedWindowsPlatform(async () => {
const resultPromise = runCommandWithTimeout(["pnpm", "--version"], {
timeoutMs: 1_000,
});
command.finish({
cause: new Error("spawn pnpm ENOENT"),
code: "ENOENT",
exitCode: undefined,
failed: true,
});
await expect(resultPromise).rejects.toMatchObject({
code: "ENOENT",
message: "Command failed during launch or output capture (ENOENT)",
});
});
});
it.each([
{ exitCode: 0, killProcessTree: undefined },
{ exitCode: 7, killProcessTree: undefined },
{ exitCode: 0, killProcessTree: true },
{ exitCode: 7, killProcessTree: true },
])(
"does not target an exited Windows root (code $exitCode, tree=$killProcessTree) while output settles",
async ({ exitCode, killProcessTree }) => {
vi.useFakeTimers();
const command = createMockSubprocess({ autoFinish: false });
execaMock.mockReturnValueOnce(command);
await withMockedWindowsPlatform(async () => {
const resultPromise = runCommandWithTimeout(["node", "quick.js"], {
timeoutMs: 80,
killProcessTree,
});
command.exitCode = exitCode;
command.emit("exit", exitCode, null);
await vi.advanceTimersByTimeAsync(81);
expect(execaMock).toHaveBeenCalledTimes(1);
expect(command.stdout.destroyed).toBe(false);
await vi.advanceTimersByTimeAsync(19);
expect(command.stdout.destroyed).toBe(false);
await vi.advanceTimersToNextTimerAsync();
expect(command.stdout.destroyed).toBe(true);
expect(command.stderr.destroyed).toBe(true);
command.finish({ exitCode });
await expect(resultPromise).resolves.toMatchObject({ code: exitCode, termination: "exit" });
});
},
);
it("gracefully then force-kills a Windows process tree", async () => {
vi.useFakeTimers();
const command = createMockSubprocess({ autoFinish: false });
execaMock
.mockImplementationOnce(() => command)
.mockImplementation(() => createMockSubprocess());
await withMockedWindowsPlatform(async () => {
const resultPromise = runCommandWithTimeout(["node", "idle.js"], {
killGraceMs: 40,
killProcessTree: true,
timeoutMs: 80,
});
await vi.advanceTimersByTimeAsync(81);
expect(requireExecaCall(1).slice(0, 2)).toEqual([
getWindowsSystem32ExePath("taskkill.exe"),
["/PID", "1234", "/T"],
]);
expect(command.kill).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(40);
expect(requireExecaCall(2)[1]).toEqual(["/PID", "1234", "/T", "/F"]);
command.finish({ signal: "SIGKILL" });
await expect(resultPromise).resolves.toMatchObject({ code: 124, termination: "timeout" });
});
});
it.each(["stdout", "stderr"] as const)(
"terminates a Windows process tree when its %s stream fails",
async (stream) => {
vi.useFakeTimers();
const command = createMockSubprocess({ autoFinish: false });
execaMock
.mockImplementationOnce(() => command)
.mockImplementation(() => createMockSubprocess());
await withMockedWindowsPlatform(async () => {
const resultPromise = runCommandBuffered(["node", "idle.js"], {
terminateOnOutputError: true,
timeoutMs: 10_000,
});
command[stream].destroy(new Error(`${stream} EPIPE`));
await vi.advanceTimersByTimeAsync(301);
command.finish({ signal: "SIGKILL" });
await expect(resultPromise).resolves.toMatchObject({
error: { message: `${stream} EPIPE` },
errorStream: stream,
termination: "error",
});
});
},
);
it("keeps forced Windows tree escalation after graceful taskkill returns nonzero", async () => {
vi.useFakeTimers();
const command = createMockSubprocess({ autoFinish: false });
execaMock
.mockImplementationOnce(() => command)
.mockImplementationOnce(() => createMockSubprocess({ exitCode: 1 }))
.mockImplementation(() => createMockSubprocess());
await withMockedWindowsPlatform(async () => {
const resultPromise = runCommandWithTimeout(["node", "idle.js"], {
killProcessTree: true,
timeoutMs: 80,
});
await vi.advanceTimersByTimeAsync(81);
expect(requireExecaCall(1)[1]).toEqual(["/PID", "1234", "/T"]);
await vi.advanceTimersByTimeAsync(300);
expect(requireExecaCall(2)[1]).toEqual(["/PID", "1234", "/T", "/F"]);
command.finish({ signal: "SIGKILL" });
await expect(resultPromise).resolves.toMatchObject({ code: 124, termination: "timeout" });
});
});
it.each(
["exits", "times out"].flatMap((gracefulOutcome) =>
(["timeout", "scope"] as const).map((interruption) => ({ gracefulOutcome, interruption })),
),
)(
"waits for forced taskkill after $interruption when graceful taskkill $gracefulOutcome",
async ({ gracefulOutcome, interruption }) => {
vi.useFakeTimers();
const command = createMockSubprocess({ autoFinish: false });
const gracefulTaskkill = createMockSubprocess({ autoFinish: gracefulOutcome === "exits" });
const forcedTaskkill = createMockSubprocess({ autoFinish: false });
execaMock
.mockImplementationOnce(() => command)
.mockImplementationOnce(() => gracefulTaskkill)
.mockImplementationOnce(() => forcedTaskkill);
await withMockedWindowsPlatform(async () => {
const controller = new AbortController();
const run = () =>
runCommandWithTimeout(["node", "idle.js"], {
killProcessTree: true,
...(interruption === "timeout" ? { timeoutMs: 80 } : {}),
});
const resultPromise =
interruption === "scope" ? withCommandProcessScope(run, controller.signal) : run();
const cancelSignal = requireExecaCall(0)[2].cancelSignal as AbortSignal;
if (interruption === "scope") {
controller.abort();
} else {
await vi.advanceTimersByTimeAsync(80);
}
await vi.advanceTimersByTimeAsync(300);
expect(requireExecaCall(2)[1]).toEqual(["/PID", "1234", "/T", "/F"]);
if (gracefulOutcome === "times out") {
// Graceful taskkill expires while its later-started forced sibling still owns the root.
await vi.advanceTimersByTimeAsync(5_000 - 300);
gracefulTaskkill.finish({ signal: "SIGTERM", timedOut: true });
await vi.advanceTimersByTimeAsync(0);
}
expect(command.kill).not.toHaveBeenCalled();
expect(cancelSignal.aborted).toBe(false);
for (const index of [1, 2]) {
const cleanupSignal = requireExecaCall(index)[2].cancelSignal as AbortSignal | undefined;
expect(cleanupSignal?.aborted).not.toBe(true);
}
forcedTaskkill.finish();
await vi.advanceTimersByTimeAsync(0);
expect(cancelSignal.aborted).toBe(true);
command.finish({ signal: "SIGKILL" });
await expect(resultPromise).resolves.toMatchObject({
...(interruption === "timeout" ? { code: 124 } : {}),
termination: interruption === "timeout" ? "timeout" : "signal",
cleanup: "forced",
});
});
},
);
it("waits for immediate forced taskkill before aborting the Windows root", async () => {
vi.useFakeTimers();
const command = createMockSubprocess({ autoFinish: false });
const forcedTaskkill = createMockSubprocess({ autoFinish: false });
execaMock.mockImplementationOnce(() => command).mockImplementationOnce(() => forcedTaskkill);
await withMockedWindowsPlatform(async () => {
const resultPromise = runCommandWithTimeout(["node", "idle.js"], {
killProcessTree: false,
timeoutMs: 80,
});
const cancelSignal = requireExecaCall(0)[2].cancelSignal as AbortSignal;
await vi.advanceTimersByTimeAsync(81);
expect(requireExecaCall(1)[1]).toEqual(["/PID", "1234", "/T", "/F"]);
expect(cancelSignal.aborted).toBe(false);
forcedTaskkill.finish();
await vi.advanceTimersByTimeAsync(0);
expect(cancelSignal.aborted).toBe(true);
command.finish({ signal: "SIGKILL" });
await expect(resultPromise).resolves.toMatchObject({ code: 124, termination: "timeout" });
});
});
it.each([false, true])(
"cancels the Windows root when every taskkill fails to spawn (tree=%s)",
async (killProcessTree) => {
vi.useFakeTimers();
const command = createMockSubprocess({ autoFinish: false });
execaMock
.mockImplementationOnce(() => command)
.mockImplementation(() => {
throw new Error("taskkill could not spawn");
});
await withMockedWindowsPlatform(async () => {
const resultPromise = runCommandWithTimeout(["node", "idle.js"], {
killProcessTree,
timeoutMs: 80,
});
const cancelSignal = requireExecaCall(0)[2].cancelSignal as AbortSignal;
await vi.advanceTimersByTimeAsync(380);
expect(cancelSignal.aborted).toBe(true);
command.finish({ signal: "SIGKILL" });
await expect(resultPromise).resolves.toMatchObject({ code: 124, termination: "timeout" });
});
},
);
it.each(["exec", "command"])("decodes GBK stdout and stderr from %s", async (runner) => {
execaMock.mockImplementationOnce(() =>
createMockSubprocess({
stderrChunks: [Buffer.from([0xa3, 0xbb])],
stdoutChunks: [Buffer.from([0xb2, 0xe2, 0xca, 0xd4])],
}),
);
await withMockedWindowsPlatform(async () => {
const result =
runner === "exec"
? runExec("node", ["gbk-output.js"], 1_000)
: runCommandWithTimeout(["node", "gbk-output.js"], 1_000);
await expect(result).resolves.toMatchObject({
stdout: "测试",
stderr: ";",
});
expect(requireExecaCall(0)[2].encoding).toBe("buffer");
});
});
it.each(["exec", "command"])("decodes UTF-16 output from %s", async (runner) => {
execaMock.mockImplementationOnce(() =>
createMockSubprocess({
stdoutChunks: [Buffer.from([0xff, 0xfe, 0x6f, 0x00, 0x6b, 0x00])],
stderrChunks: [Buffer.from([0xfe, 0xff, 0x00, 0x6e, 0x00, 0x6f])],
}),
);
await withMockedWindowsPlatform(async () => {
const result =
runner === "exec"
? runExec("node", ["utf16-output.js"], 1_000)
: runCommandWithTimeout(["node", "utf16-output.js"], 1_000);
await expect(result).resolves.toMatchObject({
stdout: "ok",
stderr: "no",
});
expect(spawnSyncMock).toHaveBeenCalledTimes(runner === "exec" ? 0 : 1);
});
});
it.each(["exec", "command"])("decodes UTF-8 output from %s", async (runner) => {
execaMock.mockImplementationOnce(() =>
createMockSubprocess({ stdoutChunks: [Buffer.from("测试", "utf8")] }),
);
await withMockedWindowsPlatform(async () => {
const result =
runner === "exec"
? runExec("node", ["utf8-output.js"], 1_000)
: runCommandWithTimeout(["node", "utf8-output.js"], 1_000);
await expect(result).resolves.toMatchObject({
stdout: "测试",
stderr: "",
});
expect(spawnSyncMock).toHaveBeenCalledTimes(runner === "exec" ? 0 : 1);
});
});
it.each([
{ encoding: "UTF-8", bytes: Buffer.from("测试", "utf8"), text: "测试", probes: 0 },
{ encoding: "UTF-16", bytes: Buffer.from([0xff, 0xfe, 0x6f, 0x00]), text: "o", probes: 0 },
{ encoding: "GBK", bytes: Buffer.from([0xb2, 0xe2]), text: "测", probes: 1 },
])("preserves $encoding diagnostics on runExec failure", async ({ bytes, text, probes }) => {
execaMock.mockImplementationOnce(() =>
createMockSubprocess({ exitCode: 1, reject: true, stdout: bytes, stderr: bytes }),
);
await withMockedWindowsPlatform(async () => {
await expect(runExec("node", ["failed-output.js"], 1_000)).rejects.toMatchObject({
message: "command failed",
code: 1,
stdout: text,
stderr: text,
});
expect(spawnSyncMock).toHaveBeenCalledTimes(probes);
});
});
it.each(["raw", "command"])(
"captures the %s result encoding before the child can change the console page",
async (runner) => {
const stdout = Buffer.from([0xb2, 0xe2]);
const stderr = Buffer.from([0xa3, 0xbb]);
execaMock.mockImplementationOnce(() => {
spawnSyncMock.mockReturnValue({ stdout: "Active code page: 1252", stderr: "" });
return createMockSubprocess({ stdoutChunks: [stdout], stderrChunks: [stderr] });
});
await withMockedWindowsPlatform(async () => {
const result =
runner === "raw"
? runCommandBuffersWithTimeout(["node", "legacy-output.js"], 1_000)
: runCommandWithTimeout(["node", "legacy-output.js"], 1_000);
await expect(result).resolves.toMatchObject(
runner === "raw"
? { code: 0, stdout, stderr, windowsEncoding: "gbk" }
: { code: 0, stdout: "测", stderr: ";" },
);
});
},
);
it("keeps truncated UTF-8 head output on a code point boundary", async () => {
execaMock.mockImplementationOnce(() =>
createMockSubprocess({
stdoutChunks: [Buffer.from("a😀z", "utf8")],
stderrChunks: [Buffer.from("b😀y", "utf8")],
}),
);
await withMockedWindowsPlatform(async () => {
await expect(
runUtf8CommandWithTimeout(["node", "utf8-output.js"], {
maxOutputBytes: 3,
outputCapture: "head",
timeoutMs: 1_000,
}),
).resolves.toMatchObject({
stdout: "a",
stderr: "b",
stdoutTruncatedBytes: 5,
stderrTruncatedBytes: 5,
});
expect(spawnSyncMock).not.toHaveBeenCalled();
});
});
it("preserves complete legacy-code-page characters in truncated head output", async () => {
execaMock.mockImplementationOnce(() =>
createMockSubprocess({ stdoutChunks: [Buffer.from([0x61, 0xb2, 0xe2, 0xca, 0xd4])] }),
);
await withMockedWindowsPlatform(async () => {
await expect(
runCommandWithTimeout(["node", "gbk-output.js"], {
maxOutputBytes: 3,
outputCapture: "head",
timeoutMs: 1_000,
}),
).resolves.toMatchObject({
stdout: "a测",
stdoutTruncatedBytes: 2,
});
});
});
it("decodes split GBK chunks after complete output capture", async () => {
execaMock.mockImplementationOnce(() =>
createMockSubprocess({
stdoutChunks: [Buffer.from([0xb2]), Buffer.from([0xe2, 0xca]), Buffer.from([0xd4])],
}),
);
await withMockedWindowsPlatform(async () => {
await expect(
runCommandWithTimeout(["node", "gbk-output.js"], { timeoutMs: 1_000 }),
).resolves.toMatchObject({ code: 0, stdout: "测试", termination: "exit" });
});
});
});