216 lines
7.3 KiB
TypeScript
216 lines
7.3 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { resolveMainSessionKeyFromConfig } from "../config/sessions.js";
|
|
import { drainSystemEvents, peekSystemEvents } from "../infra/system-events.js";
|
|
import {
|
|
cronIsolatedRun,
|
|
getFreePort,
|
|
installGatewayTestHooks,
|
|
startGatewayServer,
|
|
testState,
|
|
waitForSystemEvent,
|
|
} from "./test-helpers.js";
|
|
|
|
installGatewayTestHooks();
|
|
|
|
const resolveMainKey = () => resolveMainSessionKeyFromConfig();
|
|
|
|
describe("gateway server hooks", () => {
|
|
test("hooks wake requires auth", async () => {
|
|
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
|
const port = await getFreePort();
|
|
const server = await startGatewayServer(port);
|
|
const res = await fetch(`http://127.0.0.1:${port}/hooks/wake`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ text: "Ping" }),
|
|
});
|
|
expect(res.status).toBe(401);
|
|
await server.close();
|
|
});
|
|
|
|
test("hooks wake enqueues system event", async () => {
|
|
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
|
const port = await getFreePort();
|
|
const server = await startGatewayServer(port);
|
|
const res = await fetch(`http://127.0.0.1:${port}/hooks/wake`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: "Bearer hook-secret",
|
|
},
|
|
body: JSON.stringify({ text: "Ping", mode: "next-heartbeat" }),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const events = await waitForSystemEvent();
|
|
expect(events.some((e) => e.includes("Ping"))).toBe(true);
|
|
drainSystemEvents(resolveMainKey());
|
|
await server.close();
|
|
});
|
|
|
|
test("hooks agent posts summary to main", async () => {
|
|
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
|
cronIsolatedRun.mockResolvedValueOnce({
|
|
status: "ok",
|
|
summary: "done",
|
|
});
|
|
const port = await getFreePort();
|
|
const server = await startGatewayServer(port);
|
|
const res = await fetch(`http://127.0.0.1:${port}/hooks/agent`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: "Bearer hook-secret",
|
|
},
|
|
body: JSON.stringify({ message: "Do it", name: "Email" }),
|
|
});
|
|
expect(res.status).toBe(202);
|
|
const events = await waitForSystemEvent();
|
|
expect(events.some((e) => e.includes("Hook Email: done"))).toBe(true);
|
|
drainSystemEvents(resolveMainKey());
|
|
await server.close();
|
|
});
|
|
|
|
test("hooks agent forwards model override", async () => {
|
|
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
|
cronIsolatedRun.mockClear();
|
|
cronIsolatedRun.mockResolvedValueOnce({
|
|
status: "ok",
|
|
summary: "done",
|
|
});
|
|
const port = await getFreePort();
|
|
const server = await startGatewayServer(port);
|
|
const res = await fetch(`http://127.0.0.1:${port}/hooks/agent`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: "Bearer hook-secret",
|
|
},
|
|
body: JSON.stringify({
|
|
message: "Do it",
|
|
name: "Email",
|
|
model: "openai/gpt-4.1-mini",
|
|
}),
|
|
});
|
|
expect(res.status).toBe(202);
|
|
await waitForSystemEvent();
|
|
const call = cronIsolatedRun.mock.calls[0]?.[0] as {
|
|
job?: { payload?: { model?: string } };
|
|
};
|
|
expect(call?.job?.payload?.model).toBe("openai/gpt-4.1-mini");
|
|
drainSystemEvents(resolveMainKey());
|
|
await server.close();
|
|
});
|
|
|
|
test("hooks wake accepts query token", async () => {
|
|
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
|
const port = await getFreePort();
|
|
const server = await startGatewayServer(port);
|
|
const res = await fetch(`http://127.0.0.1:${port}/hooks/wake?token=hook-secret`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ text: "Query auth" }),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const events = await waitForSystemEvent();
|
|
expect(events.some((e) => e.includes("Query auth"))).toBe(true);
|
|
drainSystemEvents(resolveMainKey());
|
|
await server.close();
|
|
});
|
|
|
|
test("hooks agent rejects invalid channel", async () => {
|
|
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
|
const port = await getFreePort();
|
|
const server = await startGatewayServer(port);
|
|
const res = await fetch(`http://127.0.0.1:${port}/hooks/agent`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: "Bearer hook-secret",
|
|
},
|
|
body: JSON.stringify({ message: "Nope", channel: "sms" }),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
expect(peekSystemEvents(resolveMainKey()).length).toBe(0);
|
|
await server.close();
|
|
});
|
|
|
|
test("hooks wake accepts x-clawdbot-token header", async () => {
|
|
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
|
const port = await getFreePort();
|
|
const server = await startGatewayServer(port);
|
|
const res = await fetch(`http://127.0.0.1:${port}/hooks/wake`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-clawdbot-token": "hook-secret",
|
|
},
|
|
body: JSON.stringify({ text: "Header auth" }),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const events = await waitForSystemEvent();
|
|
expect(events.some((e) => e.includes("Header auth"))).toBe(true);
|
|
drainSystemEvents(resolveMainKey());
|
|
await server.close();
|
|
});
|
|
|
|
test("hooks rejects non-post", async () => {
|
|
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
|
const port = await getFreePort();
|
|
const server = await startGatewayServer(port);
|
|
const res = await fetch(`http://127.0.0.1:${port}/hooks/wake`, {
|
|
method: "GET",
|
|
headers: { Authorization: "Bearer hook-secret" },
|
|
});
|
|
expect(res.status).toBe(405);
|
|
await server.close();
|
|
});
|
|
|
|
test("hooks wake requires text", async () => {
|
|
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
|
const port = await getFreePort();
|
|
const server = await startGatewayServer(port);
|
|
const res = await fetch(`http://127.0.0.1:${port}/hooks/wake`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: "Bearer hook-secret",
|
|
},
|
|
body: JSON.stringify({ text: " " }),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
await server.close();
|
|
});
|
|
|
|
test("hooks agent requires message", async () => {
|
|
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
|
const port = await getFreePort();
|
|
const server = await startGatewayServer(port);
|
|
const res = await fetch(`http://127.0.0.1:${port}/hooks/agent`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: "Bearer hook-secret",
|
|
},
|
|
body: JSON.stringify({ message: " " }),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
await server.close();
|
|
});
|
|
|
|
test("hooks rejects invalid json", async () => {
|
|
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
|
const port = await getFreePort();
|
|
const server = await startGatewayServer(port);
|
|
const res = await fetch(`http://127.0.0.1:${port}/hooks/wake`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: "Bearer hook-secret",
|
|
},
|
|
body: "{",
|
|
});
|
|
expect(res.status).toBe(400);
|
|
await server.close();
|
|
});
|
|
});
|