mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-19 03:04:45 +00:00
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
import type { ChildProcess } from "node:child_process";
|
|
import { EventEmitter } from "node:events";
|
|
import { PassThrough } from "node:stream";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { spawnWithFallback } from "./spawn-utils.js";
|
|
|
|
function createStubChild() {
|
|
const child = new EventEmitter() as ChildProcess;
|
|
child.stdin = new PassThrough() as ChildProcess["stdin"];
|
|
child.stdout = new PassThrough() as ChildProcess["stdout"];
|
|
child.stderr = new PassThrough() as ChildProcess["stderr"];
|
|
Object.defineProperty(child, "pid", { value: 1234, configurable: true });
|
|
Object.defineProperty(child, "killed", { value: false, configurable: true, writable: true });
|
|
child.kill = vi.fn(() => true) as ChildProcess["kill"];
|
|
queueMicrotask(() => {
|
|
child.emit("spawn");
|
|
});
|
|
return child;
|
|
}
|
|
|
|
function spawnOptionsAt(
|
|
spawnMock: { mock: { calls: readonly unknown[][] } },
|
|
callIndex: number,
|
|
): { stdio?: unknown } {
|
|
const call = spawnMock.mock.calls[callIndex];
|
|
if (!call) {
|
|
throw new Error(`expected spawn call ${callIndex}`);
|
|
}
|
|
const options = call[2];
|
|
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
throw new Error(`expected spawn call ${callIndex} options`);
|
|
}
|
|
return options;
|
|
}
|
|
|
|
describe("spawnWithFallback", () => {
|
|
it("retries on EBADF using fallback options", async () => {
|
|
const spawnMock = vi
|
|
.fn()
|
|
.mockImplementationOnce(() => {
|
|
const err = new Error("spawn EBADF");
|
|
(err as NodeJS.ErrnoException).code = "EBADF";
|
|
throw err;
|
|
})
|
|
.mockImplementationOnce(() => createStubChild());
|
|
|
|
const result = await spawnWithFallback({
|
|
argv: ["echo", "ok"],
|
|
options: { stdio: ["pipe", "pipe", "pipe"] },
|
|
fallbacks: [{ label: "safe-stdin", options: { stdio: ["ignore", "pipe", "pipe"] } }],
|
|
spawnImpl: spawnMock,
|
|
});
|
|
|
|
expect(result.usedFallback).toBe(true);
|
|
expect(result.fallbackLabel).toBe("safe-stdin");
|
|
expect(spawnMock).toHaveBeenCalledTimes(2);
|
|
expect(spawnOptionsAt(spawnMock, 0).stdio).toEqual(["pipe", "pipe", "pipe"]);
|
|
expect(spawnOptionsAt(spawnMock, 1).stdio).toEqual(["ignore", "pipe", "pipe"]);
|
|
});
|
|
|
|
it("does not retry on non-EBADF errors", async () => {
|
|
const spawnMock = vi.fn().mockImplementationOnce(() => {
|
|
const err = new Error("spawn ENOENT");
|
|
(err as NodeJS.ErrnoException).code = "ENOENT";
|
|
throw err;
|
|
});
|
|
|
|
await expect(
|
|
spawnWithFallback({
|
|
argv: ["missing"],
|
|
options: { stdio: ["pipe", "pipe", "pipe"] },
|
|
fallbacks: [{ label: "safe-stdin", options: { stdio: ["ignore", "pipe", "pipe"] } }],
|
|
spawnImpl: spawnMock,
|
|
}),
|
|
).rejects.toThrow(/ENOENT/);
|
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|