From 8225b9edbb1c3552cfea0b3d902ef96cce06d7ed Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Mar 2026 00:21:42 +0000 Subject: [PATCH] test: share gateway hook and cron helpers --- src/gateway/server.cron.test.ts | 76 +++++++---------- src/gateway/server.hooks.test.ts | 136 ++++++++++++------------------- 2 files changed, 81 insertions(+), 131 deletions(-) diff --git a/src/gateway/server.cron.test.ts b/src/gateway/server.cron.test.ts index 2590f63c23d..efdbecba004 100644 --- a/src/gateway/server.cron.test.ts +++ b/src/gateway/server.cron.test.ts @@ -179,6 +179,13 @@ async function addWebhookCronJob(params: { return expectCronJobIdFromResponse(response); } +async function writeCronConfig(config: unknown) { + const configPath = process.env.OPENCLAW_CONFIG_PATH; + expect(typeof configPath).toBe("string"); + await fs.mkdir(path.dirname(configPath as string), { recursive: true }); + await fs.writeFile(configPath as string, JSON.stringify(config, null, 2), "utf-8"); +} + async function runCronJobForce(ws: WebSocket, id: string) { const response = await rpcReq(ws, "cron.run", { id, mode: "force" }, 20_000); expect(response.ok).toBe(true); @@ -186,6 +193,15 @@ async function runCronJobForce(ws: WebSocket, id: string) { return response; } +async function runCronJobAndWaitForFinished(ws: WebSocket, jobId: string) { + const finished = waitForCronEvent( + ws, + (payload) => payload?.jobId === jobId && payload?.action === "finished", + ); + await runCronJobForce(ws, jobId); + await finished; +} + function getWebhookCall(index: number) { const [args] = fetchWithSsrFGuardMock.mock.calls[index] as unknown as [ { @@ -720,23 +736,12 @@ describe("gateway server cron", () => { jobs: [legacyNotifyJob], }); - const configPath = process.env.OPENCLAW_CONFIG_PATH; - expect(typeof configPath).toBe("string"); - await fs.mkdir(path.dirname(configPath as string), { recursive: true }); - await fs.writeFile( - configPath as string, - JSON.stringify( - { - cron: { - webhook: "https://legacy.example.invalid/cron-finished", - webhookToken: "cron-webhook-token", - }, - }, - null, - 2, - ), - "utf-8", - ); + await writeCronConfig({ + cron: { + webhook: "https://legacy.example.invalid/cron-finished", + webhookToken: "cron-webhook-token", + }, + }); fetchWithSsrFGuardMock.mockClear(); @@ -760,12 +765,7 @@ describe("gateway server cron", () => { name: "webhook enabled", delivery: { mode: "webhook", to: "https://example.invalid/cron-finished" }, }); - const notifyFinished = waitForCronEvent( - ws, - (payload) => payload?.jobId === notifyJobId && payload?.action === "finished", - ); - await runCronJobForce(ws, notifyJobId); - await notifyFinished; + await runCronJobAndWaitForFinished(ws, notifyJobId); const notifyCall = getWebhookCall(0); expect(notifyCall.url).toBe("https://example.invalid/cron-finished"); expect(notifyCall.init.method).toBe("POST"); @@ -899,24 +899,13 @@ describe("gateway server cron", () => { cronEnabled: false, }); - const configPath = process.env.OPENCLAW_CONFIG_PATH; - expect(typeof configPath).toBe("string"); - await fs.mkdir(path.dirname(configPath as string), { recursive: true }); - await fs.writeFile( - configPath as string, - JSON.stringify( - { - cron: { - webhookToken: { - opaque: true, - }, - }, + await writeCronConfig({ + cron: { + webhookToken: { + opaque: true, }, - null, - 2, - ), - "utf-8", - ); + }, + }); fetchWithSsrFGuardMock.mockClear(); @@ -929,12 +918,7 @@ describe("gateway server cron", () => { name: "webhook secretinput object", delivery: { mode: "webhook", to: "https://example.invalid/cron-finished" }, }); - const notifyFinished = waitForCronEvent( - ws, - (payload) => payload?.jobId === notifyJobId && payload?.action === "finished", - ); - await runCronJobForce(ws, notifyJobId); - await notifyFinished; + await runCronJobAndWaitForFinished(ws, notifyJobId); const [notifyArgs] = fetchWithSsrFGuardMock.mock.calls[0] as unknown as [ { url?: string; diff --git a/src/gateway/server.hooks.test.ts b/src/gateway/server.hooks.test.ts index 612e7db865b..943565a9b50 100644 --- a/src/gateway/server.hooks.test.ts +++ b/src/gateway/server.hooks.test.ts @@ -62,6 +62,42 @@ function mockIsolatedRunOkOnce(): void { }); } +function mockIsolatedRunOk(): void { + cronIsolatedRun.mockClear(); + cronIsolatedRun.mockResolvedValue({ + status: "ok", + summary: "done", + }); +} + +async function postAgentHookWithIdempotency( + port: number, + idempotencyKey: string, + headers?: Record, +) { + const response = await postHook( + port, + "/hooks/agent", + { message: "Do it", name: "Email" }, + { headers: { "Idempotency-Key": idempotencyKey, ...headers } }, + ); + expect(response.status).toBe(200); + return response; +} + +async function expectFirstHookDelivery( + port: number, + idempotencyKey: string, + headers?: Record, +) { + const first = await postAgentHookWithIdempotency(port, idempotencyKey, headers); + const firstBody = (await first.json()) as { runId?: string }; + expect(firstBody.runId).toBeTruthy(); + await waitForSystemEvent(); + drainSystemEvents(resolveMainKey()); + return firstBody; +} + describe("gateway server hooks", () => { test("handles auth, wake, and agent flows", async () => { testState.hooksConfig = { enabled: true, token: HOOK_TOKEN }; @@ -288,29 +324,11 @@ describe("gateway server hooks", () => { test("dedupes repeated /hooks/agent deliveries by idempotency key", async () => { testState.hooksConfig = { enabled: true, token: HOOK_TOKEN }; await withGatewayServer(async ({ port }) => { - cronIsolatedRun.mockClear(); - cronIsolatedRun.mockResolvedValue({ status: "ok", summary: "done" }); - - const first = await postHook( - port, - "/hooks/agent", - { message: "Do it", name: "Email" }, - { headers: { "Idempotency-Key": "hook-idem-1" } }, - ); - expect(first.status).toBe(200); - const firstBody = (await first.json()) as { runId?: string }; - expect(firstBody.runId).toBeTruthy(); - await waitForSystemEvent(); + mockIsolatedRunOk(); + const firstBody = await expectFirstHookDelivery(port, "hook-idem-1"); expect(cronIsolatedRun).toHaveBeenCalledTimes(1); - drainSystemEvents(resolveMainKey()); - const second = await postHook( - port, - "/hooks/agent", - { message: "Do it", name: "Email" }, - { headers: { "Idempotency-Key": "hook-idem-1" } }, - ); - expect(second.status).toBe(200); + const second = await postAgentHookWithIdempotency(port, "hook-idem-1"); const secondBody = (await second.json()) as { runId?: string }; expect(secondBody.runId).toBe(firstBody.runId); expect(cronIsolatedRun).toHaveBeenCalledTimes(1); @@ -329,37 +347,13 @@ describe("gateway server hooks", () => { ); await withGatewayServer(async ({ port }) => { - cronIsolatedRun.mockClear(); - cronIsolatedRun.mockResolvedValue({ status: "ok", summary: "done" }); - - const first = await postHook( - port, - "/hooks/agent", - { message: "Do it", name: "Email" }, - { - headers: { - "Idempotency-Key": "hook-idem-forwarded", - "X-Forwarded-For": "198.51.100.10", - }, - }, - ); - expect(first.status).toBe(200); - const firstBody = (await first.json()) as { runId?: string }; - await waitForSystemEvent(); - drainSystemEvents(resolveMainKey()); - - const second = await postHook( - port, - "/hooks/agent", - { message: "Do it", name: "Email" }, - { - headers: { - "Idempotency-Key": "hook-idem-forwarded", - "X-Forwarded-For": "203.0.113.25", - }, - }, - ); - expect(second.status).toBe(200); + mockIsolatedRunOk(); + const firstBody = await expectFirstHookDelivery(port, "hook-idem-forwarded", { + "X-Forwarded-For": "198.51.100.10", + }); + const second = await postAgentHookWithIdempotency(port, "hook-idem-forwarded", { + "X-Forwarded-For": "203.0.113.25", + }); const secondBody = (await second.json()) as { runId?: string }; expect(secondBody.runId).toBe(firstBody.runId); expect(cronIsolatedRun).toHaveBeenCalledTimes(1); @@ -371,26 +365,9 @@ describe("gateway server hooks", () => { const oversizedKey = "x".repeat(257); await withGatewayServer(async ({ port }) => { - cronIsolatedRun.mockClear(); - cronIsolatedRun.mockResolvedValue({ status: "ok", summary: "done" }); - - const first = await postHook( - port, - "/hooks/agent", - { message: "Do it", name: "Email" }, - { headers: { "Idempotency-Key": oversizedKey } }, - ); - expect(first.status).toBe(200); - await waitForSystemEvent(); - drainSystemEvents(resolveMainKey()); - - const second = await postHook( - port, - "/hooks/agent", - { message: "Do it", name: "Email" }, - { headers: { "Idempotency-Key": oversizedKey } }, - ); - expect(second.status).toBe(200); + mockIsolatedRunOk(); + await expectFirstHookDelivery(port, oversizedKey); + await postAgentHookWithIdempotency(port, oversizedKey); await waitForSystemEvent(); expect(cronIsolatedRun).toHaveBeenCalledTimes(2); @@ -403,19 +380,8 @@ describe("gateway server hooks", () => { nowSpy.mockReturnValue(1_000_000); await withGatewayServer(async ({ port }) => { - cronIsolatedRun.mockClear(); - cronIsolatedRun.mockResolvedValue({ status: "ok", summary: "done" }); - - const first = await postHook( - port, - "/hooks/agent", - { message: "Do it", name: "Email" }, - { headers: { "Idempotency-Key": "fixed-window-idem" } }, - ); - expect(first.status).toBe(200); - const firstBody = (await first.json()) as { runId?: string }; - await waitForSystemEvent(); - drainSystemEvents(resolveMainKey()); + mockIsolatedRunOk(); + const firstBody = await expectFirstHookDelivery(port, "fixed-window-idem"); nowSpy.mockReturnValue(1_000_000 + DEDUPE_TTL_MS - 1); const second = await postHook(