test: share gateway hook and cron helpers

This commit is contained in:
Peter Steinberger 2026-03-14 00:21:42 +00:00
parent b64466953a
commit 8225b9edbb
2 changed files with 81 additions and 131 deletions

View File

@ -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;

View File

@ -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<string, string>,
) {
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<string, string>,
) {
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(