mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-13 11:00:50 +00:00
test: fix windows runtime and restart loop harnesses
This commit is contained in:
committed by
Vincent Koc
parent
dbabaa0fb2
commit
2b8828ea46
@@ -59,20 +59,41 @@ function removeNewSignalListeners(
|
||||
}
|
||||
}
|
||||
|
||||
async function withIsolatedSignals(run: () => Promise<void>) {
|
||||
const beforeSigterm = new Set(
|
||||
process.listeners("SIGTERM") as Array<(...args: unknown[]) => void>,
|
||||
);
|
||||
const beforeSigint = new Set(process.listeners("SIGINT") as Array<(...args: unknown[]) => void>);
|
||||
const beforeSigusr1 = new Set(
|
||||
process.listeners("SIGUSR1") as Array<(...args: unknown[]) => void>,
|
||||
);
|
||||
function addedSignalListener(
|
||||
signal: NodeJS.Signals,
|
||||
existing: Set<(...args: unknown[]) => void>,
|
||||
): (() => void) | null {
|
||||
const listeners = process.listeners(signal) as Array<(...args: unknown[]) => void>;
|
||||
for (let i = listeners.length - 1; i >= 0; i -= 1) {
|
||||
const listener = listeners[i];
|
||||
if (listener && !existing.has(listener)) {
|
||||
return listener as () => void;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function withIsolatedSignals(
|
||||
run: (helpers: { captureSignal: (signal: NodeJS.Signals) => () => void }) => Promise<void>,
|
||||
) {
|
||||
const existingListeners = {
|
||||
SIGTERM: new Set(process.listeners("SIGTERM") as Array<(...args: unknown[]) => void>),
|
||||
SIGINT: new Set(process.listeners("SIGINT") as Array<(...args: unknown[]) => void>),
|
||||
SIGUSR1: new Set(process.listeners("SIGUSR1") as Array<(...args: unknown[]) => void>),
|
||||
} satisfies Record<NodeJS.Signals, Set<(...args: unknown[]) => void>>;
|
||||
const captureSignal = (signal: NodeJS.Signals) => {
|
||||
const listener = addedSignalListener(signal, existingListeners[signal]);
|
||||
if (!listener) {
|
||||
throw new Error(`expected new ${signal} listener`);
|
||||
}
|
||||
return () => listener();
|
||||
};
|
||||
try {
|
||||
await run();
|
||||
await run({ captureSignal });
|
||||
} finally {
|
||||
removeNewSignalListeners("SIGTERM", beforeSigterm);
|
||||
removeNewSignalListeners("SIGINT", beforeSigint);
|
||||
removeNewSignalListeners("SIGUSR1", beforeSigusr1);
|
||||
removeNewSignalListeners("SIGTERM", existingListeners.SIGTERM);
|
||||
removeNewSignalListeners("SIGINT", existingListeners.SIGINT);
|
||||
removeNewSignalListeners("SIGUSR1", existingListeners.SIGUSR1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,10 +165,11 @@ describe("runGatewayLoop", () => {
|
||||
it("exits 0 on SIGTERM after graceful close", async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
await withIsolatedSignals(async () => {
|
||||
await withIsolatedSignals(async ({ captureSignal }) => {
|
||||
const { close, runtime, exited } = await createSignaledLoopHarness();
|
||||
const sigterm = captureSignal("SIGTERM");
|
||||
|
||||
process.emit("SIGTERM");
|
||||
sigterm();
|
||||
|
||||
await expect(exited).resolves.toBe(0);
|
||||
expect(close).toHaveBeenCalledWith({
|
||||
@@ -161,7 +183,7 @@ describe("runGatewayLoop", () => {
|
||||
it("restarts after SIGUSR1 even when drain times out, and resets lanes for the new iteration", async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
await withIsolatedSignals(async () => {
|
||||
await withIsolatedSignals(async ({ captureSignal }) => {
|
||||
getActiveTaskCount.mockReturnValueOnce(2).mockReturnValueOnce(0);
|
||||
waitForActiveTasks.mockResolvedValueOnce({ drained: false });
|
||||
|
||||
@@ -171,6 +193,8 @@ describe("runGatewayLoop", () => {
|
||||
|
||||
const closeFirst = vi.fn(async () => {});
|
||||
const closeSecond = vi.fn(async () => {});
|
||||
const closeThird = vi.fn(async () => {});
|
||||
const { runtime, exited } = createRuntimeWithExitSignal();
|
||||
|
||||
const start = vi.fn<StartServer>();
|
||||
let resolveFirst: (() => void) | null = null;
|
||||
@@ -191,24 +215,28 @@ describe("runGatewayLoop", () => {
|
||||
return { close: closeSecond };
|
||||
});
|
||||
|
||||
start.mockRejectedValueOnce(new Error("stop-loop"));
|
||||
let resolveThird: (() => void) | null = null;
|
||||
const startedThird = new Promise<void>((resolve) => {
|
||||
resolveThird = resolve;
|
||||
});
|
||||
start.mockImplementationOnce(async () => {
|
||||
resolveThird?.();
|
||||
return { close: closeThird };
|
||||
});
|
||||
|
||||
const { runGatewayLoop } = await import("./run-loop.js");
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
const loopPromise = runGatewayLoop({
|
||||
void runGatewayLoop({
|
||||
start: start as unknown as Parameters<typeof runGatewayLoop>[0]["start"],
|
||||
runtime: runtime as unknown as Parameters<typeof runGatewayLoop>[0]["runtime"],
|
||||
});
|
||||
|
||||
await startedFirst;
|
||||
const sigusr1 = captureSignal("SIGUSR1");
|
||||
const sigterm = captureSignal("SIGTERM");
|
||||
expect(start).toHaveBeenCalledTimes(1);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
|
||||
process.emit("SIGUSR1");
|
||||
sigusr1();
|
||||
|
||||
await startedSecond;
|
||||
expect(start).toHaveBeenCalledTimes(2);
|
||||
@@ -224,9 +252,10 @@ describe("runGatewayLoop", () => {
|
||||
expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(1);
|
||||
expect(resetAllLanes).toHaveBeenCalledTimes(1);
|
||||
|
||||
process.emit("SIGUSR1");
|
||||
sigusr1();
|
||||
|
||||
await expect(loopPromise).rejects.toThrow("stop-loop");
|
||||
await startedThird;
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(closeSecond).toHaveBeenCalledWith({
|
||||
reason: "gateway restarting",
|
||||
restartExpectedMs: 1500,
|
||||
@@ -235,13 +264,20 @@ describe("runGatewayLoop", () => {
|
||||
expect(markGatewayDraining).toHaveBeenCalledTimes(2);
|
||||
expect(resetAllLanes).toHaveBeenCalledTimes(2);
|
||||
expect(acquireGatewayLock).toHaveBeenCalledTimes(3);
|
||||
|
||||
sigterm();
|
||||
await expect(exited).resolves.toBe(0);
|
||||
expect(closeThird).toHaveBeenCalledWith({
|
||||
reason: "gateway stopping",
|
||||
restartExpectedMs: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("releases the lock before exiting on spawned restart", async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
await withIsolatedSignals(async () => {
|
||||
await withIsolatedSignals(async ({ captureSignal }) => {
|
||||
const lockRelease = vi.fn(async () => {});
|
||||
acquireGatewayLock.mockResolvedValueOnce({
|
||||
release: lockRelease,
|
||||
@@ -255,11 +291,12 @@ describe("runGatewayLoop", () => {
|
||||
|
||||
const exitCallOrder: string[] = [];
|
||||
const { runtime, exited } = await createSignaledLoopHarness(exitCallOrder);
|
||||
const sigusr1 = captureSignal("SIGUSR1");
|
||||
lockRelease.mockImplementation(async () => {
|
||||
exitCallOrder.push("lockRelease");
|
||||
});
|
||||
|
||||
process.emit("SIGUSR1");
|
||||
sigusr1();
|
||||
|
||||
await exited;
|
||||
expect(lockRelease).toHaveBeenCalled();
|
||||
@@ -271,40 +308,45 @@ describe("runGatewayLoop", () => {
|
||||
it("forwards lockPort to initial and restart lock acquisitions", async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
await withIsolatedSignals(async () => {
|
||||
await withIsolatedSignals(async ({ captureSignal }) => {
|
||||
const closeFirst = vi.fn(async () => {});
|
||||
const closeSecond = vi.fn(async () => {});
|
||||
restartGatewayProcessWithFreshPid.mockReturnValueOnce({ mode: "disabled" });
|
||||
const closeThird = vi.fn(async () => {});
|
||||
const { runtime, exited } = createRuntimeWithExitSignal();
|
||||
|
||||
const start = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ close: closeFirst })
|
||||
.mockResolvedValueOnce({ close: closeSecond })
|
||||
.mockRejectedValueOnce(new Error("stop-loop"));
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
.mockResolvedValueOnce({ close: closeThird });
|
||||
const { runGatewayLoop } = await import("./run-loop.js");
|
||||
const loopPromise = runGatewayLoop({
|
||||
void runGatewayLoop({
|
||||
start: start as unknown as Parameters<typeof runGatewayLoop>[0]["start"],
|
||||
runtime: runtime as unknown as Parameters<typeof runGatewayLoop>[0]["runtime"],
|
||||
lockPort: 18789,
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
const sigusr1 = captureSignal("SIGUSR1");
|
||||
const sigterm = captureSignal("SIGTERM");
|
||||
|
||||
sigusr1();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
sigusr1();
|
||||
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
process.emit("SIGUSR1");
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
process.emit("SIGUSR1");
|
||||
|
||||
await expect(loopPromise).rejects.toThrow("stop-loop");
|
||||
expect(acquireGatewayLock).toHaveBeenNthCalledWith(1, { port: 18789 });
|
||||
expect(acquireGatewayLock).toHaveBeenNthCalledWith(2, { port: 18789 });
|
||||
expect(acquireGatewayLock).toHaveBeenNthCalledWith(3, { port: 18789 });
|
||||
|
||||
sigterm();
|
||||
await expect(exited).resolves.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("exits when lock reacquire fails during in-process restart fallback", async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
await withIsolatedSignals(async () => {
|
||||
await withIsolatedSignals(async ({ captureSignal }) => {
|
||||
const lockRelease = vi.fn(async () => {});
|
||||
acquireGatewayLock
|
||||
.mockResolvedValueOnce({
|
||||
@@ -317,7 +359,8 @@ describe("runGatewayLoop", () => {
|
||||
});
|
||||
|
||||
const { start, exited } = await createSignaledLoopHarness();
|
||||
process.emit("SIGUSR1");
|
||||
const sigusr1 = captureSignal("SIGUSR1");
|
||||
sigusr1();
|
||||
|
||||
await expect(exited).resolves.toBe(1);
|
||||
expect(acquireGatewayLock).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -229,9 +229,16 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `openclaw-${params.runtime}-path-`));
|
||||
const binDir = path.join(tmp, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
const runtimePath = path.join(binDir, params.runtime);
|
||||
fs.writeFileSync(runtimePath, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
||||
fs.chmodSync(runtimePath, 0o755);
|
||||
const runtimePath =
|
||||
process.platform === "win32"
|
||||
? path.join(binDir, `${params.runtime}.cmd`)
|
||||
: path.join(binDir, params.runtime);
|
||||
const runtimeBody =
|
||||
process.platform === "win32" ? "@echo off\r\nexit /b 0\r\n" : "#!/bin/sh\nexit 0\n";
|
||||
fs.writeFileSync(runtimePath, runtimeBody, { mode: 0o755 });
|
||||
if (process.platform !== "win32") {
|
||||
fs.chmodSync(runtimePath, 0o755);
|
||||
}
|
||||
const oldPath = process.env.PATH;
|
||||
process.env.PATH = `${binDir}${path.delimiter}${oldPath ?? ""}`;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user