mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 21:53:43 +00:00
test(perf): tighten process test timeouts and fs setup
This commit is contained in:
@@ -32,9 +32,7 @@ let fixtureRoot = "";
|
|||||||
let fixtureCount = 0;
|
let fixtureCount = 0;
|
||||||
|
|
||||||
async function makeStorePath() {
|
async function makeStorePath() {
|
||||||
const dir = path.join(fixtureRoot, `case-${fixtureCount++}`);
|
const storePath = path.join(fixtureRoot, `case-${fixtureCount++}.jobs.json`);
|
||||||
await fs.mkdir(dir, { recursive: true });
|
|
||||||
const storePath = path.join(dir, "jobs.json");
|
|
||||||
return {
|
return {
|
||||||
storePath,
|
storePath,
|
||||||
};
|
};
|
||||||
@@ -1523,7 +1521,7 @@ describe("Cron issue regressions", () => {
|
|||||||
|
|
||||||
// Keep this short for suite speed while still separating expected timeout
|
// Keep this short for suite speed while still separating expected timeout
|
||||||
// from the 1/3-regression timeout.
|
// from the 1/3-regression timeout.
|
||||||
const timeoutSeconds = 0.06;
|
const timeoutSeconds = 0.03;
|
||||||
const cronJob = createIsolatedRegressionJob({
|
const cronJob = createIsolatedRegressionJob({
|
||||||
id: "timeout-fraction-29774",
|
id: "timeout-fraction-29774",
|
||||||
name: "timeout fraction regression",
|
name: "timeout fraction regression",
|
||||||
@@ -1579,7 +1577,7 @@ describe("Cron issue regressions", () => {
|
|||||||
// The abort must not fire at the old ~1/3 regression value.
|
// The abort must not fire at the old ~1/3 regression value.
|
||||||
// Keep the lower bound conservative for loaded CI runners.
|
// Keep the lower bound conservative for loaded CI runners.
|
||||||
const elapsedMs = (abortWallMs ?? Date.now()) - wallStart;
|
const elapsedMs = (abortWallMs ?? Date.now()) - wallStart;
|
||||||
expect(elapsedMs).toBeGreaterThanOrEqual(timeoutSeconds * 1000 * 0.6);
|
expect(elapsedMs).toBeGreaterThanOrEqual(timeoutSeconds * 1000 * 0.55);
|
||||||
|
|
||||||
const job = state.store?.jobs.find((entry) => entry.id === "timeout-fraction-29774");
|
const job = state.store?.jobs.find((entry) => entry.id === "timeout-fraction-29774");
|
||||||
expect(job?.state.lastStatus).toBe("error");
|
expect(job?.state.lastStatus).toBe("error");
|
||||||
|
|||||||
@@ -1,52 +1,11 @@
|
|||||||
import { spawn } from "node:child_process";
|
import type { ChildProcess } from "node:child_process";
|
||||||
import path from "node:path";
|
import { EventEmitter } from "node:events";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { withEnvAsync } from "../test-utils/env.js";
|
import { withEnvAsync } from "../test-utils/env.js";
|
||||||
import { attachChildProcessBridge } from "./child-process-bridge.js";
|
import { attachChildProcessBridge } from "./child-process-bridge.js";
|
||||||
import { runCommandWithTimeout, shouldSpawnWithShell } from "./exec.js";
|
import { runCommandWithTimeout, shouldSpawnWithShell } from "./exec.js";
|
||||||
|
|
||||||
const CHILD_READY_TIMEOUT_MS = 2_000;
|
|
||||||
const CHILD_EXIT_TIMEOUT_MS = 2_000;
|
|
||||||
|
|
||||||
function waitForLine(
|
|
||||||
stream: NodeJS.ReadableStream,
|
|
||||||
timeoutMs = CHILD_READY_TIMEOUT_MS,
|
|
||||||
): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let buffer = "";
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
cleanup();
|
|
||||||
reject(new Error("timeout waiting for line"));
|
|
||||||
}, timeoutMs);
|
|
||||||
|
|
||||||
const onData = (chunk: Buffer | string): void => {
|
|
||||||
buffer += chunk.toString();
|
|
||||||
const idx = buffer.indexOf("\n");
|
|
||||||
if (idx >= 0) {
|
|
||||||
const line = buffer.slice(0, idx).trim();
|
|
||||||
cleanup();
|
|
||||||
resolve(line);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onError = (err: unknown): void => {
|
|
||||||
cleanup();
|
|
||||||
reject(err);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanup = (): void => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
stream.off("data", onData);
|
|
||||||
stream.off("error", onError);
|
|
||||||
};
|
|
||||||
|
|
||||||
stream.on("data", onData);
|
|
||||||
stream.on("error", onError);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("runCommandWithTimeout", () => {
|
describe("runCommandWithTimeout", () => {
|
||||||
it("never enables shell execution (Windows cmd.exe injection hardening)", () => {
|
it("never enables shell execution (Windows cmd.exe injection hardening)", () => {
|
||||||
expect(
|
expect(
|
||||||
@@ -66,7 +25,7 @@ describe("runCommandWithTimeout", () => {
|
|||||||
'process.stdout.write((process.env.OPENCLAW_BASE_ENV ?? "") + "|" + (process.env.OPENCLAW_TEST_ENV ?? ""))',
|
'process.stdout.write((process.env.OPENCLAW_BASE_ENV ?? "") + "|" + (process.env.OPENCLAW_TEST_ENV ?? ""))',
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
timeoutMs: 1_000,
|
timeoutMs: 400,
|
||||||
env: { OPENCLAW_TEST_ENV: "ok" },
|
env: { OPENCLAW_TEST_ENV: "ok" },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -79,10 +38,10 @@ describe("runCommandWithTimeout", () => {
|
|||||||
|
|
||||||
it("kills command when no output timeout elapses", async () => {
|
it("kills command when no output timeout elapses", async () => {
|
||||||
const result = await runCommandWithTimeout(
|
const result = await runCommandWithTimeout(
|
||||||
[process.execPath, "-e", "setTimeout(() => {}, 60)"],
|
[process.execPath, "-e", "setTimeout(() => {}, 30)"],
|
||||||
{
|
{
|
||||||
timeoutMs: 500,
|
timeoutMs: 220,
|
||||||
noOutputTimeoutMs: 12,
|
noOutputTimeoutMs: 8,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -105,13 +64,13 @@ describe("runCommandWithTimeout", () => {
|
|||||||
"clearInterval(ticker);",
|
"clearInterval(ticker);",
|
||||||
"process.exit(0);",
|
"process.exit(0);",
|
||||||
"}",
|
"}",
|
||||||
"}, 10);",
|
"}, 6);",
|
||||||
].join(" "),
|
].join(" "),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
timeoutMs: 2_000,
|
timeoutMs: 600,
|
||||||
// Keep a healthy margin above the emit interval while avoiding long idle waits.
|
// Keep a healthy margin above the emit interval while avoiding long idle waits.
|
||||||
noOutputTimeoutMs: 70,
|
noOutputTimeoutMs: 60,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -123,9 +82,9 @@ describe("runCommandWithTimeout", () => {
|
|||||||
|
|
||||||
it("reports global timeout termination when overall timeout elapses", async () => {
|
it("reports global timeout termination when overall timeout elapses", async () => {
|
||||||
const result = await runCommandWithTimeout(
|
const result = await runCommandWithTimeout(
|
||||||
[process.execPath, "-e", "setTimeout(() => {}, 40)"],
|
[process.execPath, "-e", "setTimeout(() => {}, 20)"],
|
||||||
{
|
{
|
||||||
timeoutMs: 15,
|
timeoutMs: 10,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -145,62 +104,38 @@ describe("runCommandWithTimeout", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("attachChildProcessBridge", () => {
|
describe("attachChildProcessBridge", () => {
|
||||||
const children: Array<{ kill: (signal?: NodeJS.Signals) => boolean }> = [];
|
function createFakeChild() {
|
||||||
const detachments: Array<() => void> = [];
|
const emitter = new EventEmitter() as EventEmitter & ChildProcess;
|
||||||
|
const kill = vi.fn<(signal?: NodeJS.Signals) => boolean>(() => true);
|
||||||
afterEach(() => {
|
emitter.kill = kill as ChildProcess["kill"];
|
||||||
for (const detach of detachments) {
|
return { child: emitter, kill };
|
||||||
try {
|
}
|
||||||
detach();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
detachments.length = 0;
|
|
||||||
for (const child of children) {
|
|
||||||
try {
|
|
||||||
child.kill("SIGKILL");
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
children.length = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("forwards SIGTERM to the wrapped child", async () => {
|
|
||||||
const childPath = path.resolve(process.cwd(), "test/fixtures/child-process-bridge/child.js");
|
|
||||||
|
|
||||||
|
it("forwards SIGTERM to the wrapped child and detaches on exit", () => {
|
||||||
const beforeSigterm = new Set(process.listeners("SIGTERM"));
|
const beforeSigterm = new Set(process.listeners("SIGTERM"));
|
||||||
const child = spawn(process.execPath, [childPath], {
|
const { child, kill } = createFakeChild();
|
||||||
stdio: ["ignore", "pipe", "inherit"],
|
const observedSignals: NodeJS.Signals[] = [];
|
||||||
env: process.env,
|
|
||||||
|
const { detach } = attachChildProcessBridge(child, {
|
||||||
|
signals: ["SIGTERM"],
|
||||||
|
onSignal: (signal) => observedSignals.push(signal),
|
||||||
});
|
});
|
||||||
const { detach } = attachChildProcessBridge(child);
|
|
||||||
detachments.push(detach);
|
|
||||||
children.push(child);
|
|
||||||
const afterSigterm = process.listeners("SIGTERM");
|
const afterSigterm = process.listeners("SIGTERM");
|
||||||
const addedSigterm = afterSigterm.find((listener) => !beforeSigterm.has(listener));
|
const addedSigterm = afterSigterm.find((listener) => !beforeSigterm.has(listener));
|
||||||
|
|
||||||
if (!child.stdout) {
|
|
||||||
throw new Error("expected stdout");
|
|
||||||
}
|
|
||||||
const ready = await waitForLine(child.stdout);
|
|
||||||
expect(ready).toBe("ready");
|
|
||||||
|
|
||||||
if (!addedSigterm) {
|
if (!addedSigterm) {
|
||||||
throw new Error("expected SIGTERM listener");
|
throw new Error("expected SIGTERM listener");
|
||||||
}
|
}
|
||||||
addedSigterm("SIGTERM");
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
addedSigterm();
|
||||||
const timeout = setTimeout(
|
expect(observedSignals).toEqual(["SIGTERM"]);
|
||||||
() => reject(new Error("timeout waiting for child exit")),
|
expect(kill).toHaveBeenCalledWith("SIGTERM");
|
||||||
CHILD_EXIT_TIMEOUT_MS,
|
|
||||||
);
|
child.emit("exit");
|
||||||
child.once("exit", () => {
|
expect(process.listeners("SIGTERM")).toHaveLength(beforeSigterm.size);
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve();
|
// Detached already via exit; should remain a safe no-op.
|
||||||
});
|
detach();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createProcessSupervisor } from "./supervisor.js";
|
|||||||
type ProcessSupervisor = ReturnType<typeof createProcessSupervisor>;
|
type ProcessSupervisor = ReturnType<typeof createProcessSupervisor>;
|
||||||
type SpawnOptions = Parameters<ProcessSupervisor["spawn"]>[0];
|
type SpawnOptions = Parameters<ProcessSupervisor["spawn"]>[0];
|
||||||
type ChildSpawnOptions = Omit<Extract<SpawnOptions, { mode: "child" }>, "backendId" | "mode">;
|
type ChildSpawnOptions = Omit<Extract<SpawnOptions, { mode: "child" }>, "backendId" | "mode">;
|
||||||
const OUTPUT_DELAY_MS = 8;
|
const OUTPUT_DELAY_MS = 6;
|
||||||
|
|
||||||
async function spawnChild(supervisor: ProcessSupervisor, options: ChildSpawnOptions) {
|
async function spawnChild(supervisor: ProcessSupervisor, options: ChildSpawnOptions) {
|
||||||
return supervisor.spawn({
|
return supervisor.spawn({
|
||||||
@@ -25,7 +25,7 @@ describe("process supervisor", () => {
|
|||||||
"-e",
|
"-e",
|
||||||
`setTimeout(() => process.stdout.write("ok"), ${OUTPUT_DELAY_MS})`,
|
`setTimeout(() => process.stdout.write("ok"), ${OUTPUT_DELAY_MS})`,
|
||||||
],
|
],
|
||||||
timeoutMs: 2_000,
|
timeoutMs: 1_000,
|
||||||
stdinMode: "pipe-closed",
|
stdinMode: "pipe-closed",
|
||||||
});
|
});
|
||||||
const exit = await run.wait();
|
const exit = await run.wait();
|
||||||
@@ -38,9 +38,9 @@ describe("process supervisor", () => {
|
|||||||
const supervisor = createProcessSupervisor();
|
const supervisor = createProcessSupervisor();
|
||||||
const run = await spawnChild(supervisor, {
|
const run = await spawnChild(supervisor, {
|
||||||
sessionId: "s1",
|
sessionId: "s1",
|
||||||
argv: [process.execPath, "-e", "setTimeout(() => {}, 24)"],
|
argv: [process.execPath, "-e", "setTimeout(() => {}, 18)"],
|
||||||
timeoutMs: 500,
|
timeoutMs: 300,
|
||||||
noOutputTimeoutMs: 8,
|
noOutputTimeoutMs: 6,
|
||||||
stdinMode: "pipe-closed",
|
stdinMode: "pipe-closed",
|
||||||
});
|
});
|
||||||
const exit = await run.wait();
|
const exit = await run.wait();
|
||||||
@@ -54,8 +54,8 @@ describe("process supervisor", () => {
|
|||||||
const first = await spawnChild(supervisor, {
|
const first = await spawnChild(supervisor, {
|
||||||
sessionId: "s1",
|
sessionId: "s1",
|
||||||
scopeKey: "scope:a",
|
scopeKey: "scope:a",
|
||||||
argv: [process.execPath, "-e", "setTimeout(() => {}, 500)"],
|
argv: [process.execPath, "-e", "setTimeout(() => {}, 120)"],
|
||||||
timeoutMs: 2_000,
|
timeoutMs: 1_000,
|
||||||
stdinMode: "pipe-open",
|
stdinMode: "pipe-open",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ describe("process supervisor", () => {
|
|||||||
"-e",
|
"-e",
|
||||||
`setTimeout(() => process.stdout.write("new"), ${OUTPUT_DELAY_MS})`,
|
`setTimeout(() => process.stdout.write("new"), ${OUTPUT_DELAY_MS})`,
|
||||||
],
|
],
|
||||||
timeoutMs: 2_000,
|
timeoutMs: 1_000,
|
||||||
stdinMode: "pipe-closed",
|
stdinMode: "pipe-closed",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ describe("process supervisor", () => {
|
|||||||
"-e",
|
"-e",
|
||||||
`setTimeout(() => process.stdout.write("streamed"), ${OUTPUT_DELAY_MS})`,
|
`setTimeout(() => process.stdout.write("streamed"), ${OUTPUT_DELAY_MS})`,
|
||||||
],
|
],
|
||||||
timeoutMs: 2_000,
|
timeoutMs: 1_000,
|
||||||
stdinMode: "pipe-closed",
|
stdinMode: "pipe-closed",
|
||||||
captureOutput: false,
|
captureOutput: false,
|
||||||
onStdout: (chunk) => {
|
onStdout: (chunk) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user