Files
openclaw/src/process/exec.windows.test.ts
Peter Steinberger d1684f48a3 refactor: delete dead infra and config exports (#106019)
* refactor: delete dead infra and config exports

* refactor: preserve live infra and config contracts

* refactor(config): remove obsolete file-store lifecycle APIs

* refactor(infra): finish current-main dead export cleanup
2026-07-13 12:00:47 -07:00

545 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 & {
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;
signal?: NodeJS.Signals;
stderr?: Buffer;
stderrChunks?: Buffer[];
stdout?: Buffer;
stdoutChunks?: Buffer[];
}): MockSubprocess {
const child = new EventEmitter() as MockSubprocess;
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;
const completion = new Promise<MockResult>((resolvePromise) => {
resolve = resolvePromise;
});
// oxlint-disable-next-line unicorn/no-thenable -- Stub matches Execa's event-emitting promise shape.
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);
resolve({
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?.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 runExec: typeof import("./exec.js").runExec;
let spawnCommand: typeof import("./exec.js").spawnCommand;
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"));
({ runCommandWithTimeout, runExec, spawnCommand } = await import("./exec.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("escapes command arguments 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);
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("does not time out after the direct child exits while output settles", async () => {
vi.useFakeTimers();
const command = createMockSubprocess({ autoFinish: false });
execaMock.mockReturnValueOnce(command);
await withMockedWindowsPlatform(async () => {
const resultPromise = runCommandWithTimeout(["node", "quick.js"], { timeoutMs: 80 });
command.exitCode = 0;
command.emit("exit", 0, null);
await vi.advanceTimersByTimeAsync(81);
expect(execaMock).toHaveBeenCalledTimes(1);
expect(command.stdout.destroyed).toBe(false);
await vi.advanceTimersByTimeAsync(19);
expect(command.stdout.destroyed).toBe(true);
expect(command.stderr.destroyed).toBe(true);
command.finish();
await expect(resultPromise).resolves.toMatchObject({ code: 0, 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"], {
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(300);
expect(requireExecaCall(2)[1]).toEqual(["/PID", "1234", "/T", "/F"]);
command.finish({ signal: "SIGKILL" });
await expect(resultPromise).resolves.toMatchObject({ code: 124, termination: "timeout" });
});
});
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("waits for forced taskkill before aborting the live Windows root", async () => {
vi.useFakeTimers();
const command = createMockSubprocess({ autoFinish: false });
const forcedTaskkill = createMockSubprocess({ autoFinish: false });
execaMock
.mockImplementationOnce(() => command)
.mockImplementationOnce(() => createMockSubprocess())
.mockImplementationOnce(() => forcedTaskkill);
await withMockedWindowsPlatform(async () => {
const resultPromise = runCommandWithTimeout(["node", "idle.js"], {
killProcessTree: true,
timeoutMs: 80,
});
const cancelSignal = requireExecaCall(0)[2].cancelSignal as AbortSignal;
await vi.advanceTimersByTimeAsync(381);
expect(requireExecaCall(2)[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("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("decodes GBK stdout and stderr from runExec", async () => {
execaMock.mockImplementationOnce(() =>
createMockSubprocess({
stderr: Buffer.from([0xa3, 0xbb]),
stdout: Buffer.from([0xb2, 0xe2, 0xca, 0xd4]),
}),
);
await withMockedWindowsPlatform(async () => {
await expect(runExec("node", ["gbk-output.js"], 1_000)).resolves.toEqual({
stdout: "测试",
stderr: "",
});
expect(requireExecaCall(0)[2].encoding).toBe("buffer");
});
});
it("prefers valid UTF-8 output from runExec", async () => {
execaMock.mockImplementationOnce(() =>
createMockSubprocess({ stdout: Buffer.from("测试", "utf8") }),
);
await withMockedWindowsPlatform(async () => {
await expect(runExec("node", ["utf8-output.js"], 1_000)).resolves.toEqual({
stdout: "测试",
stderr: "",
});
});
});
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" });
});
});
});