test(runtime): trim timer-heavy regression suites

This commit is contained in:
Peter Steinberger
2026-03-02 09:45:57 +00:00
parent fd4d157e45
commit 04030ddf68
4 changed files with 27 additions and 50 deletions

View File

@@ -115,7 +115,7 @@ function createIsolatedRegressionJob(params: {
} }
async function writeCronJobs(storePath: string, jobs: CronJob[]) { async function writeCronJobs(storePath: string, jobs: CronJob[]) {
await fs.writeFile(storePath, JSON.stringify({ version: 1, jobs }, null, 2), "utf-8"); await fs.writeFile(storePath, JSON.stringify({ version: 1, jobs }), "utf-8");
} }
async function startCronForStore(params: { async function startCronForStore(params: {
@@ -665,7 +665,7 @@ describe("Cron issue regressions", () => {
if (targetJob?.delivery?.channel === "telegram") { if (targetJob?.delivery?.channel === "telegram") {
targetJob.delivery.to = rewrittenTarget; targetJob.delivery.to = rewrittenTarget;
} }
await fs.writeFile(store.storePath, JSON.stringify(persisted, null, 2), "utf-8"); await fs.writeFile(store.storePath, JSON.stringify(persisted), "utf-8");
return { status: "ok" as const, summary: "done", delivered: true }; return { status: "ok" as const, summary: "done", delivered: true };
}); });
@@ -736,11 +736,7 @@ describe("Cron issue regressions", () => {
]; ];
for (const { id, state } of terminalStates) { for (const { id, state } of terminalStates) {
const job: CronJob = { id, ...baseJob, state }; const job: CronJob = { id, ...baseJob, state };
await fs.writeFile( await fs.writeFile(store.storePath, JSON.stringify({ version: 1, jobs: [job] }), "utf-8");
store.storePath,
JSON.stringify({ version: 1, jobs: [job] }, null, 2),
"utf-8",
);
const enqueueSystemEvent = vi.fn(); const enqueueSystemEvent = vi.fn();
const cron = await startCronForStore({ const cron = await startCronForStore({
storePath: store.storePath, storePath: store.storePath,
@@ -1410,7 +1406,7 @@ describe("Cron issue regressions", () => {
const second = createDueIsolatedJob({ id: "batch-second", nowMs: dueAt, nextRunAtMs: dueAt }); const second = createDueIsolatedJob({ id: "batch-second", nowMs: dueAt, nextRunAtMs: dueAt });
await fs.writeFile( await fs.writeFile(
store.storePath, store.storePath,
JSON.stringify({ version: 1, jobs: [first, second] }, null, 2), JSON.stringify({ version: 1, jobs: [first, second] }),
"utf-8", "utf-8",
); );
@@ -1460,7 +1456,7 @@ describe("Cron issue regressions", () => {
}); });
await fs.writeFile( await fs.writeFile(
store.storePath, store.storePath,
JSON.stringify({ version: 1, jobs: [first, second] }, null, 2), JSON.stringify({ version: 1, jobs: [first, second] }),
"utf-8", "utf-8",
); );
@@ -1524,9 +1520,9 @@ describe("Cron issue regressions", () => {
const store = await makeStorePath(); const store = await makeStorePath();
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z"); const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
// Use a short but observable timeout: 300 ms. // Keep this short for suite speed while still separating expected timeout
// Before the fix, premature timeout would fire at ~100 ms (1/3 of 300 ms). // from the 1/3-regression timeout.
const timeoutSeconds = 0.3; const timeoutSeconds = 0.16;
const cronJob = createIsolatedRegressionJob({ const cronJob = createIsolatedRegressionJob({
id: "timeout-fraction-29774", id: "timeout-fraction-29774",
name: "timeout fraction regression", name: "timeout fraction regression",
@@ -1537,10 +1533,10 @@ describe("Cron issue regressions", () => {
}); });
await writeCronJobs(store.storePath, [cronJob]); await writeCronJobs(store.storePath, [cronJob]);
const tempFile = path.join(os.tmpdir(), `cron-29774-${Date.now()}.txt`);
let now = scheduledAt; let now = scheduledAt;
const wallStart = Date.now(); const wallStart = Date.now();
let abortWallMs: number | undefined; let abortWallMs: number | undefined;
let started = false;
const state = createCronServiceState({ const state = createCronServiceState({
cronEnabled: true, cronEnabled: true,
@@ -1550,8 +1546,7 @@ describe("Cron issue regressions", () => {
enqueueSystemEvent: vi.fn(), enqueueSystemEvent: vi.fn(),
requestHeartbeatNow: vi.fn(), requestHeartbeatNow: vi.fn(),
runIsolatedAgentJob: vi.fn(async ({ abortSignal }: { abortSignal?: AbortSignal }) => { runIsolatedAgentJob: vi.fn(async ({ abortSignal }: { abortSignal?: AbortSignal }) => {
// Real side effect: confirm the job actually started. started = true;
await fs.writeFile(tempFile, "started", "utf-8");
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
if (!abortSignal) { if (!abortSignal) {
resolve(); resolve();
@@ -1578,15 +1573,12 @@ describe("Cron issue regressions", () => {
await onTimer(state); await onTimer(state);
// Confirm job started (real side effect). expect(started).toBe(true);
await expect(fs.readFile(tempFile, "utf-8")).resolves.toBe("started");
await fs.unlink(tempFile).catch(() => {});
// The outer cron timeout fires at timeoutSeconds * 1000 = 300 ms. // The abort must not fire at the old ~1/3 regression value.
// The abort must not have fired at ~100 ms (the 1/3 regression value). // Keep the lower bound conservative for loaded CI runners.
// Allow generous lower bound (80%) to keep the test stable on loaded CI runners.
const elapsedMs = (abortWallMs ?? Date.now()) - wallStart; const elapsedMs = (abortWallMs ?? Date.now()) - wallStart;
expect(elapsedMs).toBeGreaterThanOrEqual(timeoutSeconds * 1000 * 0.8); expect(elapsedMs).toBeGreaterThanOrEqual(timeoutSeconds * 1000 * 0.7);
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");

View File

@@ -234,21 +234,6 @@ describe("loadOpenClawPlugins", () => {
const bundled = registry.plugins.find((entry) => entry.id === "bundled"); const bundled = registry.plugins.find((entry) => entry.id === "bundled");
expect(bundled?.status).toBe("disabled"); expect(bundled?.status).toBe("disabled");
const enabledRegistry = loadOpenClawPlugins({
cache: false,
config: {
plugins: {
allow: ["bundled"],
entries: {
bundled: { enabled: true },
},
},
},
});
const enabled = enabledRegistry.plugins.find((entry) => entry.id === "bundled");
expect(enabled?.status).toBe("loaded");
}); });
it("loads bundled telegram plugin when enabled", () => { it("loads bundled telegram plugin when enabled", () => {

View File

@@ -6,8 +6,8 @@ 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 = 4_000; const CHILD_READY_TIMEOUT_MS = 2_000;
const CHILD_EXIT_TIMEOUT_MS = 4_000; const CHILD_EXIT_TIMEOUT_MS = 2_000;
function waitForLine( function waitForLine(
stream: NodeJS.ReadableStream, stream: NodeJS.ReadableStream,
@@ -79,10 +79,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(() => {}, 40)"], [process.execPath, "-e", "setTimeout(() => {}, 80)"],
{ {
timeoutMs: 500, timeoutMs: 500,
noOutputTimeoutMs: 20, noOutputTimeoutMs: 25,
}, },
); );
@@ -101,24 +101,24 @@ describe("runCommandWithTimeout", () => {
"let count = 0;", "let count = 0;",
'const ticker = setInterval(() => { process.stdout.write(".");', 'const ticker = setInterval(() => { process.stdout.write(".");',
"count += 1;", "count += 1;",
"if (count === 6) {", "if (count === 3) {",
"clearInterval(ticker);", "clearInterval(ticker);",
"process.exit(0);", "process.exit(0);",
"}", "}",
"}, 25);", "}, 15);",
].join(" "), ].join(" "),
], ],
{ {
timeoutMs: 3_000, timeoutMs: 3_000,
// Keep a healthy margin above the emit interval while avoiding a 1s+ test delay. // Keep a healthy margin above the emit interval while avoiding long idle waits.
noOutputTimeoutMs: 400, noOutputTimeoutMs: 120,
}, },
); );
expect(result.code ?? 0).toBe(0); expect(result.code ?? 0).toBe(0);
expect(result.termination).toBe("exit"); expect(result.termination).toBe("exit");
expect(result.noOutputTimedOut).toBe(false); expect(result.noOutputTimedOut).toBe(false);
expect(result.stdout.length).toBeGreaterThanOrEqual(7); expect(result.stdout.length).toBeGreaterThanOrEqual(4);
}); });
it("reports global timeout termination when overall timeout elapses", async () => { it("reports global timeout termination when overall timeout elapses", async () => {

View File

@@ -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 = 40; const OUTPUT_DELAY_MS = 15;
async function spawnChild(supervisor: ProcessSupervisor, options: ChildSpawnOptions) { async function spawnChild(supervisor: ProcessSupervisor, options: ChildSpawnOptions) {
return supervisor.spawn({ return supervisor.spawn({
@@ -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(() => {}, 40)"], argv: [process.execPath, "-e", "setTimeout(() => {}, 30)"],
timeoutMs: 500, timeoutMs: 500,
noOutputTimeoutMs: 20, noOutputTimeoutMs: 12,
stdinMode: "pipe-closed", stdinMode: "pipe-closed",
}); });
const exit = await run.wait(); const exit = await run.wait();
@@ -84,7 +84,7 @@ describe("process supervisor", () => {
const supervisor = createProcessSupervisor(); const supervisor = createProcessSupervisor();
const run = await spawnChild(supervisor, { const run = await spawnChild(supervisor, {
sessionId: "s-timeout", sessionId: "s-timeout",
argv: [process.execPath, "-e", "setTimeout(() => {}, 40)"], argv: [process.execPath, "-e", "setTimeout(() => {}, 20)"],
timeoutMs: 1, timeoutMs: 1,
stdinMode: "pipe-closed", stdinMode: "pipe-closed",
}); });