diff --git a/scripts/e2e/cron-mcp-cleanup-docker-client.ts b/scripts/e2e/cron-mcp-cleanup-docker-client.ts index 0969d48cf90..e699c1e1397 100644 --- a/scripts/e2e/cron-mcp-cleanup-docker-client.ts +++ b/scripts/e2e/cron-mcp-cleanup-docker-client.ts @@ -4,15 +4,36 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { setTimeout as delay } from "node:timers/promises"; +import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; -import { assert, connectGateway, type GatewayRpcClient, waitFor } from "./mcp-channels-harness.ts"; +import type { GatewayRpcClient } from "./mcp-channels-harness.ts"; const execFileAsync = promisify(execFile); +const PROBE_PID_WAIT_MS = readPositiveInt( + process.env.OPENCLAW_CRON_MCP_CLEANUP_PID_WAIT_MS, + 120_000, +); +type McpChannelsHarness = typeof import("./mcp-channels-harness.ts"); +let mcpChannelsHarness: McpChannelsHarness | undefined; type CronJob = { id?: string }; type CronRunResult = { ok?: boolean; enqueued?: boolean; runId?: string }; type AgentRunResult = { runId?: string; status?: string }; +async function loadMcpChannelsHarness(): Promise { + mcpChannelsHarness ??= await import("./mcp-channels-harness.ts"); + return mcpChannelsHarness; +} + +function readPositiveInt(raw: string | undefined, fallback: number): number { + const text = (raw ?? "").trim(); + if (!/^\d+$/u.test(text)) { + return fallback; + } + const parsed = Number(text); + return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; +} + async function readProbePid(pidPath: string): Promise { try { const raw = (await fs.readFile(pidPath, "utf-8")).trim(); @@ -52,14 +73,19 @@ async function describeProbePid(pid: number): Promise { } } -async function waitForProbePid(pidPath: string): Promise { +export async function waitForProbePid( + pidPath: string, + options: { pollMs?: number; timeoutMs?: number } = {}, +): Promise { + const timeoutMs = options.timeoutMs ?? PROBE_PID_WAIT_MS; + const pollMs = options.pollMs ?? 100; const startedAt = Date.now(); - while (Date.now() - startedAt < 600_000) { + while (Date.now() - startedAt < timeoutMs) { const pid = await readProbePid(pidPath); if (pid) { return pid; } - await delay(100); + await delay(pollMs); } return undefined; } @@ -128,6 +154,7 @@ async function runCronCleanupScenario(params: { gateway: GatewayRpcClient; pidPath: string; }): Promise<{ jobId: string; runId?: string; pid: number; status?: unknown }> { + const { assert, waitFor } = await loadMcpChannelsHarness(); const { gateway, pidPath } = params; const job = await gateway.request("cron.add", { name: "cron mcp cleanup docker e2e", @@ -171,7 +198,7 @@ async function runCronCleanupScenario(params: { const pid = await waitForProbePid(pidPath); assert( pid, - `cron MCP probe did not start; missing pid file at ${pidPath}; events=${JSON.stringify( + `cron MCP probe did not start within ${PROBE_PID_WAIT_MS}ms; missing pid file at ${pidPath}; events=${JSON.stringify( gateway.events.slice(-10), )}`, ); @@ -209,6 +236,7 @@ async function runSubagentCleanupScenario(params: { pidsPath: string; exitPath: string; }): Promise<{ runId: string; exitedPids: number[]; pids: number[] }> { + const { assert } = await loadMcpChannelsHarness(); const { gateway, pidPath, pidsPath, exitPath } = params; await resetProbeFiles({ pidPath, pidsPath, exitPath }); @@ -258,6 +286,7 @@ async function runSubagentCleanupScenario(params: { } async function main() { + const { assert, connectGateway } = await loadMcpChannelsHarness(); const gatewayUrl = process.env.GW_URL?.trim(); const gatewayToken = process.env.GW_TOKEN?.trim(); const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw"); @@ -283,4 +312,6 @@ async function main() { } } -await main(); +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await main(); +} diff --git a/test/scripts/cron-mcp-cleanup-docker-client.test.ts b/test/scripts/cron-mcp-cleanup-docker-client.test.ts new file mode 100644 index 00000000000..4abe29b280b --- /dev/null +++ b/test/scripts/cron-mcp-cleanup-docker-client.test.ts @@ -0,0 +1,20 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { waitForProbePid } from "../../scripts/e2e/cron-mcp-cleanup-docker-client.ts"; + +describe("cron MCP cleanup docker client", () => { + it("bounds missing probe pid waits", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cron-mcp-client-")); + try { + const startedAt = Date.now(); + await expect( + waitForProbePid(path.join(root, "missing.pid"), { pollMs: 1, timeoutMs: 20 }), + ).resolves.toBeUndefined(); + expect(Date.now() - startedAt).toBeLessThan(1000); + } finally { + fs.rmSync(root, { force: true, recursive: true }); + } + }); +});